Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
.gitignore export-ignore
/test export-ignore
/phpunit.xml.dist export-ignore
/phpcs.xml.dist export-ignore
4 changes: 2 additions & 2 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ jobs:
php-versions: ['8.3', '8.4']
name: PHP ${{ matrix.php-versions }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
- name: Get composer cache directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
Expand Down
74 changes: 32 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ This component lets you automatically store revisions for a set of entities per

Requirements
------------
The revision component requires a minimum of php 7.3 and runs on Doctrine2. For specific requirements, please check [composer.json](../master/composer.json).
The revision component requires a minimum of php 8.3 and runs on Doctrine2. For specific requirements, please check [composer.json](../master/composer.json).

Installation
------------
Expand All @@ -33,7 +33,7 @@ Installing is pretty easy, this package is available on [packagist](https://pack

```javascript
"require" : {
"hostnet/entity-revision-component" : "1.*"
"hostnet/entity-revision-component" : "2.*"
}

```
Expand All @@ -46,15 +46,15 @@ Documentation
How does it work?
-----------------

It works by putting the `@Revision` annotation and RevisionableInterface on your Entity and registering the listener on the entityChanged event, assuming you have already configured the [Entity Tracker Component](https://github.com/hostnet/entity-tracker-component/#setup).
It works by putting the `#[Revision]` attribute and RevisionableInterface on your Entity and registering the listener on the entityChanged event, assuming you have already configured the [Entity Tracker Component](https://github.com/hostnet/entity-tracker-component/#setup).

For a usage example, follow the setup below.

> Note: this component works very well with the [Entity Mutation Component](https://github.com/hostnet/entity-mutation-component). This combination is ideal to go back in time based on revisions.

Setup
-----
- You have to add `@Revision` to your entity
- You have to add `#[Revision]` to your entity
- You have to add the RevisionableInterface to your entity
- You need a Revision Entity

Expand All @@ -72,29 +72,26 @@ It might look a bit complicated to set up, but it's pretty much setting up the t
use Acme\Bundle\AcmeBundle\AcmeRevisionFactory;
use Hostnet\Component\EntityRevision\Resolver\RevisionResolver;
use Hostnet\Component\EntityTracker\Listener\EntityChangedListener;
use Hostnet\Component\EntityTracker\Provider\EntityAnnotationMetadataProvider;
use Hostnet\Component\EntityTracker\Provider\EntityMetadataProvider;
use Hostnet\Component\EntityTracker\Provider\EntityMutationMetadataProvider;

/* @var $em \Doctrine\ORM\EntityManager */
$event_manager = $em->getEventManager();

// default doctrine annotation reader
$annotation_reader = new AnnotationReader();

// setup required providers
$mutation_metadata_provider = new EntityMutationMetadataProvider($annotation_reader);
$annotation_metadata_provider = new EntityAnnotationMetadataProvider($annotation_reader);
$mutation_metadata_provider = new EntityMutationMetadataProvider();
$metadata_provider = new EntityMetadataProvider();
$logger = ...; // instance of LoggerInterface from the psr/log package, optional argument
// pre flush event listener that uses the @Revision annotation

// pre flush event listener that uses the #[Tracked]-derived attributes, such as #[Revision]
$entity_changed_listener = new EntityChangedListener(
$metadata_provider,
$mutation_metadata_provider,
$annotation_metadata_provider,
$logger
);

// the resolver is used to find the correct annotation
$revision_resolver = new RevisionResolver($annotation_metadata_provider);
// the resolver is used to find the correct attribute
$revision_resolver = new RevisionResolver($metadata_provider);

// this factory will provide the revision and as author Henk
$revision_factory = new AcmeRevisionFactory('Henk');
Expand All @@ -118,22 +115,15 @@ Revisions are stored in the database, thus need an Entity. You're free to decide
use Doctrine\ORM\Mapping as ORM;
use Hostnet\Component\EntityRevision\RevisionInterface;

/**
* @ORM\Entity
*/
#[ORM\Entity]
class Revision implements RevisionInterface
{

/**
* @ORM\...
*/
#[ORM\...]
private $author;

/**
* @ORM\...
*/

#[ORM\...]
private $created_at;

public function __construct($author, \DateTime $created_at)
{
$this->author = $author;
Expand All @@ -155,34 +145,34 @@ class Revision implements RevisionInterface


#### Configuring the Entity
All we have to do now is put the `@Revision` annotation and RevisionableInterface on our Entity.
All we have to do now is put the `#[Revision]` attribute and RevisionableInterface on our Entity.

```php

use Doctrine\ORM\Mapping as ORM;
use Hostnet\Component\EntityRevision\Attributes\Revision as RevisionAttribute;
use Hostnet\Component\EntityRevision\RevisionableInterface;
use Hostnet\Component\EntityRevision\RevisionInterface;

/**
* @ORM\Entity
* @Revision
*/
#[ORM\Entity]
#[RevisionAttribute]
class MyEntity implements RevisionableInterface
{
/**
* The current revision of the contact person
*
* @ORM\ManyToOne(targetEntity="Revision")
* @ORM\JoinColumn(name="revision_id", referencedColumnName="id")
* @var Revision
*/
private $revision;

public function setRevision(RevisionInterface $revision)
#[ORM\ManyToOne(targetEntity: Revision::class)]
#[ORM\JoinColumn(name: 'revision_id', referencedColumnName: 'id')]
private ?RevisionInterface $revision = null;

public function setRevision(RevisionInterface $revision): static
{
$this->revision = $revision;

return $this;
}
public function getRevision()

public function getRevision(): RevisionInterface
{
return $this->revision;
}
Expand Down Expand Up @@ -217,7 +207,7 @@ class AcmeRevisionFactory implements RevisionFactoryInterface
```

#### What's next?
All you have to do now is change multiple entities with `@Revision` and they will be grouped by a Revision Entity.
All you have to do now is change multiple entities with `#[Revision]` and they will be grouped by a Revision Entity.
```php
// imagine all instances to be Revisionable
$user->setName('Henk');
Expand Down
22 changes: 22 additions & 0 deletions UPGRADE-2.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
UPGRADE FROM 1.x to 2.0
===============================

### General Changes

- Dropped support for `doctrine/annotations`. Entities must use the `#[Revision]`
attribute instead of the `@Revision` annotation.

- Removed `RevisionListener::preFlush()`. This method has been deprecated since 1.1
and its functionality already lives in `RevisionListener::entityChanged()` and
`RevisionListener::postFlush()`. Remove any `preFlush` registration of the
`RevisionListener` from your event manager, if you still have one.

- Added return type hints to `RevisionableInterface`, `RevisionInterface` and
`RevisionFactoryInterface`. Any implementation of these interfaces must add
matching return types:
- `RevisionableInterface::setRevision()`: `static` (now fluent, must `return $this;`)
- `RevisionableInterface::getRevision()`: `RevisionInterface`
- `RevisionInterface::getUser()`: `string`
- `RevisionInterface::getCreatedAt()`: `\DateTimeInterface`
- `RevisionFactoryInterface::createRevision()`: `RevisionInterface` (no longer
nullable)
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"require": {
"php": "^8.3",
"doctrine/orm": "^2.4.0",
"hostnet/entity-tracker-component": "^2.3.0",
"hostnet/entity-tracker-component": "^3.0.0",
"psr/log": "^1.0.0|^2.0.0|^3.0.0",
"symfony/cache": "^6.4|^7.4"
},
Expand Down
7 changes: 3 additions & 4 deletions src/Factory/RevisionFactoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@

namespace Hostnet\Component\EntityRevision\Factory;

use Hostnet\Component\EntityRevision\RevisionInterface;

interface RevisionFactoryInterface
{
/**
* Create a revision
*
* @param \DateTime $created_at
* @return \Hostnet\Component\EntityRevision\RevisionInterface
*/
public function createRevision(\DateTime $created_at);
public function createRevision(\DateTime $created_at): RevisionInterface;
}
45 changes: 8 additions & 37 deletions src/Listener/RevisionListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

namespace Hostnet\Component\EntityRevision\Listener;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Event\PreFlushEventArgs;
use Hostnet\Component\EntityRevision\Factory\RevisionFactoryInterface;
use Hostnet\Component\EntityRevision\Resolver\RevisionResolverInterface;
use Hostnet\Component\EntityRevision\RevisionableInterface;
Expand All @@ -26,32 +26,16 @@ class RevisionListener
public function __construct(
private RevisionResolverInterface $resolver,
private RevisionFactoryInterface $factory,
private ?LoggerInterface $logger = new NullLogger(),
private ?CacheItemPoolInterface $is_revision_cache = new ArrayAdapter()
private LoggerInterface $logger = new NullLogger(),
private CacheItemPoolInterface $is_revision_cache = new ArrayAdapter()
) {
}

/**
* Event is used to create a new revision
*
* Used to group all entities to the same revision
* in the same flush if they use @Revision
*
* @deprecated functionality was moved to entityChanged and postFlush, will be removed when removing
* doctrine/annotations.
* @param PreFlushEventArgs $event
*/
public function preFlush(PreFlushEventArgs $event): void
{
trigger_error(__METHOD__ . ' is deprecated, please remove it from your event listener.', E_USER_DEPRECATED);
$this->revision = $this->factory->createRevision(new \DateTime());
}

/**
* Event is used to remove the previous revision
*
* Used to group all entities to the same revision
* in the same flush if they use @Revision. This method
* in the same flush if they use #[Revision]. This method
* can safely be overwritten if you prefer a Revision
* per Request.
*/
Expand All @@ -60,9 +44,6 @@ public function postFlush(PostFlushEventArgs $event): void
$this->revision = null;
}

/**
* @param EntityChangedEvent $event
*/
public function entityChanged(EntityChangedEvent $event): void
{
if (!$this->shouldBePersisted($event)) {
Expand All @@ -73,22 +54,16 @@ public function entityChanged(EntityChangedEvent $event): void
$this->revision = $this->factory->createRevision(new \DateTime());
}

if (null === $this->revision) {
throw new \RuntimeException('No Revision set for current flush.');
}

$event->getEntityManager()->persist($this->revision);

$entity = $event->getCurrentEntity();
$entity->setRevision($this->revision);

$this->logger->info(sprintf('Added revision for entity', ['entity' => get_class($entity)]));
$this->logger->info('Added revision for entity {entity}', ['entity' => $entity::class]);
}

/**
* Checks if the current entity is eligable for a revision.
*
* @param EntityChangedEvent $event
*/
private function shouldBePersisted(EntityChangedEvent $event): bool
{
Expand All @@ -102,16 +77,16 @@ private function shouldBePersisted(EntityChangedEvent $event): bool
$fields = $this->resolver->getRevisionableFields($em, $entity);

// only create a revision if the mutated fields are tracked
if (count(array_intersect($fields, $event->getMutatedFields())) == 0) {
if (count(array_intersect($fields, $event->getMutatedFields())) === 0) {
return false;
}

return true;
}

private function isRevision($em, $entity): bool
private function isRevision(EntityManagerInterface $em, object $entity): bool
{
$cache_key = base64_encode('REVISION-' . get_class($entity));
$cache_key = base64_encode('REVISION-' . $entity::class);
$cached_item = $this->is_revision_cache->getItem($cache_key);

if ($cached_item->isHit()) {
Expand All @@ -126,10 +101,6 @@ private function isRevision($em, $entity): bool
return $this->save($cached_item, true);
}

if (null !== $this->resolver->getRevisionAnnotation($em, $entity)) {
return $this->save($cached_item, true);
}

return $this->save($cached_item, false);
}

Expand Down
22 changes: 5 additions & 17 deletions src/Resolver/RevisionResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

use Doctrine\ORM\EntityManagerInterface;
use Hostnet\Component\EntityRevision\Attributes\Revision;
use Hostnet\Component\EntityRevision\Revision as RevisionAnnotation;
use Hostnet\Component\EntityTracker\Provider\EntityMetadataProvider;

class RevisionResolver implements RevisionResolverInterface
Expand All @@ -17,27 +16,16 @@ public function __construct(private EntityMetadataProvider $provider)
{
}

/**
* @see \Hostnet\Component\EntityRevision\Resolver\RevisionResolverInterface::getRevisionAnnotation()
*
* @deprecated Please use the Revision attribute instead
*/
public function getRevisionAnnotation(EntityManagerInterface $em, $entity): ?RevisionAnnotation
{
return $this->provider->getAnnotationFromEntity($em, $entity, RevisionAnnotation::class);
}

public function getRevisionAttribute(EntityManagerInterface $em, $entity): ?Revision
#[\Override]
public function getRevisionAttribute(EntityManagerInterface $em, object $entity): ?Revision
{
return $this->provider->getAttributeFromEntity(Revision::class, $em, $entity);
}

/**
* @see \Hostnet\Component\EntityRevision\Resolver\RevisionResolverInterface::getRevisionableFields()
*/
public function getRevisionableFields(EntityManagerInterface $em, $entity): array
#[\Override]
public function getRevisionableFields(EntityManagerInterface $em, object $entity): array
{
$metadata = $em->getClassMetadata(get_class($entity));
$metadata = $em->getClassMetadata($entity::class);
return array_merge($metadata->getFieldNames(), $metadata->getAssociationNames());
}
}
Loading