Add JSON and JSON/serialized types for Doctrine
[MAILPOET-2216]
This commit is contained in:
@ -3,8 +3,11 @@
|
||||
namespace MailPoet\Doctrine;
|
||||
|
||||
use MailPoet\Config\Env;
|
||||
use MailPoet\Doctrine\Types\JsonOrSerializedType;
|
||||
use MailPoet\Doctrine\Types\JsonType;
|
||||
use MailPoetVendor\Doctrine\DBAL\DriverManager;
|
||||
use MailPoetVendor\Doctrine\DBAL\Platforms\MySqlPlatform;
|
||||
use MailPoetVendor\Doctrine\DBAL\Types\Type;
|
||||
use PDO;
|
||||
|
||||
class ConnectionFactory {
|
||||
@ -13,6 +16,11 @@ class ConnectionFactory {
|
||||
|
||||
private $min_wait_timeout = 60;
|
||||
|
||||
private $types = [
|
||||
JsonType::NAME => JsonType::class,
|
||||
JsonOrSerializedType::NAME => JsonOrSerializedType::class,
|
||||
];
|
||||
|
||||
function createConnection() {
|
||||
$platform_class = self::PLATFORM_CLASS;
|
||||
$connection_params = [
|
||||
@ -36,6 +44,7 @@ class ConnectionFactory {
|
||||
$connection_params['port'] = Env::$db_port;
|
||||
}
|
||||
|
||||
$this->setupTypes();
|
||||
return DriverManager::getConnection($connection_params);
|
||||
}
|
||||
|
||||
@ -56,4 +65,14 @@ class ConnectionFactory {
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET ' . implode(', ', $driver_options),
|
||||
];
|
||||
}
|
||||
|
||||
private function setupTypes() {
|
||||
foreach ($this->types as $name => $class) {
|
||||
if (Type::hasType($name)) {
|
||||
Type::overrideType($name, $class);
|
||||
} else {
|
||||
Type::addType($name, $class);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
28
lib/Doctrine/Types/JsonOrSerializedType.php
Normal file
28
lib/Doctrine/Types/JsonOrSerializedType.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace MailPoet\Doctrine\Types;
|
||||
|
||||
use MailPoetVendor\Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
|
||||
class JsonOrSerializedType extends JsonType {
|
||||
const NAME = 'json_or_serialized';
|
||||
|
||||
function convertToPHPValue($value, AbstractPlatform $platform) {
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_resource($value)) {
|
||||
$value = stream_get_contents($value);
|
||||
}
|
||||
|
||||
if (is_serialized($value)) {
|
||||
return unserialize($value);
|
||||
}
|
||||
return parent::convertToPHPValue($value, $platform);
|
||||
}
|
||||
|
||||
function getName() {
|
||||
return self::NAME;
|
||||
}
|
||||
}
|
58
lib/Doctrine/Types/JsonType.php
Normal file
58
lib/Doctrine/Types/JsonType.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace MailPoet\Doctrine\Types;
|
||||
|
||||
use MailPoetVendor\Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use MailPoetVendor\Doctrine\DBAL\Types\Type;
|
||||
|
||||
class JsonType extends Type {
|
||||
const NAME = 'json';
|
||||
|
||||
function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) {
|
||||
return $platform->getJsonTypeDeclarationSQL($fieldDeclaration);
|
||||
}
|
||||
|
||||
function convertToDatabaseValue($value, AbstractPlatform $platform) {
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$flags = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
|
||||
if (defined('JSON_PRESERVE_ZERO_FRACTION')) {
|
||||
$flags |= JSON_PRESERVE_ZERO_FRACTION; // phpcs:ignore
|
||||
}
|
||||
|
||||
$encoded = json_encode($value, $flags);
|
||||
$this->handleErrors();
|
||||
return $encoded;
|
||||
}
|
||||
|
||||
function convertToPHPValue($value, AbstractPlatform $platform) {
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_resource($value)) {
|
||||
$value = stream_get_contents($value);
|
||||
}
|
||||
|
||||
$decoded = json_decode($value, true);
|
||||
$this->handleErrors();
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
function getName() {
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
function requiresSQLCommentHint(AbstractPlatform $platform) {
|
||||
return !$platform->hasNativeJsonType();
|
||||
}
|
||||
|
||||
private function handleErrors() {
|
||||
$error = json_last_error();
|
||||
if ($error !== JSON_ERROR_NONE) {
|
||||
throw new \RuntimeException(json_last_error_msg(), $error);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,8 +5,11 @@ namespace MailPoet\Test\Config;
|
||||
use MailPoet\Config\Env;
|
||||
use MailPoet\Doctrine\ConnectionFactory;
|
||||
use MailPoet\Doctrine\SerializableConnection;
|
||||
use MailPoet\Doctrine\Types\JsonOrSerializedType;
|
||||
use MailPoet\Doctrine\Types\JsonType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Driver\PDOMySql;
|
||||
use MailPoetVendor\Doctrine\DBAL\Platforms\MySqlPlatform;
|
||||
use MailPoetVendor\Doctrine\DBAL\Types\Type;
|
||||
use PDO;
|
||||
|
||||
class ConnectionFactoryTest extends \MailPoetTest {
|
||||
@ -36,6 +39,9 @@ class ConnectionFactoryTest extends \MailPoetTest {
|
||||
expect($connection->getPassword())->equals(Env::$db_password);
|
||||
expect($connection->getParams()['charset'])->equals(Env::$db_charset);
|
||||
expect($connection->getDatabase())->equals(Env::$db_name);
|
||||
|
||||
expect(Type::getType(JsonType::NAME))->isInstanceOf(JsonType::class);
|
||||
expect(Type::getType(JsonOrSerializedType::NAME))->isInstanceOf(JsonOrSerializedType::class);
|
||||
}
|
||||
|
||||
function testItIgnoresEmptyCharset() {
|
||||
|
64
tests/integration/Doctrine/Types/JsonEntity.php
Normal file
64
tests/integration/Doctrine/Types/JsonEntity.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace MailPoet\Test\Doctrine\Types;
|
||||
|
||||
/**
|
||||
* @Entity()
|
||||
* @Table(name="test_json_entity")
|
||||
*/
|
||||
class JsonEntity {
|
||||
/**
|
||||
* @Column(type="integer")
|
||||
* @Id
|
||||
* @GeneratedValue
|
||||
* @var int|null
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @Column(type="json")
|
||||
* @var array|null
|
||||
*/
|
||||
private $json_data;
|
||||
|
||||
/**
|
||||
* @Column(type="json_or_serialized")
|
||||
* @var array|null
|
||||
*/
|
||||
private $json_or_serialized_data;
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
function getId() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|null
|
||||
*/
|
||||
public function getJsonData() {
|
||||
return $this->json_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|null $json_data
|
||||
*/
|
||||
public function setJsonData($json_data) {
|
||||
$this->json_data = $json_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|null
|
||||
*/
|
||||
public function getJsonOrSerializedData() {
|
||||
return $this->json_or_serialized_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|null $json_or_serialized_data
|
||||
*/
|
||||
public function setJsonOrSerializedData($json_or_serialized_data) {
|
||||
$this->json_or_serialized_data = $json_or_serialized_data;
|
||||
}
|
||||
}
|
164
tests/integration/Doctrine/Types/JsonTypesTest.php
Normal file
164
tests/integration/Doctrine/Types/JsonTypesTest.php
Normal file
@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace MailPoet\Test\Doctrine\EventListeners;
|
||||
|
||||
use Exception;
|
||||
use MailPoet\Doctrine\ConfigurationFactory;
|
||||
use MailPoet\Doctrine\EntityManagerFactory;
|
||||
use MailPoet\Doctrine\EventListeners\TimestampListener;
|
||||
use MailPoet\Test\Doctrine\Types\JsonEntity;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
use MailPoetVendor\Doctrine\Common\Cache\ArrayCache;
|
||||
use RuntimeException;
|
||||
|
||||
require_once __DIR__ . '/JsonEntity.php';
|
||||
|
||||
class JsonTypesTest extends \MailPoetTest {
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
/** @var string */
|
||||
private $table_name;
|
||||
|
||||
/** @var array */
|
||||
private $test_data = [
|
||||
'key' => [
|
||||
'a' => 'string',
|
||||
'b' => 10,
|
||||
'c' => true,
|
||||
'd' => null,
|
||||
],
|
||||
];
|
||||
|
||||
function _before() {
|
||||
$this->wp = new WPFunctions();
|
||||
$this->entity_manager = $this->createEntityManager();
|
||||
$this->table_name = $this->entity_manager->getClassMetadata(JsonEntity::class)->getTableName();
|
||||
$this->connection->executeUpdate("DROP TABLE IF EXISTS $this->table_name");
|
||||
$this->connection->executeUpdate("
|
||||
CREATE TABLE $this->table_name (
|
||||
id int(11) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
json_data longtext NULL,
|
||||
json_or_serialized_data longtext NULL
|
||||
)
|
||||
");
|
||||
}
|
||||
|
||||
function testItSavesJsonData() {
|
||||
$entity = new JsonEntity();
|
||||
$entity->setJsonData($this->test_data);
|
||||
$entity->setJsonOrSerializedData($this->test_data);
|
||||
$this->entity_manager->persist($entity);
|
||||
$this->entity_manager->flush();
|
||||
|
||||
$saved_data = $this->connection->executeQuery("SELECT * FROM $this->table_name")->fetch();
|
||||
expect($saved_data['json_data'])->same(json_encode($this->test_data));
|
||||
expect($saved_data['json_or_serialized_data'])->same(json_encode($this->test_data));
|
||||
}
|
||||
|
||||
function testItLoadsJsonData() {
|
||||
$this->connection->executeUpdate(
|
||||
"INSERT INTO $this->table_name (id, json_data, json_or_serialized_data) VALUES (?, ?, ?)",
|
||||
[
|
||||
1,
|
||||
json_encode($this->test_data),
|
||||
json_encode($this->test_data),
|
||||
]
|
||||
);
|
||||
|
||||
$entity = $this->entity_manager->find(JsonEntity::class, 1);
|
||||
expect($entity->getJsonData())->same($this->test_data);
|
||||
expect($entity->getJsonOrSerializedData())->same($this->test_data);
|
||||
}
|
||||
|
||||
function testItLoadsSerializedData() {
|
||||
$this->connection->executeUpdate(
|
||||
"INSERT INTO $this->table_name (id, json_or_serialized_data) VALUES (?, ?)",
|
||||
[
|
||||
1,
|
||||
serialize($this->test_data),
|
||||
]
|
||||
);
|
||||
|
||||
$entity = $this->entity_manager->find(JsonEntity::class, 1);
|
||||
expect($entity->getJsonData())->null();
|
||||
expect($entity->getJsonOrSerializedData())->same($this->test_data);
|
||||
}
|
||||
|
||||
function testItSavesNullData() {
|
||||
$entity = new JsonEntity();
|
||||
$entity->setJsonData(null);
|
||||
$entity->setJsonOrSerializedData(null);
|
||||
$this->entity_manager->persist($entity);
|
||||
$this->entity_manager->flush();
|
||||
|
||||
$saved_data = $this->connection->executeQuery("SELECT * FROM $this->table_name")->fetch();
|
||||
expect($saved_data['json_data'])->null();
|
||||
expect($saved_data['json_or_serialized_data'])->null();
|
||||
}
|
||||
|
||||
function testItLoadsNullData() {
|
||||
$this->connection->executeUpdate(
|
||||
"INSERT INTO $this->table_name (id, json_data, json_or_serialized_data) VALUES (?, ?, ?)",
|
||||
[
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
]
|
||||
);
|
||||
|
||||
$entity = $this->entity_manager->find(JsonEntity::class, 1);
|
||||
expect($entity->getJsonData())->null();
|
||||
expect($entity->getJsonOrSerializedData())->null();
|
||||
}
|
||||
|
||||
function testItDoesNotSaveInvalidData() {
|
||||
$entity = new JsonEntity();
|
||||
$entity->setJsonData("\xB1\x31"); // invalid unicode sequence
|
||||
$this->entity_manager->persist($entity);
|
||||
|
||||
$exception = null;
|
||||
try {
|
||||
$this->entity_manager->flush();
|
||||
} catch (Exception $e) {
|
||||
$exception = $e;
|
||||
}
|
||||
expect($exception)->isInstanceOf(RuntimeException::class);
|
||||
}
|
||||
|
||||
function testItDoesNotLoadInvalidData() {
|
||||
$this->connection->executeUpdate(
|
||||
"INSERT INTO $this->table_name (id, json_data) VALUES (?, ?)",
|
||||
[
|
||||
1,
|
||||
'{', // invalid JSON
|
||||
]
|
||||
);
|
||||
|
||||
$exception = null;
|
||||
try {
|
||||
$this->entity_manager->find(JsonEntity::class, 1);
|
||||
} catch (Exception $e) {
|
||||
$exception = $e;
|
||||
}
|
||||
expect($exception)->isInstanceOf(RuntimeException::class);
|
||||
}
|
||||
|
||||
function _after() {
|
||||
parent::_after();
|
||||
$this->connection->executeUpdate("DROP TABLE IF EXISTS $this->table_name");
|
||||
}
|
||||
|
||||
private function createEntityManager() {
|
||||
$configuration_factory = new ConfigurationFactory();
|
||||
$configuration = $configuration_factory->createConfiguration();
|
||||
|
||||
$metadata_driver = $configuration->newDefaultAnnotationDriver([__DIR__]);
|
||||
$configuration->setMetadataDriverImpl($metadata_driver);
|
||||
$configuration->setMetadataCacheImpl(new ArrayCache());
|
||||
|
||||
$timestamp_listener = new TimestampListener($this->wp);
|
||||
$entity_manager_factory = new EntityManagerFactory($this->connection, $configuration, $timestamp_listener);
|
||||
return $entity_manager_factory->createEntityManager();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user