Add JSON and JSON/serialized types for Doctrine
[MAILPOET-2216]
This commit is contained in:
@ -3,8 +3,11 @@
|
|||||||
namespace MailPoet\Doctrine;
|
namespace MailPoet\Doctrine;
|
||||||
|
|
||||||
use MailPoet\Config\Env;
|
use MailPoet\Config\Env;
|
||||||
|
use MailPoet\Doctrine\Types\JsonOrSerializedType;
|
||||||
|
use MailPoet\Doctrine\Types\JsonType;
|
||||||
use MailPoetVendor\Doctrine\DBAL\DriverManager;
|
use MailPoetVendor\Doctrine\DBAL\DriverManager;
|
||||||
use MailPoetVendor\Doctrine\DBAL\Platforms\MySqlPlatform;
|
use MailPoetVendor\Doctrine\DBAL\Platforms\MySqlPlatform;
|
||||||
|
use MailPoetVendor\Doctrine\DBAL\Types\Type;
|
||||||
use PDO;
|
use PDO;
|
||||||
|
|
||||||
class ConnectionFactory {
|
class ConnectionFactory {
|
||||||
@ -13,6 +16,11 @@ class ConnectionFactory {
|
|||||||
|
|
||||||
private $min_wait_timeout = 60;
|
private $min_wait_timeout = 60;
|
||||||
|
|
||||||
|
private $types = [
|
||||||
|
JsonType::NAME => JsonType::class,
|
||||||
|
JsonOrSerializedType::NAME => JsonOrSerializedType::class,
|
||||||
|
];
|
||||||
|
|
||||||
function createConnection() {
|
function createConnection() {
|
||||||
$platform_class = self::PLATFORM_CLASS;
|
$platform_class = self::PLATFORM_CLASS;
|
||||||
$connection_params = [
|
$connection_params = [
|
||||||
@ -36,6 +44,7 @@ class ConnectionFactory {
|
|||||||
$connection_params['port'] = Env::$db_port;
|
$connection_params['port'] = Env::$db_port;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->setupTypes();
|
||||||
return DriverManager::getConnection($connection_params);
|
return DriverManager::getConnection($connection_params);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,4 +65,14 @@ class ConnectionFactory {
|
|||||||
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET ' . implode(', ', $driver_options),
|
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\Config\Env;
|
||||||
use MailPoet\Doctrine\ConnectionFactory;
|
use MailPoet\Doctrine\ConnectionFactory;
|
||||||
use MailPoet\Doctrine\SerializableConnection;
|
use MailPoet\Doctrine\SerializableConnection;
|
||||||
|
use MailPoet\Doctrine\Types\JsonOrSerializedType;
|
||||||
|
use MailPoet\Doctrine\Types\JsonType;
|
||||||
use MailPoetVendor\Doctrine\DBAL\Driver\PDOMySql;
|
use MailPoetVendor\Doctrine\DBAL\Driver\PDOMySql;
|
||||||
use MailPoetVendor\Doctrine\DBAL\Platforms\MySqlPlatform;
|
use MailPoetVendor\Doctrine\DBAL\Platforms\MySqlPlatform;
|
||||||
|
use MailPoetVendor\Doctrine\DBAL\Types\Type;
|
||||||
use PDO;
|
use PDO;
|
||||||
|
|
||||||
class ConnectionFactoryTest extends \MailPoetTest {
|
class ConnectionFactoryTest extends \MailPoetTest {
|
||||||
@ -36,6 +39,9 @@ class ConnectionFactoryTest extends \MailPoetTest {
|
|||||||
expect($connection->getPassword())->equals(Env::$db_password);
|
expect($connection->getPassword())->equals(Env::$db_password);
|
||||||
expect($connection->getParams()['charset'])->equals(Env::$db_charset);
|
expect($connection->getParams()['charset'])->equals(Env::$db_charset);
|
||||||
expect($connection->getDatabase())->equals(Env::$db_name);
|
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() {
|
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