Add JSON and JSON/serialized types for Doctrine

[MAILPOET-2216]
This commit is contained in:
Jan Jakeš
2019-08-13 10:07:46 +02:00
committed by M. Shull
parent 09105dd730
commit 03fb82cf95
6 changed files with 339 additions and 0 deletions

View File

@ -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);
}
}
}
}

View 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;
}
}

View 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);
}
}
}

View File

@ -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() {

View 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;
}
}

View 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();
}
}