init
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine\Annotations;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Doctrine\PSRArrayCache;
|
||||
use MailPoetVendor\Doctrine\Common\Annotations\AnnotationReader;
|
||||
use MailPoetVendor\Doctrine\Common\Annotations\AnnotationRegistry;
|
||||
use MailPoetVendor\Doctrine\Common\Annotations\PsrCachedReader;
|
||||
|
||||
class AnnotationReaderProvider {
|
||||
/** @var PsrCachedReader */
|
||||
private $annotationReader;
|
||||
|
||||
public function __construct() {
|
||||
// register annotation reader if doctrine/annotations package is installed
|
||||
// (i.e. in dev environment, on production metadata is dumped in the build)
|
||||
$readAnnotations = class_exists(PsrCachedReader::class) && class_exists(AnnotationReader::class);
|
||||
if ($readAnnotations) {
|
||||
// autoload all annotation classes using registered loaders (Composer)
|
||||
// (needed for Symfony\Validator constraint annotations to be loaded)
|
||||
AnnotationRegistry::registerLoader('class_exists');
|
||||
$this->annotationReader = new PsrCachedReader(new AnnotationReader(), new PSRArrayCache());
|
||||
}
|
||||
}
|
||||
|
||||
public function getAnnotationReader(): ?PsrCachedReader {
|
||||
return $this->annotationReader;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Doctrine\Common\Cache\CacheProvider;
|
||||
|
||||
/**
|
||||
* Array cache
|
||||
* Based on https://github.com/doctrine/cache/blob/1.11.x/lib/Doctrine/Common/Cache/ArrayCache.php
|
||||
* The cache implementation was removed from the doctrine/cache v2.0 so we need to provide own implementation.
|
||||
*/
|
||||
class ArrayCache extends CacheProvider {
|
||||
|
||||
/** @var mixed[] */
|
||||
private $data = [];
|
||||
|
||||
/** @var int */
|
||||
private $hitsCount = 0;
|
||||
|
||||
/** @var int */
|
||||
private $missesCount = 0;
|
||||
|
||||
/** @var int */
|
||||
private $upTime;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->upTime = time();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doFetch($id) {
|
||||
if (!$this->doContains($id)) {
|
||||
$this->missesCount += 1;
|
||||
return false;
|
||||
}
|
||||
$this->hitsCount += 1;
|
||||
return $this->data[$id][0];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doContains($id) {
|
||||
if (!isset($this->data[$id])) {
|
||||
return false;
|
||||
}
|
||||
$expiration = $this->data[$id][1];
|
||||
if ($expiration && $expiration < \time()) {
|
||||
$this->doDelete($id);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doSave($id, $data, $lifeTime = 0) {
|
||||
$this->data[$id] = [$data, $lifeTime ? \time() + $lifeTime : false];
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doDelete($id) {
|
||||
unset($this->data[$id]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doFlush() {
|
||||
$this->data = [];
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doGetStats() {
|
||||
return [
|
||||
CacheProvider::STATS_HITS => $this->hitsCount,
|
||||
CacheProvider::STATS_MISSES => $this->missesCount,
|
||||
CacheProvider::STATS_UPTIME => $this->upTime,
|
||||
CacheProvider::STATS_MEMORY_USAGE => null,
|
||||
CacheProvider::STATS_MEMORY_AVAILABLE => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Doctrine;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\RuntimeException;
|
||||
use MailPoetVendor\Doctrine\Persistence\Mapping\ClassMetadata;
|
||||
use MailPoetVendor\Doctrine\Persistence\Mapping\Driver\MappingDriver;
|
||||
use MailPoetVendor\Psr\Cache\CacheItemPoolInterface;
|
||||
|
||||
/**
|
||||
* Intended to be used in production environment where we rely on metadata cache for reading all metadata.
|
||||
*/
|
||||
class CacheOnlyMappingDriver implements MappingDriver {
|
||||
/** @var string */
|
||||
protected $cacheSalt = '__CLASSMETADATA__';
|
||||
|
||||
/** @var CacheItemPoolInterface */
|
||||
private $metaDataCache;
|
||||
|
||||
public function __construct(
|
||||
CacheItemPoolInterface $metaDataCache
|
||||
) {
|
||||
$this->metaDataCache = $metaDataCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inerhitDoc
|
||||
*/
|
||||
public function loadMetadataForClass($className, ClassMetadata $metadata) {
|
||||
// We don't need to load anything it is all cached.
|
||||
}
|
||||
|
||||
/**
|
||||
* @inerhitDoc
|
||||
*/
|
||||
public function getAllClassNames() {
|
||||
throw new RuntimeException('CacheOnlyMappingDriver::getAllClassNames should not be called');
|
||||
}
|
||||
|
||||
/**
|
||||
* @inerhitDoc
|
||||
*/
|
||||
public function isTransient($className) {
|
||||
// Everything in cache are metadata and class with metadata is non-transient
|
||||
// See https://github.com/doctrine/persistence/blob/b07e347a24e7a19a2b6462e00a6dff899e4c2dd2/src/Persistence/Mapping/Driver/MappingDriver.php#L34
|
||||
return !$this->metaDataCache->hasItem($this->getCacheKey($className));
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy pasted from MailPoetVendor\Doctrine\Persistence\Mapping\AbstractClassMetadataFactory
|
||||
*/
|
||||
protected function getCacheKey(string $className): string {
|
||||
return str_replace('\\', '__', $className) . $this->cacheSalt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Doctrine\Annotations\AnnotationReaderProvider;
|
||||
use MailPoetVendor\Doctrine\Common\Proxy\AbstractProxyFactory;
|
||||
use MailPoetVendor\Doctrine\ORM\Configuration;
|
||||
use MailPoetVendor\Doctrine\ORM\Mapping\Driver\AnnotationDriver;
|
||||
use MailPoetVendor\Doctrine\ORM\Mapping\UnderscoreNamingStrategy;
|
||||
|
||||
class ConfigurationFactory {
|
||||
const ENTITY_DIR = __DIR__ . '/../Entities';
|
||||
const METADATA_DIR = __DIR__ . '/../../generated/doctrine-metadata';
|
||||
const PROXY_DIR = __DIR__ . '/../../generated/doctrine-proxies';
|
||||
const PROXY_NAMESPACE = 'MailPoetDoctrineProxies';
|
||||
|
||||
/** @var bool */
|
||||
private $isDevMode;
|
||||
|
||||
/** @var AnnotationReaderProvider */
|
||||
private $annotationReaderProvider;
|
||||
|
||||
public function __construct(
|
||||
AnnotationReaderProvider $annotationReaderProvider,
|
||||
$isDevMode = null
|
||||
) {
|
||||
$this->isDevMode = $isDevMode === null ? WP_DEBUG : $isDevMode;
|
||||
$this->annotationReaderProvider = $annotationReaderProvider;
|
||||
}
|
||||
|
||||
public function createConfiguration() {
|
||||
$configuration = new Configuration();
|
||||
$configuration->setNamingStrategy(new UnderscoreNamingStrategy(\CASE_LOWER, true));
|
||||
|
||||
$this->configureMetadata($configuration);
|
||||
$this->configureProxies($configuration);
|
||||
$this->configureCache($configuration);
|
||||
return $configuration;
|
||||
}
|
||||
|
||||
private function configureMetadata(Configuration $configuration) {
|
||||
$configuration->setClassMetadataFactoryName(TablePrefixMetadataFactory::class);
|
||||
|
||||
// annotation reader exists only in dev environment, on production cache is pre-generated
|
||||
$annotationReader = $this->annotationReaderProvider->getAnnotationReader();
|
||||
$isReadOnly = !$annotationReader;
|
||||
$metadataStorage = new PSRMetadataCache(self::METADATA_DIR, $isReadOnly);
|
||||
$configuration->setMetadataCache($metadataStorage);
|
||||
|
||||
if ($isReadOnly) {
|
||||
$configuration->setMetadataDriverImpl(new CacheOnlyMappingDriver($metadataStorage));
|
||||
} else {
|
||||
$configuration->setMetadataDriverImpl(new AnnotationDriver($annotationReader, [self::ENTITY_DIR]));
|
||||
}
|
||||
}
|
||||
|
||||
private function configureProxies(Configuration $configuration) {
|
||||
$configuration->setProxyDir(self::PROXY_DIR);
|
||||
$configuration->setProxyNamespace(self::PROXY_NAMESPACE);
|
||||
$configuration->setAutoGenerateProxyClasses(
|
||||
$this->isDevMode
|
||||
? AbstractProxyFactory::AUTOGENERATE_FILE_NOT_EXISTS
|
||||
: AbstractProxyFactory::AUTOGENERATE_NEVER
|
||||
);
|
||||
}
|
||||
|
||||
private function configureCache(Configuration $configuration) {
|
||||
$cache = new ArrayCache();
|
||||
$configuration->setQueryCacheImpl($cache);
|
||||
$configuration->setResultCacheImpl($cache);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Doctrine\Middlewares\PostConnectMiddleware;
|
||||
use MailPoet\Doctrine\Types\BigIntType;
|
||||
use MailPoet\Doctrine\Types\DateTimeTzToStringType;
|
||||
use MailPoet\Doctrine\Types\JsonOrSerializedType;
|
||||
use MailPoet\Doctrine\Types\JsonType;
|
||||
use MailPoet\Doctrine\Types\SerializedArrayType;
|
||||
use MailPoet\Doctrine\WPDB\Driver as WPDBDriver;
|
||||
use MailPoetVendor\Doctrine\DBAL\Configuration;
|
||||
use MailPoetVendor\Doctrine\DBAL\Driver;
|
||||
use MailPoetVendor\Doctrine\DBAL\Driver\Middleware;
|
||||
use MailPoetVendor\Doctrine\DBAL\DriverManager;
|
||||
use MailPoetVendor\Doctrine\DBAL\Types\Type;
|
||||
|
||||
class ConnectionFactory {
|
||||
const DRIVER_CLASS = WPDBDriver::class;
|
||||
|
||||
private $types = [
|
||||
BigIntType::NAME => BigIntType::class,
|
||||
DateTimeTzToStringType::NAME => DateTimeTzToStringType::class,
|
||||
JsonType::NAME => JsonType::class,
|
||||
JsonOrSerializedType::NAME => JsonOrSerializedType::class,
|
||||
SerializedArrayType::NAME => SerializedArrayType::class,
|
||||
];
|
||||
|
||||
public function createConnection() {
|
||||
$this->setupTypes();
|
||||
return DriverManager::getConnection(
|
||||
[
|
||||
'driverClass' => self::DRIVER_CLASS,
|
||||
],
|
||||
$this->getConfiguration()
|
||||
);
|
||||
}
|
||||
|
||||
private function setupTypes() {
|
||||
foreach ($this->types as $name => $class) {
|
||||
if (Type::hasType($name)) {
|
||||
Type::overrideType($name, $class);
|
||||
} else {
|
||||
Type::addType($name, $class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getConfiguration(): Configuration {
|
||||
$config = new Configuration();
|
||||
$driverMiddleware = new class implements Middleware {
|
||||
public function wrap(Driver $driver): Driver {
|
||||
return new PostConnectMiddleware($driver);
|
||||
}
|
||||
};
|
||||
$config->setMiddlewares([$driverMiddleware]);
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Doctrine\EventListeners\EmojiEncodingListener;
|
||||
use MailPoet\Doctrine\EventListeners\LastSubscribedAtListener;
|
||||
use MailPoet\Doctrine\EventListeners\NewsletterListener;
|
||||
use MailPoet\Doctrine\EventListeners\SubscriberListener;
|
||||
use MailPoet\Doctrine\EventListeners\TimestampListener;
|
||||
use MailPoet\Doctrine\EventListeners\ValidationListener;
|
||||
use MailPoet\Tracy\DoctrinePanel\DoctrinePanel;
|
||||
use MailPoetVendor\Doctrine\DBAL\Connection;
|
||||
use MailPoetVendor\Doctrine\ORM\Configuration;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
use MailPoetVendor\Doctrine\ORM\Events;
|
||||
use Tracy\Debugger;
|
||||
|
||||
class EntityManagerFactory {
|
||||
|
||||
/** @var Connection */
|
||||
private $connection;
|
||||
|
||||
/** @var Configuration */
|
||||
private $configuration;
|
||||
|
||||
/** @var TimestampListener */
|
||||
private $timestampListener;
|
||||
|
||||
/** @var ValidationListener */
|
||||
private $validationListener;
|
||||
|
||||
/** @var EmojiEncodingListener */
|
||||
private $emojiEncodingListener;
|
||||
|
||||
/** @var LastSubscribedAtListener */
|
||||
private $lastSubscribedAtListener;
|
||||
|
||||
/** @var SubscriberListener */
|
||||
private $subscriberListener;
|
||||
|
||||
private NewsletterListener $newsletterListener;
|
||||
|
||||
public function __construct(
|
||||
Connection $connection,
|
||||
Configuration $configuration,
|
||||
TimestampListener $timestampListener,
|
||||
ValidationListener $validationListener,
|
||||
EmojiEncodingListener $emojiEncodingListener,
|
||||
LastSubscribedAtListener $lastSubscribedAtListener,
|
||||
NewsletterListener $newsletterListener,
|
||||
SubscriberListener $subscriberListener
|
||||
) {
|
||||
$this->connection = $connection;
|
||||
$this->configuration = $configuration;
|
||||
$this->timestampListener = $timestampListener;
|
||||
$this->validationListener = $validationListener;
|
||||
$this->emojiEncodingListener = $emojiEncodingListener;
|
||||
$this->lastSubscribedAtListener = $lastSubscribedAtListener;
|
||||
$this->subscriberListener = $subscriberListener;
|
||||
$this->newsletterListener = $newsletterListener;
|
||||
}
|
||||
|
||||
public function createEntityManager(): EntityManager {
|
||||
$entityManager = EntityManager::create($this->connection, $this->configuration);
|
||||
$this->cleanupListeners($entityManager);
|
||||
$this->setupListeners($entityManager);
|
||||
if (
|
||||
class_exists(Debugger::class)
|
||||
&& class_exists(DoctrinePanel::class)
|
||||
) {
|
||||
DoctrinePanel::init($entityManager);
|
||||
}
|
||||
return $entityManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* We sometimes work with more EntityManager in tests, and the behavior could be inconsistent with multiple listeners
|
||||
*/
|
||||
private function cleanupListeners(EntityManager $entityManager) {
|
||||
$eventManager = $entityManager->getEventManager();
|
||||
foreach ($eventManager->getListeners() as $event => $listeners) {
|
||||
if (!is_array($listeners)) {
|
||||
$eventManager->removeEventListener($event, $listeners);
|
||||
continue;
|
||||
}
|
||||
foreach ($listeners as $listener) {
|
||||
$eventManager->removeEventListener($event, $listener);
|
||||
}
|
||||
}
|
||||
|
||||
$entityManager->getConfiguration()->getEntityListenerResolver()->clear(SubscriberListener::class);
|
||||
}
|
||||
|
||||
private function setupListeners(EntityManager $entityManager) {
|
||||
$entityManager->getEventManager()->addEventListener(
|
||||
[Events::prePersist, Events::preUpdate],
|
||||
$this->timestampListener
|
||||
);
|
||||
|
||||
$entityManager->getEventManager()->addEventListener(
|
||||
[Events::onFlush],
|
||||
$this->validationListener
|
||||
);
|
||||
|
||||
$entityManager->getEventManager()->addEventListener(
|
||||
[Events::prePersist, Events::preUpdate],
|
||||
$this->emojiEncodingListener
|
||||
);
|
||||
|
||||
$entityManager->getEventManager()->addEventListener(
|
||||
[Events::prePersist, Events::preUpdate],
|
||||
$this->lastSubscribedAtListener
|
||||
);
|
||||
|
||||
$entityManager->getEventManager()->addEventListener(
|
||||
[Events::preUpdate],
|
||||
$this->newsletterListener
|
||||
);
|
||||
|
||||
$entityManager->getConfiguration()->getEntityListenerResolver()->register($this->subscriberListener);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine\EntityTraits;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
trait AutoincrementedIdTrait {
|
||||
/**
|
||||
* @ORM\Column(type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @var int|null
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/** @return int|null */
|
||||
public function getId() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/** @param int|null $id */
|
||||
public function setId($id) {
|
||||
$this->id = $id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine\EntityTraits;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use DateTimeInterface;
|
||||
use MailPoetVendor\Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
trait CreatedAtTrait {
|
||||
/**
|
||||
* @ORM\Column(type="datetimetz", nullable=true)
|
||||
* @var DateTimeInterface|null
|
||||
*/
|
||||
private $createdAt;
|
||||
|
||||
public function getCreatedAt(): ?DateTimeInterface {
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function setCreatedAt(DateTimeInterface $createdAt): void {
|
||||
$this->createdAt = $createdAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine\EntityTraits;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use DateTimeInterface;
|
||||
use MailPoetVendor\Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
trait DeletedAtTrait {
|
||||
/**
|
||||
* @ORM\Column(type="datetimetz", nullable=true)
|
||||
* @var DateTimeInterface|null
|
||||
*/
|
||||
private $deletedAt;
|
||||
|
||||
/** @return DateTimeInterface|null */
|
||||
public function getDeletedAt() {
|
||||
return $this->deletedAt;
|
||||
}
|
||||
|
||||
/** @param DateTimeInterface|null $deletedAt */
|
||||
public function setDeletedAt($deletedAt) {
|
||||
$this->deletedAt = $deletedAt;
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine\EntityTraits;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Doctrine\ORM\EntityNotFoundException;
|
||||
use MailPoetVendor\Doctrine\ORM\Proxy\Proxy;
|
||||
|
||||
trait SafeToOneAssociationLoadTrait {
|
||||
private function safelyLoadToOneAssociation(string $propertyName, $emptyValue = null) {
|
||||
if (!property_exists($this, $propertyName)) {
|
||||
throw new \InvalidArgumentException("Property '$propertyName' does not exist on class '" . get_class($this) . "'");
|
||||
}
|
||||
|
||||
if (!$this->$propertyName instanceof Proxy) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->$propertyName->__isInitialized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if a proxy exists (= we have related entity ID), try to load it
|
||||
// to see if it is a valid ID referencing an existing entity
|
||||
try {
|
||||
$this->$propertyName->__load();
|
||||
} catch (EntityNotFoundException $e) {
|
||||
$this->$propertyName = $emptyValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine\EntityTraits;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use DateTimeInterface;
|
||||
use MailPoetVendor\Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
trait UpdatedAtTrait {
|
||||
/**
|
||||
* @ORM\Column(type="datetimetz")
|
||||
* @var DateTimeInterface
|
||||
*/
|
||||
private $updatedAt;
|
||||
|
||||
/** @return DateTimeInterface */
|
||||
public function getUpdatedAt() {
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
public function setUpdatedAt(DateTimeInterface $updatedAt) {
|
||||
$this->updatedAt = $updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Doctrine\EntityTraits;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
trait ValidationGroupsTrait {
|
||||
/**
|
||||
* @var array|null
|
||||
*/
|
||||
private $validationGroups;
|
||||
|
||||
public function getValidationGroups(): ?array {
|
||||
return $this->validationGroups;
|
||||
}
|
||||
|
||||
public function setValidationGroups(?array $validationGroups): void {
|
||||
$this->validationGroups = $validationGroups;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine\EventListeners;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\FormEntity;
|
||||
use MailPoet\WP\Emoji;
|
||||
use MailPoetVendor\Doctrine\ORM\Event\LifecycleEventArgs;
|
||||
|
||||
class EmojiEncodingListener {
|
||||
/** @var Emoji */
|
||||
private $emoji;
|
||||
|
||||
public function __construct(
|
||||
Emoji $emoji
|
||||
) {
|
||||
$this->emoji = $emoji;
|
||||
}
|
||||
|
||||
public function prePersist(LifecycleEventArgs $eventArgs) {
|
||||
$this->sanitizeEmojiBeforeSaving($eventArgs);
|
||||
}
|
||||
|
||||
public function preUpdate(LifecycleEventArgs $eventArgs) {
|
||||
$this->sanitizeEmojiBeforeSaving($eventArgs);
|
||||
}
|
||||
|
||||
private function sanitizeEmojiBeforeSaving(LifecycleEventArgs $eventArgs) {
|
||||
$entity = $eventArgs->getEntity();
|
||||
if ($entity instanceof FormEntity) {
|
||||
$body = $entity->getBody();
|
||||
if ($body !== null) {
|
||||
$entity->setBody($this->emoji->sanitizeEmojisInFormBody($body));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine\EventListeners;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\ORM\Event\LifecycleEventArgs;
|
||||
|
||||
class LastSubscribedAtListener {
|
||||
public function prePersist(LifecycleEventArgs $eventArgs): void {
|
||||
$entity = $eventArgs->getEntity();
|
||||
|
||||
if ($entity instanceof SubscriberEntity && $entity->getStatus() === SubscriberEntity::STATUS_SUBSCRIBED) {
|
||||
$entity->setLastSubscribedAt(Carbon::now()->millisecond(0));
|
||||
}
|
||||
}
|
||||
|
||||
public function preUpdate(LifecycleEventArgs $eventArgs): void {
|
||||
$entity = $eventArgs->getEntity();
|
||||
if (!$entity instanceof SubscriberEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
$unitOfWork = $eventArgs->getEntityManager()->getUnitOfWork();
|
||||
$changeSet = $unitOfWork->getEntityChangeSet($entity);
|
||||
if (!isset($changeSet['status'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
[$oldStatus, $newStatus] = $changeSet['status'];
|
||||
// Update last_subscribed_at when status changes to subscribed
|
||||
if ($oldStatus !== SubscriberEntity::STATUS_SUBSCRIBED && $newStatus === SubscriberEntity::STATUS_SUBSCRIBED) {
|
||||
$entity->setLastSubscribedAt(Carbon::now()->millisecond(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Doctrine\EventListeners;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\NewsletterEntity;
|
||||
use MailPoet\Entities\WpPostEntity;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
use MailPoetVendor\Doctrine\ORM\Event\LifecycleEventArgs;
|
||||
|
||||
class NewsletterListener {
|
||||
private WPFunctions $wp;
|
||||
|
||||
public function __construct(
|
||||
WPFunctions $wp
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
public function preUpdate(LifecycleEventArgs $eventArgs): void {
|
||||
$entity = $eventArgs->getEntity();
|
||||
if (!$entity instanceof NewsletterEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
$unitOfWork = $eventArgs->getEntityManager()->getUnitOfWork();
|
||||
|
||||
/** @var array{status: array{0: string, 1: string}} $changeSet */
|
||||
$changeSet = $unitOfWork->getEntityChangeSet($entity);
|
||||
if (!isset($changeSet['status'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
[$oldStatus, $newStatus] = $changeSet['status'];
|
||||
|
||||
if ($oldStatus !== NewsletterEntity::STATUS_SENT && $newStatus === NewsletterEntity::STATUS_SENT) {
|
||||
$post = $entity->getWpPost();
|
||||
if ($post instanceof WpPostEntity) {
|
||||
$this->wp->wpUpdatePost([
|
||||
'ID' => $post->getId(),
|
||||
'post_status' => NewsletterEntity::STATUS_SENT,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Doctrine\EventListeners;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Config\SubscriberChangesNotifier;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoetVendor\Doctrine\ORM\Event\LifecycleEventArgs;
|
||||
|
||||
class SubscriberListener {
|
||||
|
||||
/** @var SubscriberChangesNotifier */
|
||||
private $subscriberChangesNotifier;
|
||||
|
||||
public function __construct(
|
||||
SubscriberChangesNotifier $subscriberChangesNotifier
|
||||
) {
|
||||
$this->subscriberChangesNotifier = $subscriberChangesNotifier;
|
||||
}
|
||||
|
||||
private function maybeNotifyStatusChanged(SubscriberEntity $subscriber, LifecycleEventArgs $event): void {
|
||||
$entityManager = $event->getEntityManager();
|
||||
$unitOfWork = $entityManager->getUnitOfWork();
|
||||
$changeset = $unitOfWork->getEntityChangeSet($subscriber);
|
||||
|
||||
if (array_key_exists('status', $changeset) && $changeset['status'][0] !== $changeset['status'][1]) {
|
||||
$this->subscriberChangesNotifier->subscriberStatusChanged((int)$subscriber->getId());
|
||||
}
|
||||
}
|
||||
|
||||
public function postPersist(SubscriberEntity $subscriber, LifecycleEventArgs $event): void {
|
||||
$this->subscriberChangesNotifier->subscriberCreated((int)$subscriber->getId());
|
||||
}
|
||||
|
||||
public function postUpdate(SubscriberEntity $subscriber, LifecycleEventArgs $event): void {
|
||||
$this->subscriberChangesNotifier->subscriberUpdated((int)$subscriber->getId());
|
||||
$this->maybeNotifyStatusChanged($subscriber, $event);
|
||||
}
|
||||
|
||||
public function postRemove(SubscriberEntity $subscriber, LifecycleEventArgs $event): void {
|
||||
$this->subscriberChangesNotifier->subscriberDeleted((int)$subscriber->getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine\EventListeners;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Doctrine\EntityTraits\CreatedAtTrait;
|
||||
use MailPoet\Doctrine\EntityTraits\UpdatedAtTrait;
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\ORM\Event\LifecycleEventArgs;
|
||||
use ReflectionObject;
|
||||
|
||||
class TimestampListener {
|
||||
public function prePersist(LifecycleEventArgs $eventArgs) {
|
||||
$entity = $eventArgs->getEntity();
|
||||
$entityTraits = $this->getEntityTraits($entity);
|
||||
$now = $this->getNow();
|
||||
|
||||
if (
|
||||
in_array(CreatedAtTrait::class, $entityTraits, true)
|
||||
&& method_exists($entity, 'setCreatedAt')
|
||||
&& method_exists($entity, 'getCreatedAt')
|
||||
&& !$entity->getCreatedAt()
|
||||
) {
|
||||
$entity->setCreatedAt(clone $now);
|
||||
}
|
||||
|
||||
if (in_array(UpdatedAtTrait::class, $entityTraits, true) && method_exists($entity, 'setUpdatedAt')) {
|
||||
$entity->setUpdatedAt(clone $now);
|
||||
}
|
||||
}
|
||||
|
||||
public function preUpdate(LifecycleEventArgs $eventArgs) {
|
||||
$entity = $eventArgs->getEntity();
|
||||
$entityTraits = $this->getEntityTraits($entity);
|
||||
|
||||
if (in_array(UpdatedAtTrait::class, $entityTraits, true) && method_exists($entity, 'setUpdatedAt')) {
|
||||
$entity->setUpdatedAt($this->getNow());
|
||||
}
|
||||
}
|
||||
|
||||
private function getEntityTraits($entity) {
|
||||
$entityReflection = new ReflectionObject($entity);
|
||||
return $entityReflection->getTraitNames();
|
||||
}
|
||||
|
||||
public function getNow(): \DateTimeInterface {
|
||||
return Carbon::now()->millisecond(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine\EventListeners;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Doctrine\Validator\ValidationException;
|
||||
use MailPoetVendor\Doctrine\ORM\Event\OnFlushEventArgs;
|
||||
use MailPoetVendor\Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
class ValidationListener {
|
||||
/** @var ValidatorInterface */
|
||||
private $validator;
|
||||
|
||||
public function __construct(
|
||||
ValidatorInterface $validator
|
||||
) {
|
||||
$this->validator = $validator;
|
||||
}
|
||||
|
||||
public function onFlush(OnFlushEventArgs $eventArgs) {
|
||||
$unitOfWork = $eventArgs->getEntityManager()->getUnitOfWork();
|
||||
|
||||
foreach ($unitOfWork->getScheduledEntityInsertions() as $entity) {
|
||||
$this->validate($entity);
|
||||
}
|
||||
|
||||
foreach ($unitOfWork->getScheduledEntityUpdates() as $entity) {
|
||||
$this->validate($entity);
|
||||
}
|
||||
}
|
||||
|
||||
private function validate($entity) {
|
||||
$groups = $this->getValidationGroups($entity);
|
||||
$violations = $this->validator->validate($entity, null, $groups);
|
||||
if ($violations->count() > 0) {
|
||||
throw new ValidationException(get_class($entity), $violations);
|
||||
}
|
||||
}
|
||||
|
||||
private function getValidationGroups($entity) {
|
||||
if (is_object($entity) && method_exists($entity, 'getValidationGroups')) {
|
||||
return $entity->getValidationGroups();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Doctrine\Common\Cache\CacheProvider;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
|
||||
// Simple filesystem-based cache storage for Doctrine Metadata.
|
||||
//
|
||||
// Needed because Doctrine's FilesystemCache doesn't work read-only (when metadata dumped)
|
||||
// and it calls realpath() that could fail on some hostings due to filesystem permissions.
|
||||
class MetadataCache extends CacheProvider {
|
||||
/** @var bool */
|
||||
private $isDevMode;
|
||||
|
||||
/** @var string */
|
||||
private $directory;
|
||||
|
||||
public function __construct(
|
||||
$dir,
|
||||
$isReadOnly
|
||||
) {
|
||||
$this->isDevMode = defined('WP_DEBUG') && WP_DEBUG && !$isReadOnly;
|
||||
$this->directory = rtrim($dir, '/\\');
|
||||
if (!file_exists($this->directory)) {
|
||||
mkdir($this->directory);
|
||||
}
|
||||
}
|
||||
|
||||
protected function doFetch($id) {
|
||||
if (!$this->doContains($id)) {
|
||||
return false;
|
||||
}
|
||||
return unserialize((string)file_get_contents($this->getFilename($id)));
|
||||
}
|
||||
|
||||
protected function doContains($id) {
|
||||
$filename = $this->getFilename($id);
|
||||
$fileExists = file_exists($filename);
|
||||
|
||||
// in dev mode invalidate cache if source file has changed
|
||||
if ($fileExists && $this->isDevMode) {
|
||||
/** @var \stdClass $classMetadata */
|
||||
$classMetadata = unserialize((string)file_get_contents($filename));
|
||||
if (!isset($classMetadata->name) || (!class_exists($classMetadata->name) && !interface_exists($classMetadata->name))) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
$reflection = new ReflectionClass($classMetadata->name);
|
||||
} catch (ReflectionException $e) {
|
||||
return false;
|
||||
}
|
||||
clearstatcache();
|
||||
return filemtime((string)$filename) >= filemtime((string)$reflection->getFileName());
|
||||
}
|
||||
|
||||
return $fileExists;
|
||||
}
|
||||
|
||||
protected function doSave($id, $data, $lifeTime = 0) {
|
||||
$filename = $this->getFilename($id);
|
||||
$result = @file_put_contents($filename, serialize($data));
|
||||
if ($result === false) {
|
||||
throw new \RuntimeException("Error while writing to '$filename'");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function doDelete($id) {
|
||||
@unlink($this->getFilename($id));
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function doFlush() {
|
||||
$directoryContent = glob($this->directory . DIRECTORY_SEPARATOR . '*');
|
||||
if ($directoryContent === false) {
|
||||
return false;
|
||||
}
|
||||
foreach ($directoryContent as $filename) {
|
||||
if (is_file($filename)) {
|
||||
@unlink($filename);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function doGetStats() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getFilename($id) {
|
||||
return $this->directory . DIRECTORY_SEPARATOR . md5($id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Doctrine\Middlewares;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Doctrine\DBAL\Driver\Connection;
|
||||
use MailPoetVendor\Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
|
||||
|
||||
class PostConnectMiddleware extends AbstractDriverMiddleware {
|
||||
public function connect(array $params): Connection {
|
||||
$connection = parent::connect($params);
|
||||
$connection->exec('SET time_zone = "+00:00"');
|
||||
return $connection;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Psr\Cache\CacheItemInterface;
|
||||
use MailPoetVendor\Psr\Cache\CacheItemPoolInterface;
|
||||
|
||||
class PSRArrayCache implements CacheItemPoolInterface {
|
||||
/** @var mixed[] */
|
||||
private $cache = [];
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getItem($key) {
|
||||
if (!is_string($key)) {
|
||||
throw new PSRCacheInvalidArgumentException('Invalid key');
|
||||
}
|
||||
if (!$this->hasItem($key)) {
|
||||
return new PSRCacheItem($key, false);
|
||||
}
|
||||
return new PSRCacheItem($key, $this->cache[$key]);
|
||||
}
|
||||
|
||||
public function getItems(array $keys = []) {
|
||||
return array_map([$this, 'getItem'], $keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function hasItem($key) {
|
||||
return array_key_exists($key, $this->cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function clear() {
|
||||
$this->cache = [];
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function deleteItem($key) {
|
||||
if (!is_string($key)) {
|
||||
throw new PSRCacheInvalidArgumentException('Invalid key');
|
||||
}
|
||||
unset($this->cache[$key]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function deleteItems(array $keys) {
|
||||
try {
|
||||
array_map([$this, 'deleteItem'], $keys);
|
||||
} catch (PSRCacheInvalidArgumentException $e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function save(CacheItemInterface $item) {
|
||||
$this->cache[$item->getKey()] = $item->get();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function saveDeferred(CacheItemInterface $item) {
|
||||
return $this->save($item);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function commit() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Psr\Cache\InvalidArgumentException;
|
||||
|
||||
class PSRCacheInvalidArgumentException extends \Exception implements InvalidArgumentException {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Psr\Cache\CacheItemInterface;
|
||||
|
||||
class PSRCacheItem implements CacheItemInterface {
|
||||
|
||||
/** @var string */
|
||||
private $key;
|
||||
|
||||
/** @var mixed */
|
||||
private $value;
|
||||
|
||||
/** @var bool */
|
||||
private $isHit;
|
||||
|
||||
public function __construct(
|
||||
string $key,
|
||||
bool $isHit
|
||||
) {
|
||||
$this->key = $key;
|
||||
$this->isHit = $isHit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getKey(): string {
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function get() {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function isHit(): bool {
|
||||
// TODO: Implement isHit() method.
|
||||
return $this->isHit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function set($value) {
|
||||
$this->value = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function expiresAt($expiration) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function expiresAfter($time) {
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Psr\Cache\CacheItemInterface;
|
||||
use MailPoetVendor\Psr\Cache\CacheItemPoolInterface;
|
||||
|
||||
class PSRMetadataCache implements CacheItemPoolInterface {
|
||||
/** @var MetadataCache */
|
||||
private $metadataCache;
|
||||
|
||||
public function __construct(
|
||||
string $dir,
|
||||
bool $isReadOnly
|
||||
) {
|
||||
$this->metadataCache = new MetadataCache($dir, $isReadOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getItem($key): CacheItemInterface {
|
||||
if (!$this->hasItem($key)) {
|
||||
return new PSRCacheItem($key, false);
|
||||
}
|
||||
$item = new PSRCacheItem($key, true);
|
||||
$item->set($this->metadataCache->fetch($key));
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getItems(array $keys = []) {
|
||||
if (empty($keys)) {
|
||||
return [];
|
||||
}
|
||||
$foundItems = [];
|
||||
// no internal array function supports this sort of mapping: needs to be iterative
|
||||
// this filters and combines keys in one pass
|
||||
foreach ($keys as $key) {
|
||||
if (!is_string($key)) {
|
||||
throw new PSRCacheInvalidArgumentException('Invalid key');
|
||||
}
|
||||
$foundItems[$key] = $this->getItem($key);
|
||||
}
|
||||
return $foundItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function hasItem($key): bool {
|
||||
return $this->metadataCache->contains($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function clear(): bool {
|
||||
return $this->metadataCache->flushAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function deleteItem($key): bool {
|
||||
return $this->metadataCache->delete($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function deleteItems(array $keys): bool {
|
||||
if (empty($keys)) {
|
||||
return true;
|
||||
}
|
||||
foreach ($keys as $key) {
|
||||
$this->deleteItem($key);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function save(CacheItemInterface $item) {
|
||||
try {
|
||||
return $this->metadataCache->save($item->getKey(), $item->get());
|
||||
} catch (\RuntimeException $e) {
|
||||
throw new PSRCacheInvalidArgumentException($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function saveDeferred(CacheItemInterface $item) {
|
||||
return $this->save($item);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function commit(): bool {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Doctrine\Persistence\Mapping\ProxyClassNameResolver as IProxyClassNameResolver;
|
||||
|
||||
/**
|
||||
* This is exact copy of an anonymous class from \MailPoetVendor\Doctrine\Persistence\Mapping\AbstractClassMetadataFactory
|
||||
* We need to use a non-anonymous class so that it is serializable within integration tests
|
||||
* @see https://github.com/doctrine/persistence/blob/2.2.x/lib/Doctrine/Persistence/Mapping/AbstractClassMetadataFactory.php#L516-L536
|
||||
*/
|
||||
class ProxyClassNameResolver implements IProxyClassNameResolver {
|
||||
/**
|
||||
* @template T
|
||||
* @return class-string<T>
|
||||
*/
|
||||
public function resolveClassName(string $className): string {
|
||||
$pos = \strrpos($className, '\\' . \MailPoetVendor\Doctrine\Persistence\Proxy::MARKER . '\\');
|
||||
if ($pos === \false) {
|
||||
/** @var class-string<T> */
|
||||
return $className;
|
||||
}
|
||||
/** @var class-string<T> */
|
||||
return \substr($className, $pos + \MailPoetVendor\Doctrine\Persistence\Proxy::MARKER_LENGTH + 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Doctrine\Common\Collections\Collection;
|
||||
use MailPoetVendor\Doctrine\Common\Collections\Criteria;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityManager;
|
||||
use MailPoetVendor\Doctrine\ORM\EntityRepository as DoctrineEntityRepository;
|
||||
use MailPoetVendor\Doctrine\ORM\Mapping\ClassMetadata;
|
||||
|
||||
/**
|
||||
* @template T of object
|
||||
*/
|
||||
abstract class Repository {
|
||||
/** @var EntityManager */
|
||||
protected $entityManager;
|
||||
|
||||
/** @var ClassMetadata<object> */
|
||||
protected $classMetadata;
|
||||
|
||||
/** @var DoctrineEntityRepository<T> */
|
||||
protected $doctrineRepository;
|
||||
|
||||
/** @var string[] */
|
||||
protected $ignoreColumnsForUpdate = [
|
||||
'created_at',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->classMetadata = $entityManager->getClassMetadata($this->getEntityClassName());
|
||||
$this->doctrineRepository = new DoctrineEntityRepository($this->entityManager, $this->classMetadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $criteria
|
||||
* @param array|null $orderBy
|
||||
* @param int|null $limit
|
||||
* @param int|null $offset
|
||||
* @return T[]
|
||||
*/
|
||||
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) {
|
||||
return $this->doctrineRepository->findBy($criteria, $orderBy, $limit, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Criteria $criteria
|
||||
* @return Collection<int, T>
|
||||
*/
|
||||
public function matching(Criteria $criteria) {
|
||||
return $this->doctrineRepository->matching($criteria);
|
||||
}
|
||||
|
||||
public function countBy(array $criteria): int {
|
||||
return $this->doctrineRepository->count($criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $criteria
|
||||
* @param array|null $orderBy
|
||||
* @return T|null
|
||||
*/
|
||||
public function findOneBy(array $criteria, array $orderBy = null) {
|
||||
return $this->doctrineRepository->findOneBy($criteria, $orderBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $id
|
||||
* @return T|null
|
||||
*/
|
||||
public function findOneById($id) {
|
||||
return $this->doctrineRepository->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return T[]
|
||||
*/
|
||||
public function findAll() {
|
||||
return $this->doctrineRepository->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param T $entity
|
||||
*/
|
||||
public function persist($entity) {
|
||||
$this->entityManager->persist($entity);
|
||||
}
|
||||
|
||||
public function truncate() {
|
||||
$tableName = $this->classMetadata->getTableName();
|
||||
$connection = $this->entityManager->getConnection();
|
||||
$connection->executeQuery('SET FOREIGN_KEY_CHECKS=0');
|
||||
$q = "TRUNCATE $tableName";
|
||||
$connection->executeStatement($q);
|
||||
$connection->executeQuery('SET FOREIGN_KEY_CHECKS=1');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param T $entity
|
||||
*/
|
||||
public function remove($entity) {
|
||||
$this->entityManager->remove($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param T $entity
|
||||
*/
|
||||
public function refresh($entity) {
|
||||
$this->entityManager->refresh($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(T): bool|null $filter
|
||||
*/
|
||||
public function refreshAll(callable $filter = null): void {
|
||||
$entities = $this->getAllFromIdentityMap();
|
||||
foreach ($entities as $entity) {
|
||||
if ($filter && !$filter($entity)) {
|
||||
continue;
|
||||
}
|
||||
$this->entityManager->refresh($entity);
|
||||
}
|
||||
}
|
||||
|
||||
public function flush() {
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function getReference($id) {
|
||||
return $this->entityManager->getReference($this->getEntityClassName(), $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param T $entity
|
||||
*/
|
||||
public function detach($entity) {
|
||||
$this->entityManager->detach($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(T): bool|null $filter
|
||||
*/
|
||||
public function detachAll(callable $filter = null): void {
|
||||
$entities = $this->getAllFromIdentityMap();
|
||||
foreach ($entities as $entity) {
|
||||
if ($filter && !$filter($entity)) {
|
||||
continue;
|
||||
}
|
||||
$this->entityManager->detach($entity);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return T[] */
|
||||
public function getAllFromIdentityMap(): array {
|
||||
$className = $this->getEntityClassName();
|
||||
$rootClassName = $this->entityManager->getClassMetadata($className)->rootEntityName;
|
||||
$entities = $this->entityManager->getUnitOfWork()->getIdentityMap()[$rootClassName] ?? [];
|
||||
|
||||
$result = [];
|
||||
foreach ($entities as $entity) {
|
||||
if ($entity instanceof $className) {
|
||||
$result[] = $entity;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getTableName(): string {
|
||||
return $this->classMetadata->getTableName();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return class-string<T>
|
||||
*/
|
||||
abstract protected function getEntityClassName();
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Config\Env;
|
||||
use MailPoetVendor\Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use MailPoetVendor\Doctrine\ORM\Mapping\ClassMetadataFactory;
|
||||
use MailPoetVendor\Doctrine\ORM\Mapping\ClassMetadataInfo;
|
||||
|
||||
// Taken from Doctrine docs (see link bellow) but implemented in metadata factory instead of an event
|
||||
// because we need to add prefix at runtime, not at metadata dump (which is included in builds).
|
||||
// @see https://www.doctrine-project.org/projects/doctrine-orm/en/2.5/cookbook/sql-table-prefixes.html
|
||||
class TablePrefixMetadataFactory extends ClassMetadataFactory {
|
||||
// WordPress tables that are used by MailPoet via Doctrine
|
||||
const WP_TABLES = [
|
||||
'posts',
|
||||
];
|
||||
|
||||
/** @var string */
|
||||
private $prefix;
|
||||
|
||||
/** @var string */
|
||||
private $wpDbPrefix;
|
||||
|
||||
/** @var array */
|
||||
private $prefixedMap = [];
|
||||
|
||||
public function __construct() {
|
||||
if (Env::$dbPrefix === null) {
|
||||
throw new \RuntimeException('DB table prefix not initialized');
|
||||
}
|
||||
$this->prefix = Env::$dbPrefix;
|
||||
$this->wpDbPrefix = Env::$wpDbPrefix;
|
||||
$this->setProxyClassNameResolver(new ProxyClassNameResolver());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ClassMetadata<object>
|
||||
*/
|
||||
public function getMetadataFor($className) {
|
||||
$classMetadata = parent::getMetadataFor($className);
|
||||
if (isset($this->prefixedMap[$classMetadata->getName()])) {
|
||||
return $classMetadata;
|
||||
}
|
||||
|
||||
// prefix tables only after they are saved to cache so the prefix does not get included in cache
|
||||
// (getMetadataFor can call itself recursively but it saves to cache only after the recursive calls)
|
||||
$cacheKey = $this->getCacheKey($classMetadata->getName());
|
||||
$isCached = ($cache = $this->getCache()) ? $cache->hasItem($cacheKey) : false;
|
||||
if ($classMetadata instanceof ClassMetadata && $isCached) {
|
||||
$this->addPrefix($classMetadata);
|
||||
$this->prefixedMap[$classMetadata->getName()] = true;
|
||||
}
|
||||
return $classMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ClassMetadata<object> $classMetadata
|
||||
*/
|
||||
public function addPrefix(ClassMetadata $classMetadata) {
|
||||
if (!$classMetadata->isInheritanceTypeSingleTable() || $classMetadata->getName() === $classMetadata->rootEntityName) {
|
||||
$classMetadata->setPrimaryTable([
|
||||
'name' => $this->createPrefixedName($classMetadata->getTableName()),
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($classMetadata->getAssociationMappings() as $fieldName => $mapping) {
|
||||
if ($mapping['type'] === ClassMetadataInfo::MANY_TO_MANY && $mapping['isOwningSide']) {
|
||||
/** @var string $mappedTableName */
|
||||
$mappedTableName = $mapping['joinTable']['name'];
|
||||
$classMetadata->associationMappings[$fieldName]['joinTable']['name'] = $this->createPrefixedName($mappedTableName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MailPoet tables are prefixed by WP prefix + plugin prefix.
|
||||
* For entities for WP tables we use WP prefix only.
|
||||
*/
|
||||
private function createPrefixedName(string $tableName): string {
|
||||
// Use WP prefix for WP tables
|
||||
if (in_array($tableName, self::WP_TABLES, true)) {
|
||||
return $this->wpDbPrefix . $tableName;
|
||||
}
|
||||
// Use WP + plugin prefix for MailPoet tables
|
||||
return $this->prefix . $tableName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine\Types;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Doctrine\DBAL\ParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use MailPoetVendor\Doctrine\DBAL\Types\BigIntType as DoctrineBigIntType;
|
||||
|
||||
class BigIntType extends DoctrineBigIntType {
|
||||
// override Doctrine's bigint type that historically maps DB's "bigint" to PHP's "string"
|
||||
// (we want to map DB's "bigint" to PHP's "int" in today's 64-bit world)
|
||||
const NAME = 'bigint';
|
||||
|
||||
public function getBindingType() {
|
||||
return ParameterType::INTEGER;
|
||||
}
|
||||
|
||||
public function convertToPHPValue($value, AbstractPlatform $platform) {
|
||||
return $value === null ? null : (int)$value;
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
return self::NAME;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine\Types;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Carbon\Carbon;
|
||||
use MailPoetVendor\Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use MailPoetVendor\Doctrine\DBAL\Types\DateTimeTzType;
|
||||
|
||||
class DateTimeTzToStringType extends DateTimeTzType {
|
||||
const NAME = 'datetimetz_to_string';
|
||||
|
||||
public function convertToPHPValue($value, AbstractPlatform $platform) {
|
||||
$dateTime = parent::convertToPHPValue($value, $platform);
|
||||
|
||||
if (!$dateTime) {
|
||||
return $dateTime;
|
||||
}
|
||||
|
||||
return Carbon::instance($dateTime);
|
||||
}
|
||||
|
||||
public function requiresSQLCommentHint(AbstractPlatform $platform): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return self::NAME;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine\Types;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
|
||||
class JsonOrSerializedType extends JsonType {
|
||||
const NAME = 'json_or_serialized';
|
||||
|
||||
public 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);
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
return self::NAME;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine\Types;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use MailPoetVendor\Doctrine\DBAL\Types\Type;
|
||||
|
||||
class JsonType extends Type {
|
||||
const NAME = 'json';
|
||||
|
||||
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) {
|
||||
return $platform->getJsonTypeDeclarationSQL($fieldDeclaration);
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
|
||||
public function convertToPHPValue($value, AbstractPlatform $platform) {
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_resource($value)) {
|
||||
$value = stream_get_contents($value);
|
||||
}
|
||||
|
||||
$value = mb_convert_encoding((string)$value, 'UTF-8', 'UTF-8'); // sanitize invalid utf8
|
||||
$decoded = json_decode($value, true);
|
||||
$this->handleErrors();
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
public function requiresSQLCommentHint(AbstractPlatform $platform) {
|
||||
return !$platform->hasNativeJsonType();
|
||||
}
|
||||
|
||||
private function handleErrors() {
|
||||
$error = json_last_error();
|
||||
if ($error !== JSON_ERROR_NONE) {
|
||||
throw new \RuntimeException('Error when parsing JSON database value: "' . json_last_error_msg() . '"', $error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine\Types;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use MailPoetVendor\Doctrine\DBAL\Types\Type;
|
||||
|
||||
class SerializedArrayType extends Type {
|
||||
const NAME = 'serialized_array';
|
||||
|
||||
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) {
|
||||
return $platform->getClobTypeDeclarationSQL($fieldDeclaration);
|
||||
}
|
||||
|
||||
public function convertToDatabaseValue($value, AbstractPlatform $platform) {
|
||||
return \serialize($value);
|
||||
}
|
||||
|
||||
public function convertToPHPValue($value, AbstractPlatform $platform) {
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
$value = \is_resource($value) ? \stream_get_contents($value) : $value;
|
||||
$val = \unserialize($value);
|
||||
if ($val === \false && $value !== 'b:0;') {
|
||||
return null;
|
||||
}
|
||||
return $val;
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
return self::NAME;
|
||||
}
|
||||
|
||||
public function requiresSQLCommentHint(AbstractPlatform $platform) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine\Validator;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Symfony\Contracts\Translation\TranslatorTrait;
|
||||
|
||||
class Translator implements \MailPoetVendor\Symfony\Contracts\Translation\TranslatorInterface {
|
||||
|
||||
use TranslatorTrait;
|
||||
|
||||
public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null) {
|
||||
return $this->trans($id, ['%count%' => $number] + $parameters, $domain, $locale);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine\Validator;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Symfony\Component\Validator\ConstraintViolationInterface;
|
||||
use MailPoetVendor\Symfony\Component\Validator\ConstraintViolationListInterface;
|
||||
|
||||
class ValidationException extends \RuntimeException {
|
||||
/** @var string */
|
||||
private $resourceName;
|
||||
|
||||
/** @var ConstraintViolationListInterface|ConstraintViolationInterface[] */
|
||||
private $violations;
|
||||
|
||||
public function __construct(
|
||||
$resourceName,
|
||||
ConstraintViolationListInterface $violations
|
||||
) {
|
||||
$this->resourceName = $resourceName;
|
||||
$this->violations = $violations;
|
||||
|
||||
$linePrefix = ' ';
|
||||
$message = "Validation failed for '$resourceName'.\nDetails:\n";
|
||||
$message .= $linePrefix . implode("\n$linePrefix", $this->getErrors());
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
public function getResourceName() {
|
||||
return $this->resourceName;
|
||||
}
|
||||
|
||||
/** @return ConstraintViolationListInterface|ConstraintViolationInterface[] */
|
||||
public function getViolations() {
|
||||
return $this->violations;
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function getErrors() {
|
||||
$messages = [];
|
||||
foreach ($this->violations as $violation) {
|
||||
$messages[] = $this->formatError($violation);
|
||||
}
|
||||
sort($messages);
|
||||
return $messages;
|
||||
}
|
||||
|
||||
private function formatError(ConstraintViolationInterface $violation) {
|
||||
return '[' . $violation->getPropertyPath() . '] ' . $violation->getMessage();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Doctrine\Validator;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Doctrine\Annotations\AnnotationReaderProvider;
|
||||
use MailPoet\Doctrine\PSRMetadataCache;
|
||||
use MailPoetVendor\Symfony\Component\Validator\Validation;
|
||||
|
||||
class ValidatorFactory {
|
||||
const METADATA_DIR = __DIR__ . '/../../../generated/validator-metadata';
|
||||
|
||||
/** @var AnnotationReaderProvider */
|
||||
private $annotationReaderProvider;
|
||||
|
||||
public function __construct(
|
||||
AnnotationReaderProvider $annotationReaderProvider
|
||||
) {
|
||||
$this->annotationReaderProvider = $annotationReaderProvider;
|
||||
}
|
||||
|
||||
public function createValidator() {
|
||||
$builder = Validation::createValidatorBuilder();
|
||||
// we need to use our own translator here.
|
||||
// If we let the default translator to be used in the builder it uses an anonymous class and that is a problem
|
||||
// All integration tests would fail with: [Exception] Serialization of 'class@anonymous' is not allowed
|
||||
$translator = new Translator();
|
||||
$translator->setLocale('en');
|
||||
$builder->setTranslator($translator);
|
||||
|
||||
// annotation reader exists only in dev environment, on production cache is pre-generated
|
||||
$annotationReader = $this->annotationReaderProvider->getAnnotationReader();
|
||||
if ($annotationReader) {
|
||||
$builder->setDoctrineAnnotationReader($annotationReader)
|
||||
->enableAnnotationMapping(true);
|
||||
}
|
||||
|
||||
// metadata cache (for production cache is pre-generated at build time)
|
||||
$isReadOnly = !$annotationReader;
|
||||
$builder->setMappingCache(new PSRMetadataCache(self::METADATA_DIR, $isReadOnly));
|
||||
|
||||
return $builder->getValidator();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Doctrine\WPDB;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Doctrine\WPDB\Exceptions\ConnectionException;
|
||||
use MailPoet\Doctrine\WPDB\Exceptions\QueryException;
|
||||
use MailPoetVendor\Doctrine\DBAL\Driver\ServerInfoAwareConnection;
|
||||
use MailPoetVendor\Doctrine\DBAL\ParameterType;
|
||||
use mysqli;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use Throwable;
|
||||
use wpdb;
|
||||
|
||||
/**
|
||||
* @phpcs:disable Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
*/
|
||||
class Connection implements ServerInfoAwareConnection {
|
||||
public function __construct() {
|
||||
global $wpdb;
|
||||
if (!$wpdb instanceof wpdb) {
|
||||
throw new ConnectionException('WPDB is not initialized.');
|
||||
}
|
||||
}
|
||||
|
||||
public function prepare(string $sql): Statement {
|
||||
return new Statement($this, $sql);
|
||||
}
|
||||
|
||||
public function query(string $sql): Result {
|
||||
global $wpdb;
|
||||
$value = $this->runQuery($sql);
|
||||
$result = $wpdb->last_result;
|
||||
return new Result($result, is_int($value) ? $value : 0);
|
||||
}
|
||||
|
||||
public function exec(string $sql): int {
|
||||
global $wpdb;
|
||||
$this->runQuery($sql);
|
||||
return $wpdb->rows_affected;
|
||||
}
|
||||
|
||||
public function beginTransaction(): bool {
|
||||
$this->runQuery('START TRANSACTION');
|
||||
return true;
|
||||
}
|
||||
|
||||
public function commit(): bool {
|
||||
$this->runQuery('COMMIT');
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rollBack(): bool {
|
||||
$this->runQuery('ROLLBACK');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes a string for use in a query.
|
||||
* The type hint parameter is not needed for WPDB (mysqli).
|
||||
* See also Doctrine\DBAL\Driver\Mysqli\Connection::quote().
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param int $type
|
||||
*/
|
||||
public function quote($value, $type = ParameterType::STRING): string {
|
||||
global $wpdb;
|
||||
return "'" . $wpdb->_escape($value) . "'";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $name
|
||||
*/
|
||||
public function lastInsertId($name = null): int {
|
||||
global $wpdb;
|
||||
return $wpdb->insert_id;
|
||||
}
|
||||
|
||||
public function getServerVersion(): string {
|
||||
global $wpdb;
|
||||
return $wpdb->db_server_info();
|
||||
}
|
||||
|
||||
/**
|
||||
* MySQL — returns an instance of mysqli.
|
||||
* SQLite — returns an instance of PDO.
|
||||
*
|
||||
* @return mysqli|PDO|false|null
|
||||
*/
|
||||
public function getNativeConnection() {
|
||||
global $wpdb;
|
||||
|
||||
// WPDB keeps connection instance (mysqli) in a protected property $dbh.
|
||||
// We can access it using a closure that is bound to the $wpdb instance.
|
||||
$getDbh = function () {
|
||||
return $this->dbh; // @phpstan-ignore-line -- PHPStan doesn't know the binding context
|
||||
};
|
||||
$dbh = $getDbh->call($wpdb);
|
||||
if (is_object($dbh) && method_exists($dbh, 'get_pdo')) {
|
||||
return $dbh->get_pdo();
|
||||
}
|
||||
return $getDbh->call($wpdb);
|
||||
}
|
||||
|
||||
public static function isSQLite(): bool {
|
||||
return defined('DB_ENGINE') && DB_ENGINE === 'sqlite';
|
||||
}
|
||||
|
||||
private function runQuery(string $sql) {
|
||||
global $wpdb;
|
||||
try {
|
||||
$value = $wpdb->query($sql); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
} catch (Throwable $e) {
|
||||
if ($e instanceof PDOException) {
|
||||
throw new QueryException($e->getMessage(), $e->errorInfo[0] ?? null, $e->errorInfo[1] ?? 0);
|
||||
}
|
||||
throw new QueryException($e->getMessage(), null, 0, $e);
|
||||
}
|
||||
if ($value === false) {
|
||||
$this->handleQueryError();
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function handleQueryError(): void {
|
||||
global $wpdb;
|
||||
$nativeConnection = $this->getNativeConnection();
|
||||
if ($nativeConnection instanceof mysqli) {
|
||||
throw new QueryException($wpdb->last_error, $nativeConnection->sqlstate, $nativeConnection->errno);
|
||||
} elseif ($nativeConnection instanceof PDO) {
|
||||
$info = $nativeConnection->errorInfo();
|
||||
throw new QueryException($wpdb->last_error, $info[0] ?? null, $info[1] ?? 0);
|
||||
}
|
||||
throw new QueryException($wpdb->last_error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php declare (strict_types = 1);
|
||||
|
||||
namespace MailPoet\Doctrine\WPDB;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Doctrine\WPDB\Exceptions\MissingParameterException;
|
||||
use MailPoetVendor\Doctrine\DBAL\ParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\SQL\Parser\Visitor;
|
||||
|
||||
class ConvertParameters implements Visitor {
|
||||
private const PARAM_TYPE_MAP = [
|
||||
ParameterType::STRING => '%s',
|
||||
ParameterType::INTEGER => '%d',
|
||||
ParameterType::ASCII => '%s',
|
||||
ParameterType::BINARY => '%s',
|
||||
ParameterType::BOOLEAN => '%d',
|
||||
ParameterType::NULL => '%s',
|
||||
ParameterType::LARGE_OBJECT => '%s',
|
||||
];
|
||||
|
||||
/** @var list<string> */
|
||||
private array $buffer = [];
|
||||
|
||||
/** @var array<array-key, array{0: string, 1: mixed, 2: int}> */
|
||||
private array $params;
|
||||
|
||||
private array $values = [];
|
||||
|
||||
private int $cursor = 1;
|
||||
|
||||
public function __construct(
|
||||
array $params
|
||||
) {
|
||||
$this->params = $params;
|
||||
}
|
||||
|
||||
public function acceptPositionalParameter(string $sql): void {
|
||||
$position = $this->cursor++;
|
||||
$this->acceptParameter($position);
|
||||
}
|
||||
|
||||
public function acceptNamedParameter(string $sql): void {
|
||||
$this->acceptParameter(trim($sql, ':'));
|
||||
}
|
||||
|
||||
public function acceptOther(string $sql): void {
|
||||
$this->buffer[] = $sql;
|
||||
}
|
||||
|
||||
public function getSQL(): string {
|
||||
return implode('', $this->buffer);
|
||||
}
|
||||
|
||||
public function getValues(): array {
|
||||
return $this->values;
|
||||
}
|
||||
|
||||
/** @param array-key $key */
|
||||
private function acceptParameter($key): void {
|
||||
if (!array_key_exists($key, $this->params)) {
|
||||
throw new MissingParameterException(sprintf("Parameter '%s' was defined in the query, but not provided.", $key));
|
||||
}
|
||||
[, $value, $type] = $this->params[$key];
|
||||
|
||||
// WPDB doesn't support NULL values. We need to handle them explicitly.
|
||||
if ($value === null) {
|
||||
$this->buffer[] = 'NULL';
|
||||
return;
|
||||
}
|
||||
|
||||
// WPDB doesn't accept non-scalar values. We need to cast them (PDO-like behavior).
|
||||
if (!is_scalar($value)) {
|
||||
if ($type === ParameterType::INTEGER) {
|
||||
$value = (int)$value; // @phpstan-ignore-line -- cast may fail and that's OK
|
||||
} elseif ($type === ParameterType::BOOLEAN) {
|
||||
$value = (bool)$value;
|
||||
} else {
|
||||
$value = (string)$value; // @phpstan-ignore-line -- cast may fail and that's OK
|
||||
}
|
||||
}
|
||||
|
||||
$this->values[] = $value;
|
||||
$this->buffer[] = self::PARAM_TYPE_MAP[$type] ?? '%s';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Doctrine\WPDB;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Doctrine\DBAL\Driver\AbstractMySQLDriver;
|
||||
|
||||
class Driver extends AbstractMySQLDriver {
|
||||
public function connect(array $params): Connection {
|
||||
return new Connection();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Doctrine\WPDB\Exceptions;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Doctrine\DBAL\Driver\AbstractException;
|
||||
|
||||
class ConnectionException extends AbstractException {
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Doctrine\WPDB\Exceptions;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use Exception;
|
||||
|
||||
class MissingParameterException extends Exception {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Doctrine\WPDB\Exceptions;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use Exception;
|
||||
|
||||
class NotSupportedException extends Exception {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Doctrine\WPDB\Exceptions;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Doctrine\DBAL\Driver\AbstractException;
|
||||
|
||||
class QueryException extends AbstractException {
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Doctrine\WPDB;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoetVendor\Doctrine\DBAL\Driver\Result as ResultInterface;
|
||||
|
||||
/**
|
||||
* WPDB fetches all results from the underlying database driver,
|
||||
* so we need to implement the result methods on in-memory data.
|
||||
*/
|
||||
class Result implements ResultInterface {
|
||||
/** @var array[] */
|
||||
private array $result = [];
|
||||
private int $rowCount;
|
||||
private int $cursor = 0;
|
||||
|
||||
public function __construct(
|
||||
array $result,
|
||||
int $rowCount
|
||||
) {
|
||||
foreach ($result as $row) {
|
||||
$this->result[] = (array)$row;
|
||||
}
|
||||
$this->rowCount = $rowCount;
|
||||
}
|
||||
|
||||
public function fetchNumeric() {
|
||||
$row = $this->result[$this->cursor++] ?? null;
|
||||
return $row === null ? false : array_values($row);
|
||||
}
|
||||
|
||||
public function fetchAssociative() {
|
||||
return $this->result[$this->cursor++] ?? false;
|
||||
}
|
||||
|
||||
public function fetchOne() {
|
||||
$row = $this->result[$this->cursor++] ?? null;
|
||||
return $row === null ? false : reset($row);
|
||||
}
|
||||
|
||||
public function fetchAllNumeric(): array {
|
||||
$result = [];
|
||||
foreach ($this->result as $row) {
|
||||
$result[] = array_values($row);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function fetchAllAssociative(): array {
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
public function fetchFirstColumn(): array {
|
||||
$result = [];
|
||||
foreach ($this->result as $row) {
|
||||
$result[] = reset($row);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function rowCount(): int {
|
||||
return $this->rowCount;
|
||||
}
|
||||
|
||||
public function columnCount(): int {
|
||||
return count($this->result[0] ?? []);
|
||||
}
|
||||
|
||||
public function free(): void {
|
||||
$this->cursor = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Doctrine\WPDB;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Doctrine\WPDB\Exceptions\NotSupportedException;
|
||||
use MailPoetVendor\Doctrine\DBAL\Driver\Result;
|
||||
use MailPoetVendor\Doctrine\DBAL\Driver\Statement as StatementInterface;
|
||||
use MailPoetVendor\Doctrine\DBAL\ParameterType;
|
||||
use MailPoetVendor\Doctrine\DBAL\SQL\Parser;
|
||||
|
||||
class Statement implements StatementInterface {
|
||||
private Connection $connection;
|
||||
private Parser $parser;
|
||||
private string $sql;
|
||||
private array $params = [];
|
||||
|
||||
public function __construct(
|
||||
Connection $connection,
|
||||
string $sql
|
||||
) {
|
||||
$this->connection = $connection;
|
||||
$this->parser = new Parser(false);
|
||||
$this->sql = $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|int $param
|
||||
* @param mixed $value
|
||||
* @param int $type
|
||||
* @return true
|
||||
*/
|
||||
public function bindValue($param, $value, $type = ParameterType::STRING) {
|
||||
$this->params[$param] = [$param, $value, $type];
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|int $param
|
||||
* @param mixed $variable
|
||||
* @param int $type
|
||||
* @param int|null $length
|
||||
* @return true
|
||||
*/
|
||||
public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null) {
|
||||
throw new NotSupportedException(
|
||||
'Statement::bindParam() is deprecated in Doctrine and not implemented in WPDB driver. Use Statement::bindValue() instead.'
|
||||
);
|
||||
}
|
||||
|
||||
public function execute($params = null): Result {
|
||||
if ($params !== null) {
|
||||
throw new NotSupportedException(
|
||||
'Statement::execute() with parameters is deprecated in Doctrine and not implemented in WPDB driver. Use Statement::bindValue() instead.'
|
||||
);
|
||||
}
|
||||
|
||||
// Convert '?' parameters to WPDB format (sprintf-like: '%s', '%d', ...),
|
||||
// and add support for named parameters that are not supported by mysqli.
|
||||
$visitor = new ConvertParameters($this->params);
|
||||
$this->parser->parse($this->sql, $visitor);
|
||||
$sql = $visitor->getSQL();
|
||||
$values = $visitor->getValues();
|
||||
|
||||
global $wpdb;
|
||||
$query = count($values) > 0
|
||||
? $wpdb->prepare($sql, $values) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
: $sql;
|
||||
return $this->connection->query($query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
Reference in New Issue
Block a user