Skip to content

Commit

Permalink
feat: entity duplication
Browse files Browse the repository at this point in the history
  • Loading branch information
williarin committed Jul 14, 2022
1 parent a08f4a5 commit 020d04f
Show file tree
Hide file tree
Showing 16 changed files with 486 additions and 47 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,21 @@ $repository->persist($post);
$manager->persist($post);
```

### Entity duplication
Duplicate an entity with all its EAV attributes and terms with `DuplicationService`.
The resulting entity is already persisted and has a new ID.

```php
$duplicationService = new DuplicationService($entityManager, new AsciiSlugger();

// Duplicate by ID
$newProduct = $this->duplicationService->duplicate(23, Product::class);

// Duplicate by object
$product = $manager->getRepository(Product::class)->findOneBySku('woo-hoodie-with-zipper');
$newProduct = $this->duplicationService->duplicate($product);
```

### Available entities and repositories

* `Post` and `PostRepository`
Expand Down
15 changes: 15 additions & 0 deletions src/Attributes/Slug.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Williarin\WordpressInterop\Attributes;

use Attribute;

/**
* Mark the property as slug. When duplicating an entity, all slug properties will be slugged.
*/
#[Attribute(Attribute::TARGET_PROPERTY)]
final class Slug
{
}
15 changes: 15 additions & 0 deletions src/Attributes/Unique.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Williarin\WordpressInterop\Attributes;

use Attribute;

/**
* Mark the property as unique. When duplicating an entity, all unique properties will have a suffix appended.
*/
#[Attribute(Attribute::TARGET_PROPERTY)]
final class Unique
{
}
26 changes: 6 additions & 20 deletions src/Bridge/Entity/BaseEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@

use DateTimeInterface;
use Symfony\Component\Serializer\Annotation\Groups;
use Williarin\WordpressInterop\Attributes\Slug;
use Williarin\WordpressInterop\Attributes\Unique;

abstract class BaseEntity
{
use DynamicPropertiesTrait;

#[Groups('base')]
public ?int $id = null;

Expand All @@ -24,6 +28,7 @@ abstract class BaseEntity
#[Groups('base')]
public ?string $postContent = null;

#[Unique]
#[Groups('base')]
public ?string $postTitle = null;

Expand All @@ -42,6 +47,7 @@ abstract class BaseEntity
#[Groups('base')]
public ?string $postPassword = null;

#[Unique, Slug]
#[Groups('base')]
public ?string $postName = null;

Expand Down Expand Up @@ -77,24 +83,4 @@ abstract class BaseEntity

#[Groups('base')]
public ?int $commentCount = null;

public function __set(string $property, mixed $value): void
{
if ($value === '' && !is_string($this->{$property})) {
return;
}

try {
$expectedType = (new \ReflectionProperty(static::class, $property))->getType()->getName();
settype($value, $expectedType);
} catch (\ReflectionException) {
}

$this->{$property} = $value;
}

public function __get(string $property): mixed
{
return $this->{$property};
}
}
28 changes: 28 additions & 0 deletions src/Bridge/Entity/DynamicPropertiesTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Williarin\WordpressInterop\Bridge\Entity;

trait DynamicPropertiesTrait
{
public function __set(string $property, mixed $value): void
{
if ($value === '' && !is_string($this->{$property})) {
return;
}

try {
$expectedType = (new \ReflectionProperty(static::class, $property))->getType()->getName();
settype($value, $expectedType);
} catch (\ReflectionException) {
}

$this->{$property} = $value;
}

public function __get(string $property): mixed
{
return $this->{$property};
}
}
4 changes: 4 additions & 0 deletions src/Bridge/Entity/PostMeta.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@
#[RepositoryClass(PostMetaRepository::class)]
class PostMeta
{
// public ?int $metaId = null;
// public ?int $postId = null;
// public ?string $metaKey = null;
// public string|array|int|float|bool|null $metaValue = null;
}
3 changes: 3 additions & 0 deletions src/Bridge/Entity/Product.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace Williarin\WordpressInterop\Bridge\Entity;

use Williarin\WordpressInterop\Attributes\RepositoryClass;
use Williarin\WordpressInterop\Attributes\Slug;
use Williarin\WordpressInterop\Attributes\Unique;
use Williarin\WordpressInterop\Bridge\Repository\ProductRepository;
use Williarin\WordpressInterop\Bridge\Type\GenericData;

Expand All @@ -22,6 +24,7 @@
#[RepositoryClass(ProductRepository::class)]
class Product extends BaseEntity
{
#[Unique, Slug]
public ?string $sku = null;
public ?string $taxStatus = null;
public ?string $taxClass = null;
Expand Down
2 changes: 2 additions & 0 deletions src/Bridge/Entity/Term.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
#[RepositoryClass(TermRepository::class)]
class Term
{
use DynamicPropertiesTrait;

#[Id]
#[Groups('base')]
public ?int $termId = null;
Expand Down
56 changes: 32 additions & 24 deletions src/Bridge/Repository/AbstractEntityRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
use Williarin\WordpressInterop\Bridge\Entity\BaseEntity;
use Williarin\WordpressInterop\Bridge\Entity\PostMeta;
use Williarin\WordpressInterop\Criteria\NestedCondition;
use Williarin\WordpressInterop\Criteria\Operand;
use Williarin\WordpressInterop\Criteria\PostRelationshipCondition;
Expand All @@ -34,6 +35,7 @@ abstract class AbstractEntityRepository implements EntityRepositoryInterface
protected const MAPPED_FIELDS = [];
protected const TABLE_NAME = 'posts';
protected const TABLE_META_NAME = 'postmeta';
protected const META_ENTITY_CLASS_NAME = PostMeta::class;
protected const TABLE_IDENTIFIER = 'id';
protected const TABLE_META_IDENTIFIER = 'post_id';
protected const FALLBACK_ENTITY = BaseEntity::class;
Expand Down Expand Up @@ -74,6 +76,11 @@ public function getEntityClassName(): string
return $this->entityClassName;
}

public function getMetaEntityClassName(): string
{
return static::META_ENTITY_CLASS_NAME;
}

public function setEntityManager(EntityManagerInterface $entityManager): void
{
$this->entityManager = $entityManager;
Expand Down Expand Up @@ -187,6 +194,30 @@ public function persist(mixed $entity): void
}
}

public function getMappedMetaKey(mixed $fieldName, string $entityClassName = null): string
{
$mappedFields = $entityClassName ? (new \ReflectionClassConstant(
$this->entityManager->getRepository($entityClassName),
'MAPPED_FIELDS',
))->getValue() : static::MAPPED_FIELDS;

if (
!is_array($mappedFields)
|| empty($mappedFields)
|| !in_array($fieldName, $mappedFields, true)
) {
return $fieldName;
}

$key = array_search($fieldName, $mappedFields, true);

if (is_numeric($key)) {
return sprintf('_%s', $fieldName);
}

return $key;
}

protected function normalize(string $field, mixed $value): string
{
$value = $this->validateFieldValue($field, $value);
Expand All @@ -213,30 +244,6 @@ protected function doUpdate(string $name, array $arguments): bool
return $this->updateSingleField($arguments[0], property_to_field(substr($name, 6)), $arguments[1]);
}

protected function getMappedMetaKey(mixed $fieldName, string $entityClassName = null): string
{
$mappedFields = $entityClassName ? (new \ReflectionClassConstant(
$this->entityManager->getRepository($entityClassName),
'MAPPED_FIELDS',
))->getValue() : static::MAPPED_FIELDS;

if (
!is_array($mappedFields)
|| empty($mappedFields)
|| !in_array($fieldName, $mappedFields, true)
) {
return $fieldName;
}

$key = array_search($fieldName, $mappedFields, true);

if (is_numeric($key)) {
return sprintf('_%s', $fieldName);
}

return $key;
}

protected function addSelectForExtraFields(QueryBuilder $queryBuilder): void
{
$extraFields = $this->getEntityExtraFields();
Expand Down Expand Up @@ -502,6 +509,7 @@ private function createPostRelationshipCriteria(
)
;

$this->additionalFieldsToSelect[] = 'tt.term_taxonomy_id';
$this->addPostMetaJoinForPostRelationshipCondition($queryBuilder, $condition, $aliasNumber);
$prefixedCriteria = $this->getPrefixedCriteriaForPostRelationshipCondition($condition, $aliasNumber);
$normalizedCriteria = $this->normalizeCriteria($prefixedCriteria, $condition->getEntityClassName());
Expand Down
2 changes: 2 additions & 0 deletions src/Bridge/Repository/EntityRepositoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ public function createFindByQueryBuilder(array $criteria, ?array $orderBy): Quer
public function updateSingleField(int $id, string $field, mixed $newValue): bool;

public function persist(mixed $entity): void;

public function getMetaEntityClassName(): string;
}
23 changes: 22 additions & 1 deletion src/Bridge/Repository/PostMetaRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function getEntityClassName(): string
return PostMeta::class;
}

public function find(int $postId, string $metaKey, bool $unserialize = true): string|array|int|bool|null
public function find(int $postId, string $metaKey, bool $unserialize = true): string|array|int|float|bool|null
{
/** @var string|false $result */
$result = $this->entityManager->getConnection()
Expand All @@ -46,6 +46,27 @@ public function find(int $postId, string $metaKey, bool $unserialize = true): st
return $unserialize ? unserialize_if_needed($result) : $result;
}

/**
* @return array<string, string|array|int|float|bool|null>
*/
public function findBy(int $postId, bool $unserialize = true): array
{
$result = $this->entityManager->getConnection()
->createQueryBuilder()
->select('meta_key', 'meta_value')
->from($this->entityManager->getTablesPrefix() . 'postmeta')
->where('post_id = :id')
->setParameter('id', $postId)
->executeQuery()
->fetchAllAssociative()
;

return array_map(
static fn (string $value) => $unserialize ? unserialize_if_needed($value) : $value,
array_combine(array_column($result, 'meta_key'), array_column($result, 'meta_value')),
);
}

public function create(int $postId, string $metaKey, mixed $metaValue): bool
{
try {
Expand Down
59 changes: 57 additions & 2 deletions src/Bridge/Repository/TermRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Williarin\WordpressInterop\Bridge\Repository;

use Doctrine\DBAL\Query\QueryBuilder;
use Williarin\WordpressInterop\Bridge\Entity\BaseEntity;
use Williarin\WordpressInterop\Bridge\Entity\Term;
use Williarin\WordpressInterop\Criteria\SelectColumns;

Expand Down Expand Up @@ -34,20 +35,74 @@ public function createFindByQueryBuilder(array $criteria, ?array $orderBy): Quer
;

if (\count(array_filter($criteria, static fn ($condition) => $condition instanceof SelectColumns)) === 0) {
$queryBuilder->select($this->getPrefixedFields(['term_id', 'name', 'slug', 'taxonomy', 'count']));
$extraFields = array_diff(
$queryBuilder->getQueryPart('select'),
$this->getPrefixedFields(['term_id', 'name', 'slug', 'taxonomy', 'count']),
);

$queryBuilder->select([
...$this->getPrefixedFields(['term_id', 'name', 'slug', 'taxonomy', 'count']),
...$extraFields,
]);
} else {
foreach ($this->getPrefixedFields($queryBuilder->getQueryPart('select')) as $field) {
if (!\in_array($field, $queryBuilder->getQueryPart('groupBy'), true)) {
$queryBuilder->addGroupBy($field);
}
}
}

return $queryBuilder;
}

public function addTermsToEntity(BaseEntity $entity, array $terms): void
{
foreach ($terms as $term) {
if (!property_exists($term, 'termTaxonomyId')) {
continue;
}

$this->entityManager->getConnection()
->createQueryBuilder()
->insert($this->entityManager->getTablesPrefix() . 'term_relationships')
->values([
'object_id' => '?',
'term_taxonomy_id' => '?',
'term_order' => '0',
])
->setParameters([$entity->id, (int) $term->termTaxonomyId])
->executeStatement()
;
}

// Recount terms
$subSelect = $this->entityManager->getConnection()
->createQueryBuilder()
->select('COUNT(*)')
->from($this->entityManager->getTablesPrefix() . 'term_relationships', 'tr')
->leftJoin('tr', $this->entityManager->getTablesPrefix() . 'posts', 'p', 'p.id = tr.object_id')
->where('tr.term_taxonomy_id = tt.term_taxonomy_id')
->andWhere("tt.taxonomy NOT IN ('link_category')")
->andWhere("p.post_status IN ('publish', 'future')")
;

$this->entityManager->getConnection()
->createQueryBuilder()
->update($this->entityManager->getTablesPrefix() . 'term_taxonomy', 'tt')
->set('count', sprintf('(%s)', $subSelect->getSQL()))
->executeStatement()
;
}

private function getPrefixedFields(array $fields): array
{
$output = [];

foreach ($fields as $field) {
$output[] = match ($field) {
'term_id', 'name', 'slug' => sprintf('p.%s', $field),
'taxonomy', 'count' => sprintf('tt.%s', $field),
'taxonomy', 'count', 'term_taxonomy_id' => sprintf('tt.%s', $field),
default => $field,
};
}

Expand Down
Loading

0 comments on commit 020d04f

Please sign in to comment.