Actualización

This commit is contained in:
Xes
2025-04-10 12:24:57 +02:00
parent 8969cc929d
commit 45420b6f0d
39760 changed files with 4303286 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
/example export-ignore
/tests export-ignore

View File

@@ -0,0 +1,8 @@
tests/phpunit.xml
tests/temp/*.php
tests/temp/*.log
/vendor
/bin
/composer.lock
/composer.phar
.idea

View File

@@ -0,0 +1,43 @@
dist: trusty
language: php
sudo: false
matrix:
include:
- php: '5.4'
env: phpunit_exclude_groups=datetimeinterface
- php: '5.5'
- php: '5.6'
- php: '7.0'
- php: '7.1'
- php: '7.2'
- php: '7.3'
- php: '7.4'
cache:
directories:
- $HOME/.composer/cache
services: mongodb
before_install:
- if [[ "$TRAVIS_PHP_VERSION" = 5.* ]]; then echo 'extension=mongo.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini; fi
- if [[ "$TRAVIS_PHP_VERSION" != 5.* ]]; then echo 'extension=mongodb.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini; fi
- if [[ "$TRAVIS_PHP_VERSION" != 5.* ]]; then cp composer7.json composer.json; fi
install:
- composer install --prefer-dist
script:
- |
if [[ ! $phpunit_exclude_groups ]]; then
bin/phpunit -c tests/
else
bin/phpunit -c tests/ --exclude-group $phpunit_exclude_groups
fi
notifications:
email:
- gediminas.morkevicius@gmail.com
- developers@atlantic18.com

View File

@@ -0,0 +1,69 @@
# Doctrine Extensions Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
Each release should include sub-headers for the Extension above the types of
changes, in order to more easily recognize how an Extension has changed in
a release.
```
## [2.4.36] - 2018-07-26
### Sortable
#### Fixed
- Fix issue with add+delete position synchronization (#1932)
```
---
## [Unreleased]
## [2.4.42] - 2020-08-20
### Translatable
#### Fixed
- Allow for both falsy and null-fallback translatable values (#2152)
## [2.4.41] - 2020-05-10
### Sluggable
#### Fixed
- Remove PHPDoc samples as they are interpreted by Annotation Reader (#2120)
## [2.4.40] - 2020-04-27
### SoftDeleteable
#### Fixed
- Invalidate query cache when toggling filter on/off for an entity (#2112)
## [2.4.39] - 2020-01-18
### Tree
### Fixed
- The value of path source property is cast to string type for Materialized Path Tree strategy (#2061)
## [2.4.38] - 2019-11-08
### Global / Shared
#### Fixed
- Add `parent::__construct()` calls to Listeners w/ custom constructors (#2012)
- Add upcoming Doctrine ODM 2.0 to `composer.json` conflicts (#2027)
### Loggable
#### Fixed
- Added missing string casting of `objectId` in `LogEntryRepository::revert()` method (#2009)
### ReferenceIntegrity
#### Fixed
- Get class from meta in ReferenceIntegrityListener (#2021)
### Translatable
#### Fixed
- Return default AST executor instead of throwing Exception in Walker (#2018)
- Fix duplicate inherited properties (#2029)
### Tree
#### Fixed
- Remove hard-coded parent column name in repository prev/next sibling queries (#2020)
## [2.4.37] - 2019-03-17
### Translatable
#### Fixed
- Bugfix to load null value translations (#1990)

View File

@@ -0,0 +1,38 @@
# Contributing to Doctrine Extensions
Thank you for your interest in contributing to Doctrine Extensions!
## Which Branch Should I Contribute To?
All pull requests should target the `master` branch, with a planned 3.0 major release.
:warning: The `v.2.4.x` branch has been marked as legacy/deprecated.
## Pull Request Titles
Please include the name(s) of the related extensions as a "tag" in the
pull request title.
> [Tree] Add a new Oak Tree branching style
## Changelog
All updates must include an entry in the [Changelog](/changelog.md).
Put your entry in the `[Unreleased]` section at the top, under the
corresponding Extension and Category.
If there is a related GitHub issue, add it as a suffix to your change.
```
## [Unreleased]
### Loggable
#### Fixed
- Allow emoji in the docs (#123)
```
## What You Can Contribute
Want to contribute but aren't sure where to start? Check out our
[Issue Board](https://github.com/Atlantic18/DoctrineExtensions/issues)!
There are lots of opportunities for helping other users with their issue,
or contributing a reported bug fix or feature request.

View File

@@ -0,0 +1,21 @@
Copyright (c) 2011-2015 Gediminas Morkevičius
The MIT license, reference http://www.opensource.org/licenses/mit-license.php
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,110 @@
# Doctrine Behavioral Extensions
[![Build Status](https://travis-ci.org/Atlantic18/DoctrineExtensions.svg?branch=v2.4.x)](https://travis-ci.org/Atlantic18/DoctrineExtensions)
[![Latest Stable Version](https://poser.pugx.org/gedmo/doctrine-extensions/version)](https://packagist.org/packages/gedmo/doctrine-extensions)
This package contains extensions for Doctrine ORM and MongoDB ODM that offer new functionality or tools to use Doctrine
more efficiently. These behaviors can be easily attached to the event system of Doctrine and handle the records being
flushed in a behavioral way.
## Installation
$ composer require gedmo/doctrine-extensions
* [Symfony 2](/doc/symfony2.md)
* [Symfony 4](/doc/symfony4.md)
* [Laravel 5](https://www.laraveldoctrine.org/docs/1.3/extensions)
* [Zend Framework 2](/doc/zendframework2.md)
## Extensions
#### ORM & MongoDB ODM
- [**Blameable**](/doc/blameable.md) - updates string or reference fields on create, update and even property change with a string or object (e.g. user).
- [**Loggable**](/doc/loggable.md) - helps tracking changes and history of objects, also supports version management.
- [**Sluggable**](/doc/sluggable.md) - urlizes your specified fields into single unique slug
- [**Timestampable**](/doc/timestampable.md) - updates date fields on create, update and even property change.
- [**Translatable**](/doc/translatable.md) - gives you a very handy solution for translating records into different languages. Easy to setup, easier to use.
- [**Tree**](/doc/tree.md) - automates the tree handling process and adds some tree-specific functions on repository.
(**closure**, **nested set** or **materialized path**)
_(MongoDB ODM only supports materialized path)_
#### ORM Only
- [**IpTraceable**](/doc/ip_traceable.md) - inherited from Timestampable, sets IP address instead of timestamp
- [**SoftDeleteable**](/doc/softdeleteable.md) - allows to implicitly remove records
- [**Sortable**](/doc/sortable.md) - makes any document or entity sortable
- [**Uploadable**](/doc/uploadable.md) - provides file upload handling in entity fields
#### MongoDB ODM Only
- [**References**](/doc/references.md) - supports linking Entities in Documents and vice versa
- [**ReferenceIntegrity**](/doc/reference_integrity.md) - constrains ODM MongoDB Document references
All extensions support **YAML**, **Annotation** and **XML** mapping. Additional mapping drivers
can be easily implemented using Mapping extension to handle the additional metadata mapping.
### Version Compatibility
| Extensions Version | Compatible Doctrine ORM & Common Library |
| --- | --- |
| 2.4 | 2.5+ |
| 2.3 | 2.2 - 2.4 |
If you are setting up the Entity Manager without a framework, see the [the example](/example/em.php) to prevent issues like #1310
### XML Mapping
XML mapping needs to be in a different namespace, the declared namespace for
Doctrine extensions is http://gediminasm.org/schemas/orm/doctrine-extensions-mapping
So root node now looks like this:
```xml
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">
...
</doctrine-mapping>
```
XML mapping xsd schemas are also versioned and can be used by version suffix:
- Latest version - **http://gediminasm.org/schemas/orm/doctrine-extensions-mapping**
- 2.2.x version - **http://gediminasm.org/schemas/orm/doctrine-extensions-mapping-2-2**
- 2.1.x version - **http://gediminasm.org/schemas/orm/doctrine-extensions-mapping-2-1**
### Running Tests
**pdo-sqlite** extension is necessary.
To set up and run the tests, follow these steps:
- go to the root directory of extensions
- download composer: `wget https://getcomposer.org/composer.phar`
- install dev libraries: `php composer.phar install`
- run: `bin/phpunit -c tests`
- optional - run mongodb service if targeting mongo tests
### Running the Example
To set up and run example, follow these steps:
- go to the root directory of extensions
- download composer: `wget https://getcomposer.org/composer.phar`
- install dev libraries: `php composer.phar install`
- edit `example/em.php` and configure your database on top of the file
- run: `./example/bin/console` or `php example/bin/console` for console commands
- run: `./example/bin/console orm:schema-tool:create` to create schema
- run: `php example/run.php` to run example
### Contributors
Thanks to [everyone participating](http://github.com/l3pp4rd/DoctrineExtensions/contributors) in
the development of these great Doctrine extensions!
And especially ones who create and maintain new extensions:
- Lukas Botsch [lbotsch](http://github.com/lbotsch)
- Gustavo Adrian [comfortablynumb](http://github.com/comfortablynumb)
- Boussekeyt Jules [gordonslondon](http://github.com/gordonslondon)
- Kudryashov Konstantin [everzet](http://github.com/everzet)
- David Buchmann [dbu](https://github.com/dbu)

View File

@@ -0,0 +1,74 @@
{
"name": "gedmo/doctrine-extensions",
"type": "library",
"description": "Doctrine2 behavioral extensions",
"keywords": [
"behaviors",
"doctrine2",
"extensions",
"gedmo",
"sluggable",
"loggable",
"translatable",
"tree",
"nestedset",
"sortable",
"timestampable",
"blameable",
"uploadable"
],
"homepage": "http://gediminasm.org/",
"license": "MIT",
"authors": [
{
"name": "Gediminas Morkevicius",
"email": "gediminas.morkevicius@gmail.com"
},
{
"name": "Gustavo Falco",
"email": "comfortablynumb84@gmail.com"
},
{
"name": "David Buchmann",
"email": "david@liip.ch"
}
],
"require": {
"php": ">=5.3.2",
"behat/transliterator": "~1.2",
"doctrine/common": "~2.4"
},
"conflict": {
"doctrine/annotations": "<1.2",
"doctrine/mongodb-odm": ">=2.0"
},
"require-dev": {
"doctrine/common": ">=2.5.0",
"doctrine/mongodb-odm": ">=1.0.2 <2.0",
"doctrine/orm": ">=2.5.0",
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5",
"symfony/yaml": "^2.6 || ^3.0 || ^4.0 || ^5.0"
},
"suggest": {
"doctrine/mongodb-odm": "to use the extensions with the MongoDB ODM",
"doctrine/orm": "to use the extensions with the ORM"
},
"config": {
"bin-dir": "bin",
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-master": "2.4.x-dev"
}
},
"autoload": {
"psr-4": {
"Gedmo\\": "lib/Gedmo"
}
},
"support": {
"email": "gediminas.morkevicius@gmail.com",
"wiki": "https://github.com/Atlantic18/DoctrineExtensions/tree/master/doc"
}
}

View File

@@ -0,0 +1,77 @@
{
"name": "gedmo/doctrine-extensions",
"type": "library",
"description": "Doctrine2 behavioral extensions",
"keywords": [
"behaviors",
"doctrine2",
"extensions",
"gedmo",
"sluggable",
"loggable",
"translatable",
"tree",
"nestedset",
"sortable",
"timestampable",
"blameable",
"uploadable"
],
"homepage": "http://gediminasm.org/",
"license": "MIT",
"authors": [
{
"name": "Gediminas Morkevicius",
"email": "gediminas.morkevicius@gmail.com"
},
{
"name": "Gustavo Falco",
"email": "comfortablynumb84@gmail.com"
},
{
"name": "David Buchmann",
"email": "david@liip.ch"
}
],
"support": {
"email": "gediminas.morkevicius@gmail.com",
"wiki": "https://github.com/Atlantic18/DoctrineExtensions/tree/master/doc"
},
"require": {
"php": ">=5.4",
"behat/transliterator": "~1.2",
"doctrine/common": "~2.4"
},
"provide": {
"ext-mongo": "1.6.12"
},
"require-dev": {
"alcaeus/mongo-php-adapter": "~1.0.4",
"doctrine/mongodb-odm": ">=1.0.2 <2.0",
"doctrine/orm": ">=2.5.0",
"doctrine/common": ">=2.5.0",
"symfony/yaml": "~2.6|~3.0|~4.0",
"phpunit/phpunit": "^5.7|^6.5"
},
"conflict": {
"doctrine/annotations": "<1.2",
"doctrine/mongodb": "<1.3",
"doctrine/mongodb-odm": ">=2.0",
"sebastian/comparator": "<2.0"
},
"suggest": {
"doctrine/mongodb-odm": "to use the extensions with the MongoDB ODM",
"doctrine/orm": "to use the extensions with the ORM"
},
"autoload": {
"psr-4": { "Gedmo\\": "lib/Gedmo" }
},
"config": {
"bin-dir": "bin"
},
"extra": {
"branch-alias": {
"dev-master": "3.0.x-dev"
}
}
}

View File

@@ -0,0 +1,566 @@
# Annotation reference
Bellow you will find all annotation descriptions used in these extensions.
There will be introduction on usage with examples. For more detailed usage
on extensions, refer to their specific documentation.
Content:
- Best [practices](#em-setup) for setting up
- [Tree](#gedmo-tree)
- [Translatable](#gedmo-translatable)
- [Sluggable](#gedmo-sluggable)
- [Timestampable](#gedmo-timestampable)
- [Loggable](#gedmo-loggable)
## Annotation mapping
Starting from **doctrine2.1.x** versions you have to import all used annotations
by an **use** statement, see example bellow:
``` php
namespace MyApp\Entity;
use Gedmo\Mapping\Annotation as Gedmo; // this will be like an alias for Gedmo extensions annotations
use Doctrine\ORM\Mapping\Id; // includes single annotation
use Doctrine\ORM\Mapping as ORM; // alias for doctrine ORM annotations
/**
* @ORM\Entity
* @Gedmo\TranslationEntity(class="something")
*/
class Article
{
/**
* @Id
* @Gedmo\Slug(fields={"title"}, updatable=false, separator="_")
* @ORM\Column(length=32, unique=true)
*/
private $id;
/**
* @Gedmo\Translatable
* @ORM\Column(length=64)
*/
private $title;
/**
* @Gedmo\Timestampable(on="create")
* @ORM\Column(type="datetime")
*/
private $created;
}
```
**Note:** this mapping applies only if you use **doctrine-common** library at version **2.1.x** or higher,
extension library still supports old mapping styles if you manually set the mapping drivers
<a name="em-setup"></a>
## Best practices for setting up with annotations
New annotation reader does not depend on any namespaces, for that reason you can use
single reader instance for whole project. The example bellow shows how to setup the
mapping and listeners:
**Note:** using this repository you can test and check the [example demo configuration](https://github.com/Atlantic18/DoctrineExtensions/blob/master/example/em.php)
``` php
<?php
// WARNING: setup, assumes that autoloaders are set
// globally used cache driver, in production use APC or memcached
$cache = new Doctrine\Common\Cache\ArrayCache;
// standard annotation reader
$annotationReader = new Doctrine\Common\Annotations\AnnotationReader;
$cachedAnnotationReader = new Doctrine\Common\Annotations\CachedReader(
$annotationReader, // use reader
$cache // and a cache driver
);
// create a driver chain for metadata reading
$driverChain = new Doctrine\ORM\Mapping\Driver\DriverChain();
// load superclass metadata mapping only, into driver chain
// also registers Gedmo annotations.NOTE: you can personalize it
Gedmo\DoctrineExtensions::registerAbstractMappingIntoDriverChainORM(
$driverChain, // our metadata driver chain, to hook into
$cachedAnnotationReader // our cached annotation reader
);
// now we want to register our application entities,
// for that we need another metadata driver used for Entity namespace
$annotationDriver = new Doctrine\ORM\Mapping\Driver\AnnotationDriver(
$cachedAnnotationReader, // our cached annotation reader
array(__DIR__.'/app/Entity') // paths to look in
);
// NOTE: driver for application Entity can be different, Yaml, Xml or whatever
// register annotation driver for our application Entity namespace
$driverChain->addDriver($annotationDriver, 'Entity');
// general ORM configuration
$config = new Doctrine\ORM\Configuration;
$config->setProxyDir(sys_get_temp_dir());
$config->setProxyNamespace('Proxy');
$config->setAutoGenerateProxyClasses(false); // this can be based on production config.
// register metadata driver
$config->setMetadataDriverImpl($driverChain);
// use our already initialized cache driver
$config->setMetadataCacheImpl($cache);
$config->setQueryCacheImpl($cache);
// create event manager and hook preferred extension listeners
$evm = new Doctrine\Common\EventManager();
// gedmo extension listeners, remove which are not used
// sluggable
$sluggableListener = new Gedmo\Sluggable\SluggableListener;
// you should set the used annotation reader to listener, to avoid creating new one for mapping drivers
$sluggableListener->setAnnotationReader($cachedAnnotationReader);
$evm->addEventSubscriber($sluggableListener);
// tree
$treeListener = new Gedmo\Tree\TreeListener;
$treeListener->setAnnotationReader($cachedAnnotationReader);
$evm->addEventSubscriber($treeListener);
// loggable, not used in example
$loggableListener = new Gedmo\Loggable\LoggableListener;
$loggableListener->setAnnotationReader($cachedAnnotationReader);
$evm->addEventSubscriber($loggableListener);
// timestampable
$timestampableListener = new Gedmo\Timestampable\TimestampableListener;
$timestampableListener->setAnnotationReader($cachedAnnotationReader);
$evm->addEventSubscriber($timestampableListener);
// translatable
$translatableListener = new Gedmo\Translatable\TranslatableListener;
// current translation locale should be set from session or hook later into the listener
// most important, before entity manager is flushed
$translatableListener->setTranslatableLocale('en');
$translatableListener->setDefaultLocale('en');
$translatableListener->setAnnotationReader($cachedAnnotationReader);
$evm->addEventSubscriber($translatableListener);
// sortable, not used in example
$sortableListener = new Gedmo\Sortable\SortableListener;
$sortableListener->setAnnotationReader($cachedAnnotationReader);
$evm->addEventSubscriber($sortableListener);
// mysql set names UTF-8 if required
$evm->addEventSubscriber(new Doctrine\DBAL\Event\Listeners\MysqlSessionInit());
// DBAL connection
$connection = array(
'driver' => 'pdo_mysql',
'host' => '127.0.0.1',
'dbname' => 'test',
'user' => 'root',
'password' => ''
);
// Finally, create entity manager
$em = Doctrine\ORM\EntityManager::create($connection, $config, $evm);
```
**Note:** that symfony2 StofDoctrineExtensionsBundle does it automatically this
way you will maintain a single instance of annotation reader. It relates only
to doctrine-common-2.1.x branch and newer.
<a name="gedmo-tree"></a>
## Tree annotations
Tree can use different adapters. Currently **Tree** extension supports **NestedSet**
and **Closure** strategies which has a difference for annotations used. Note, that
tree will automatically map indexes which are considered necessary for best performance.
### @Gedmo\Mapping\Annotation\Tree (required for all tree strategies)
**class** annotation
Is the main identificator of tree used for domain object which should **act as Tree**.
**options:**
- **type** - (string) _optional_ default: **nested**
example:
``` php
<?php
/**
* @Gedmo\Mapping\Annotation\Tree(type="nested")
* @Doctrine\ORM\Mapping\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository")
*/
class Category ...
```
### @Gedmo\Mapping\Annotation\TreeParent (required for all tree strategies)
**property** annotation
This annotation forces to specify the **parent** field, which must be a **ManyToOne**
relation
example:
``` php
<?php
/**
* @Gedmo\Mapping\Annotation\TreeParent
* @Doctrine\ORM\Mapping\ManyToOne(targetEntity="Category")
* @Doctrine\ORM\Mapping\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE")
*/
private $parent;
```
### @Gedmo\Mapping\Annotation\TreeLeft (required for nested tree)
**property** annotation
This annotation forces to specify the **left** field, which will be used for generation
of nestedset left values. Property must be **integer** type.
example:
``` php
<?php
/**
* @Gedmo\Mapping\Annotation\TreeLeft
* @Doctrine\ORM\Mapping\Column(type=integer)
*/
private $lft;
```
### @Gedmo\Mapping\Annotation\TreeRight (required for nested tree)
**property** annotation
This annotation forces to specify the **right** field, which will be used for generation
of nestedset right values. Property must be **integer** type.
example:
``` php
<?php
/**
* @Gedmo\Mapping\Annotation\TreeRight
* @Doctrine\ORM\Mapping\Column(type=integer)
*/
private $rgt;
```
### @Gedmo\Mapping\Annotation\TreeRoot (optional for nested tree)
**property** annotation
This annotation will use **integer** type field to specify the root of tree. This way
updating tree will cost less because each root will act as separate tree.
example:
``` php
<?php
/**
* @Gedmo\Mapping\Annotation\TreeRoot
* @Doctrine\ORM\Mapping\Column(type=integer, nullable=true)
*/
private $root;
```
### @Gedmo\Mapping\Annotation\TreeLevel (optional for nested tree)
**property** annotation
This annotation lets to store the **level** of each node in the tree, in other word it
is depth. Can be used for indentation for instance. Property must be **integer** type.
example:
``` php
<?php
/**
* @Gedmo\Mapping\Annotation\TreeLevel
* @Doctrine\ORM\Mapping\Column(type=integer)
*/
private $lvl;
```
### @Gedmo\Mapping\Annotation\TreeClosure (required for closure tree)
**class** annotation
This annotation forces to specify the closure domain object, which must
extend **AbstractClosure** in order to have personal closures.
**options:**
- **class** - (string) _required_
example:
``` php
<?php
/**
* @Gedmo\Mapping\Annotation\Tree(type="closure")
* @Gedmo\Mapping\Annotation\TreeClosure(class="Entity\CategoryClosure")
* @Doctrine\ORM\Mapping\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\ClosureTreeRepository")
*/
class Category ...
```
<a name="gedmo-translatable"></a>
## Translatable annotations
Translatable additionally can have unmapped property, which would override the
locale used by listener.
### @Gedmo\Mapping\Annotation\TranslationEntity (optional)
**class** annotation
This class annotation can force translatable to use **personal Entity** to store
translations. In large tables this can be very handy.
**options:**
- **class** - (string) _required_
example:
``` php
<?php
/**
* @Gedmo\Mapping\Annotation\TranslationEntity(class="Entity\ProductTranslation")
* @Doctrine\ORM\Mapping\Entity
*/
class Product ...
```
### @Gedmo\Mapping\Annotation\Translatable (required in order to translate)
**property** annotation
This annotation simply marks **any type** of field to be tracked and translated into
currently used locale. Locale can be forced through entity or set by **TranslationListener**.
example:
``` php
<?php
/**
* @Gedmo\Mapping\Annotation\Translatable
* @Doctrine\ORM\Mapping\Column(type=text)
*/
private $content;
```
### @Gedmo\Mapping\Annotation\Locale or @Gedmo\Mapping\Annotation\Language (optional)
**unmapped property** annotation
Both annotations will do exactly the same - mark property as one which can override
the locale set by **TranslationListener**. Property must not be mapped, that means
it cannot be stored in database.
example:
``` php
<?php
/**
* @Gedmo\Mapping\Annotation\Locale
*/
private $locale;
```
<a name="gedmo-sluggable"></a>
## Sluggable annotations
Sluggable ensures unique slugs and correct length of the slug. It also uses utf8 to ascii
table map to create correct ascii slugs.
### @Gedmo\Mapping\Annotation\Slug (required)
**property** annotation
It will use this **string** property to store the generated slug.
**options:**
- **fields** - (array) _required_, must at least contain one field
- **updatable** - (boolean) _optional_ default: **true**
- **separator** - (string) _optional_ default: **-**
- **unique** - (boolean) _optional_ default: **true**
- **style** - (string) _optional_ default: **default** lowercase, can be **camel** also
- **handlers** - (array) _optional_ default: empty array, refer to the documentation below, possible elements: **Gedmo\Mapping\Annotation\SlugHandler**
### Slug handlers:
- Gedmo\Sluggable\Handler\TreeSlugHandler - transforms a tree slug into path based, example "food/fruits/apricots/king-apricots"
- Gedmo\Sluggable\Handler\RelativeSlugHandler - takes a relation slug and prefixes the slug, example "singers/michael-jackson"
in order to synchronize updates regarding the relation changes, you will need to hood **InversedRelativeSlugHandler** to the relation mentioned.
- Gedmo\Sluggable\Handler\InversedRelativeSlugHandler - updates prefixed slug for an inversed relation which is mapped by **RelativeSlugHandler**
examples:
``` php
<?php
/**
* @Gedmo\Mapping\Annotation\Slug
* @Doctrine\ORM\Mapping\Column(length=64, unique=true)
*/
private $slug;
```
with TreeSlugHandler
``` php
<?php
/**
* @Gedmo\Mapping\Annotation\Slug(handlers={
* @Gedmo\Mapping\Annotation\SlugHandler(class="Gedmo\Sluggable\Handler\TreeSlugHandler", options={
* @Gedmo\Mapping\Annotation\SlugHandlerOption(name="parentRelationField", value="parent"),
* @Gedmo\Mapping\Annotation\SlugHandlerOption(name="separator", value="/")
* })
* }, separator="-", updatable=true)
* @Doctrine\ORM\Mapping\Column(length=64, unique=true)
*/
private $slug;
```
with **RelativeSlugHandler**:
``` php
<?php
/**
* Person domain object class
*
* @Gedmo\Mapping\Annotation\Slug(handlers={
* @Gedmo\Mapping\Annotation\SlugHandler(class="Gedmo\Sluggable\Handler\RelativeSlugHandler", options={
* @Gedmo\Mapping\Annotation\SlugHandlerOption(name="relationField", value="category"),
* @Gedmo\Mapping\Annotation\SlugHandlerOption(name="relationSlugField", value="slug"),
* @Gedmo\Mapping\Annotation\SlugHandlerOption(name="separator", value="/")
* })
* })
* @Doctrine\ORM\Mapping\Column(length=64, unique=true)
*/
private $slug;
```
if you used **RelativeSlugHandler** - relation object should use **InversedRelativeSlugHandler**:
``` php
<?php
/**
* Category domain object class
*
* @Gedmo\Mapping\Annotation\Slug(handlers={
* @Gedmo\Mapping\Annotation\SlugHandler(class="Gedmo\Sluggable\Handler\InversedRelativeSlugHandler", options={
* @Gedmo\Mapping\Annotation\SlugHandlerOption(name="relationClass", value="App\Entity\Person"),
* @Gedmo\Mapping\Annotation\SlugHandlerOption(name="mappedBy", value="category"),
* @Gedmo\Mapping\Annotation\SlugHandlerOption(name="inverseSlugField", value="slug")
* })
* })
* @Doctrine\ORM\Mapping\Column(length=64, unique=true)
*/
private $slug;
```
<a name="gedmo-timestampable"></a>
## Timestampable annotations
Timestampable will update date fields on create, update or property change. If you set/force
date manually it will not update it.
### @Gedmo\Mapping\Annotation\Timestampable (required)
**property** annotation
Marks a **date, datetime or time** field as timestampable.
**options:**
- **on** - (string) _optional_ default: **update**, other choice is **create** or **change**
- **field** - (string) _conditional_ required only if it triggers on **change**, name of the **field**
or if it is a relation **property.field**
- **value** - (mixed) _conditional_ required only if it triggers on **change**, value of property
which would trigger an update.
example:
``` php
<?php
/**
* @Gedmo\Mapping\Annotation\Timestampable(on="create")
* @Doctrine\ORM\Mapping\Column(type="datetime")
*/
private $created;
/**
* @Gedmo\Mapping\Annotation\Timestampable(on="change", field="status.title", value="Published")
* @Doctrine\ORM\Mapping\Column(type="date")
*/
private $published;
/**
* @Doctrine\ORM\Mapping\ManyToOne(targetEntity="Status")
*/
private $status;
```
<a name="gedmo-loggable"></a>
## Loggable annotations
Loggable is used to log all actions made on annotated object class, it logs insert, update
and remove actions for a username which currently is logged in for instance.
Further more, it stores all **Versioned** property changes in the log which allows
a version management implementation for this object.
### @Gedmo\Mapping\Annotation\Loggable (required)
**class** annotation
This class annotation marks object as being loggable and logs all actions being done to
this class records.
**options:**
- **logEntryClass** - (string) _optional_ personal log storage class
example:
``` php
<?php
/**
* @Gedmo\Mapping\Annotation\Loggable(logEntryClass="Entity\ProductLogEntry")
* @Doctrine\ORM\Mapping\Entity
*/
class Product ...
```
### @Gedmo\Mapping\Annotation\Versioned (optional)
**property** annotation
Tracks the marked property for changes to be logged, can be set to single valued associations
but not for collections. Using these log entries as revisions, objects can be reverted to
a specific version.
example:
``` php
<?php
/**
* @Gedmo\Mapping\Annotation\Versioned
* @Doctrine\ORM\Mapping\Column(type="text")
*/
private $content;
/**
* @Gedmo\Mapping\Annotation\Versioned
* @Doctrine\ORM\Mapping\ManyToOne(targetEntity="Article", inversedBy="comments")
*/
private $article;
```

View File

@@ -0,0 +1,644 @@
# Blameable behavior extension for Doctrine 2
**Blameable** behavior will automate the update of username or user reference fields
on your Entities or Documents. It works through annotations and can update
fields on creation, update, property subset update, or even on specific property value change.
This is very similar to Timestampable but sets a string or user object for a user association.
If you map the blame onto a string field, this extension will try to assign the user name.
If you map the blame onto a association field, this extension will try to assign the user
object to it.
Note that you need to set the user on the BlameableListener (unless you use the
Symfony2 extension which does automatically assign the current security context
user).
Features:
- Automatic predefined user field update on creation, update, property subset update, and even on record property changes
- ORM and ODM support using same listener
- Specific annotations for properties, and no interface required
- Can react to specific property or relation changes to specific value
- Can be nested with other behaviors
- Annotation, Yaml and Xml mapping support for extensions
**Symfony:**
- **Blameable** is available as [Bundle](http://github.com/stof/StofDoctrineExtensionsBundle)
for **Symfony2**, together with all other extensions
This article will cover the basic installation and functionality of **Blameable** behavior
Content:
- [Including](#including-extension) the extension
- Entity [example](#entity-mapping)
- Document [example](#document-mapping)
- [Yaml](#yaml-mapping) mapping example
- [Xml](#xml-mapping) mapping example
- Advanced usage [examples](#advanced-examples)
- Using [Traits](#traits)
<a name="including-extension"></a>
## Setup and autoloading
Read the [documentation](http://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/annotations.md#em-setup)
or check the [example code](http://github.com/Atlantic18/DoctrineExtensions/tree/master/example)
on how to setup and use the extensions in most optimized way.
<a name="entity-mapping"></a>
## Blameable Entity example:
### Blameable annotations:
- **@Gedmo\Mapping\Annotation\Blameable** this annotation tells that this column is blameable
by default it updates this column on update. If column is not a string field or an association
it will trigger an exception.
Available configuration options:
- **on** - is main option and can be **create, update, change** this tells when it
should be updated
- **field** - only valid if **on="change"** is specified, tracks property or a list of properties for changes
- **value** - only valid if **on="change"** is specified and the tracked field is a single field (not an array), if the tracked field has this **value**
then it updates the blame
**Note:** that Blameable interface is not necessary, except in cases there
you need to identify entity as being Blameable. The metadata is loaded only once then
cache is activated
Column is a string field:
``` php
<?php
namespace Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Article
{
/** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */
private $id;
/**
* @ORM\Column(type="string", length=128)
*/
private $title;
/**
* @ORM\Column(name="body", type="string")
*/
private $body;
/**
* @var string $createdBy
*
* @Gedmo\Blameable(on="create")
* @ORM\Column(type="string")
*/
private $createdBy;
/**
* @var string $updatedBy
*
* @Gedmo\Blameable(on="update")
* @ORM\Column(type="string")
*/
private $updatedBy;
/**
* @var string $contentChangedBy
*
* @ORM\Column(name="content_changed_by", type="string", nullable=true)
* @Gedmo\Blameable(on="change", field={"title", "body"})
*/
private $contentChangedBy;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setBody($body)
{
$this->body = $body;
}
public function getBody()
{
return $this->body;
}
public function getCreatedBy()
{
return $this->createdBy;
}
public function getUpdatedBy()
{
return $this->updatedBy;
}
public function getContentChangedBy()
{
return $this->contentChangedBy;
}
}
```
Column is an association:
``` php
<?php
namespace Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Article
{
/** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */
private $id;
/**
* @ORM\Column(type="string", length=128)
*/
private $title;
/**
* @ODM\String
*/
private $body;
/**
* @var User $createdBy
*
* @Gedmo\Blameable(on="create")
* @ORM\ManyToOne(targetEntity="Path\To\Entity\User")
* @ORM\JoinColumn(name="created_by", referencedColumnName="id")
*/
private $createdBy;
/**
* @var User $updatedBy
*
* @Gedmo\Blameable(on="update")
* @ORM\ManyToOne(targetEntity="Path\To\Entity\User")
* @ORM\JoinColumn(name="updated_by", referencedColumnName="id")
*/
private $updatedBy;
/**
* @var User $contentChangedBy
*
* @Gedmo\Blameable(on="change", field={"title", "body"})
* @ORM\ManyToOne(targetEntity="Path\To\Entity\User")
* @ORM\JoinColumn(name="content_changed_by", referencedColumnName="id")
*/
private $contentChangedBy;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setBody($body)
{
$this->body = $body;
}
public function getBody()
{
return $this->body;
}
public function getCreatedBy()
{
return $this->createdBy;
}
public function getUpdatedBy()
{
return $this->updatedBy;
}
public function getContentChangedBy()
{
return $this->contentChangedBy;
}
}
```
<a name="document-mapping"></a>
## Blameable Document example:
``` php
<?php
namespace Document;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
/**
* @ODM\Document(collection="articles")
*/
class Article
{
/** @ODM\Id */
private $id;
/**
* @ODM\Field(type="string")
*/
private $title;
/**
* @var string $createdBy
*
* @ODM\Field(type="string")
* @Gedmo\Blameable(on="create")
*/
private $createdBy;
/**
* @var string $updatedBy
*
* @ODM\Field(type="string")
* @Gedmo\Blameable
*/
private $updatedBy;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function getCreatedBy()
{
return $this->createdBy;
}
public function getUpdatedBy()
{
return $this->updatedBy;
}
}
```
Now on update and creation these annotated fields will be automatically updated
<a name="yaml-mapping"></a>
## Yaml mapping example:
Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml**
```
---
Entity\Article:
type: entity
table: articles
id:
id:
type: integer
generator:
strategy: AUTO
fields:
title:
type: string
length: 64
createdBy:
type: string
gedmo:
blameable:
on: create
updatedBy:
type: string
gedmo:
blameable:
on: update
```
<a name="xml-mapping"></a>
## Xml mapping example
``` xml
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">
<entity name="Mapping\Fixture\Xml\Blameable" table="blameables">
<id name="id" type="integer" column="id">
<generator strategy="AUTO"/>
</id>
<field name="createdBy" type="string">
<gedmo:blameable on="create"/>
</field>
<field name="updatedBy" type="string">
<gedmo:blameable on="update"/>
</field>
<field name="publishedBy" type="string" nullable="true">
<gedmo:blameable on="change" field="status.title" value="Published"/>
</field>
<many-to-one field="status" target-entity="Status">
<join-column name="status_id" referenced-column-name="id"/>
</many-to-one>
</entity>
</doctrine-mapping>
```
<a name="advanced-examples"></a>
## Advanced examples:
### Using dependency of property changes
Add another entity which would represent Article Type:
``` php
<?php
namespace Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Type
{
/** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */
private $id;
/**
* @ORM\Column(type="string", length=128)
*/
private $title;
/**
* @ORM\OneToMany(targetEntity="Article", mappedBy="type")
*/
private $articles;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
}
```
Now update the Article Entity to reflect publishedBy on Type change:
``` php
<?php
namespace Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Article
{
/** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */
private $id;
/**
* @ORM\Column(type="string", length=128)
*/
private $title;
/**
* @var string $createdBy
*
* @Gedmo\Blameable(on="create")
* @ORM\Column(type="string")
*/
private $createdBy;
/**
* @var string $updatedBy
*
* @Gedmo\Blameable(on="update")
* @ORM\Column(type="string")
*/
private $updatedBy;
/**
* @ORM\ManyToOne(targetEntity="Type", inversedBy="articles")
*/
private $type;
/**
* @var string $publishedBy
*
* @ORM\Column(type="string", nullable=true)
* @Gedmo\Blameable(on="change", field="type.title", value="Published")
*/
private $publishedBy;
public function setType($type)
{
$this->type = $type;
}
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function getCreatedBy()
{
return $this->createdBy;
}
public function getUpdatedBy()
{
return $this->updatedBy;
}
public function getPublishedBy()
{
return $this->publishedBy;
}
}
```
Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml**
```
---
Entity\Article:
type: entity
table: articles
id:
id:
type: integer
generator:
strategy: AUTO
fields:
title:
type: string
length: 64
createdBy:
type: string
gedmo:
blameable:
on: create
updatedBy:
type: string
gedmo:
blameable:
on: update
publishedBy:
type: string
gedmo:
blameable:
on: change
field: type.title
value: Published
manyToOne:
type:
targetEntity: Entity\Type
inversedBy: articles
```
Now few operations to get it all done:
``` php
<?php
$article = new Article;
$article->setTitle('My Article');
$em->persist($article);
$em->flush();
// article: $createdBy, $updatedBy were set
$type = new Type;
$type->setTitle('Published');
$article = $em->getRepository('Entity\Article')->findByTitle('My Article');
$article->setType($type);
$em->persist($article);
$em->persist($type);
$em->flush();
// article: $publishedBy, $updatedBy were set
$article->getPublishedBy(); // the user that published this article
```
Easy like that, any suggestions on improvements are very welcome
<a name="traits"></a>
## Traits
You can use blameable traits for quick **createdBy** **updatedBy** string definitions
when using annotation mapping.
There is also a trait without annotations for easy integration purposes.
**Note:** this feature is only available since php **5.4.0**. And you are not required
to use the Traits provided by extensions.
``` php
<?php
namespace Blameable\Fixture;
use Gedmo\Blameable\Traits\BlameableEntity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class UsingTrait
{
/**
* Hook blameable behavior
* updates createdBy, updatedBy fields
*/
use BlameableEntity;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(length=128)
*/
private $title;
}
```
The Traits are very simplistic - if you use different field names it is recommended to simply create your
own Traits specific to your project. The ones provided by this bundle can be used as example.

View File

@@ -0,0 +1,660 @@
# IpTraceable behavior extension for Doctrine 2
**IpTraceable** behavior will automate the update of IP trace
on your Entities or Documents. It works through annotations and can update
fields on creation, update, property subset update, or even on specific property value change.
This is very similar to Timestampable but sets a string.
Note that you need to set the IP on the IpTraceableListener (unless you use the
Symfony2 extension which does automatically assign the current request IP).
Features:
- Automatic predefined ip field update on creation, update, property subset update, and even on record property changes
- ORM and ODM support using same listener
- Specific annotations for properties, and no interface required
- Can react to specific property or relation changes to specific value
- Can be nested with other behaviors
- Annotation, Yaml and Xml mapping support for extensions
**Symfony:**
- **IpTraceable** is not yet available as [Bundle](http://github.com/stof/StofDoctrineExtensionsBundle)
for **Symfony2**, together with all other extensions
This article will cover the basic installation and functionality of **IpTraceable** behavior
Content:
- [Including](#including-extension) the extension
- Entity [example](#entity-mapping)
- Document [example](#document-mapping)
- [Yaml](#yaml-mapping) mapping example
- [Xml](#xml-mapping) mapping example
- Advanced usage [examples](#advanced-examples)
- Using [Traits](#traits)
<a name="including-extension"></a>
## Setup and autoloading
Read the [documentation](http://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/annotations.md#em-setup)
or check the [example code](http://github.com/Atlantic18/DoctrineExtensions/tree/master/example)
on how to setup and use the extensions in most optimized way.
<a name="entity-mapping"></a>
## IpTraceable Entity example:
### IpTraceable annotations:
- **@Gedmo\Mapping\Annotation\IpTraceable** this annotation tells that this column is ipTraceable
by default it updates this column on update. If column is not a string field it will trigger an exception.
Available configuration options:
- **on** - is main option and can be **create, update, change** this tells when it
should be updated
- **field** - only valid if **on="change"** is specified, tracks property or a list of properties for changes
- **value** - only valid if **on="change"** is specified and the tracked field is a single field (not an array), if the tracked field has this **value**
then it updates the trace
**Note:** that IpTraceable interface is not necessary, except in cases there
you need to identify entity as being IpTraceable. The metadata is loaded only once then
cache is activated
Column is a string field:
``` php
<?php
namespace Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Article
{
/** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */
private $id;
/**
* @ORM\Column(type="string", length=128)
*/
private $title;
/**
* @ORM\Column(name="body", type="string")
*/
private $body;
/**
* @var string $createdFromIp
*
* @Gedmo\IpTraceable(on="create")
* @ORM\Column(type="string", length=45, nullable=true)
*/
private $createdFromIp;
/**
* @var string $updatedFromIp
*
* @Gedmo\IpTraceable(on="update")
* @ORM\Column(type="string", length=45, nullable=true)
*/
private $updatedFromIp;
/**
* @var datetime $contentChangedFromIp
*
* @ORM\Column(name="content_changed_by", type="string", nullable=true, length=45)
* @Gedmo\IpTraceable(on="change", field={"title", "body"})
*/
private $contentChangedFromIp;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setBody($body)
{
$this->body = $body;
}
public function getBody()
{
return $this->body;
}
public function getCreatedFromIp()
{
return $this->createdFromIp;
}
public function getUpdatedFromIp()
{
return $this->updatedFromIp;
}
public function getContentChangedFromIp()
{
return $this->contentChangedFromIp;
}
}
```
<a name="document-mapping"></a>
## IpTraceable Document example:
``` php
<?php
namespace Document;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
/**
* @ODM\Document(collection="articles")
*/
class Article
{
/** @ODM\Id */
private $id;
/**
* @ODM\Field(type="string")
*/
private $title;
/**
* @var string $createdFromIp
*
* @ODM\Field(type="string")
* @Gedmo\IpTraceable(on="create")
*/
private $createdFromIp;
/**
* @var string $updatedFromIp
*
* @ODM\Field(type="string")
* @Gedmo\IpTraceable
*/
private $updatedFromIp;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function getCreatedFromIp()
{
return $this->createdFromIp;
}
public function getUpdatedFromIp()
{
return $this->updatedFromIp;
}
}
```
Now on update and creation these annotated fields will be automatically updated
<a name="yaml-mapping"></a>
## Yaml mapping example:
Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml**
```
---
Entity\Article:
type: entity
table: articles
id:
id:
type: integer
generator:
strategy: AUTO
fields:
title:
type: string
length: 64
createdFromIp:
type: string
length: 45
nullable: true
gedmo:
ipTraceable:
on: create
updatedFromIp:
type: string
length: 45
nullable: true
gedmo:
ipTraceable:
on: update
```
<a name="xml-mapping"></a>
## Xml mapping example
``` xml
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">
<entity name="Mapping\Fixture\Xml\IpTraceable" table="ip-traceable">
<id name="id" type="integer" column="id">
<generator strategy="AUTO"/>
</id>
<field name="createdFromIp" type="string", length="45", nullable="true">
<gedmo:ip-traceable on="create"/>
</field>
<field name="updatedFromIp" type="string", length="45", nullable="true">
<gedmo:ip-traceable on="update"/>
</field>
<field name="publishedFromIp" type="string" nullable="true", length="45">
<gedmo:ip-traceable on="change" field="status.title" value="Published"/>
</field>
<many-to-one field="status" target-entity="Status">
<join-column name="status_id" referenced-column-name="id"/>
</many-to-one>
</entity>
</doctrine-mapping>
```
<a name="advanced-examples"></a>
## Advanced examples:
### Using dependency of property changes
Add another entity which would represent Article Type:
``` php
<?php
namespace Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Type
{
/** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */
private $id;
/**
* @ORM\Column(type="string", length=128)
*/
private $title;
/**
* @ORM\OneToMany(targetEntity="Article", mappedBy="type")
*/
private $articles;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
}
```
Now update the Article Entity to reflect publishedFromIp on Type change:
``` php
<?php
namespace Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Article
{
/** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */
private $id;
/**
* @ORM\Column(type="string", length=128)
*/
private $title;
/**
* @var string $createdFromIp
*
* @Gedmo\IpTraceable(on="create")
* @ORM\Column(type="string", length=45, nullable=true)
*/
private $createdFromIp;
/**
* @var string $updatedFromIp
*
* @Gedmo\IpTraceable(on="update")
* @ORM\Column(type="string", length=45, nullable=true)
*/
private $updatedFromIp;
/**
* @ORM\ManyToOne(targetEntity="Type", inversedFromIp="articles")
*/
private $type;
/**
* @var string $publishedFromIp
*
* @ORM\Column(type="string", nullable=true, length=45)
* @Gedmo\IpTraceable(on="change", field="type.title", value="Published")
*/
private $publishedFromIp;
public function setType($type)
{
$this->type = $type;
}
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function getCreatedFromIp()
{
return $this->createdFromIp;
}
public function getUpdatedFromIp()
{
return $this->updatedFromIp;
}
public function getPublishedFromIp()
{
return $this->publishedFromIp;
}
}
```
Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml**
```
---
Entity\Article:
type: entity
table: articles
id:
id:
type: integer
generator:
strategy: AUTO
fields:
title:
type: string
length: 64
createdFromIp:
type: string
length: 45
nullable: true
gedmo:
ipTraceable:
on: create
updatedFromIp:
type: string
length: 45
nullable: true
gedmo:
ipTraceable:
on: update
publishedFromIp:
type: string
length: 45
nullable: true
gedmo:
ipTraceable:
on: change
field: type.title
value: Published
manyToOne:
type:
targetEntity: Entity\Type
inversedBy: articles
```
Now few operations to get it all done:
``` php
<?php
$article = new Article;
$article->setTitle('My Article');
$em->persist($article);
$em->flush();
// article: $createdFromIp, $updatedFromIp were set
$type = new Type;
$type->setTitle('Published');
$article = $em->getRepository('Entity\Article')->findByTitle('My Article');
$article->setType($type);
$em->persist($article);
$em->persist($type);
$em->flush();
// article: $publishedFromIp, $updatedFromIp were set
$article->getPublishedFromIp(); // the IP that published this article
```
Easy like that, any suggestions on improvements are very welcome
<a name="traits"></a>
## Traits
You can use IpTraceable traits for quick **createdFromIp** **updatedFromIp** string definitions
when using annotation mapping.
There is also a trait without annotations for easy integration purposes.
**Note:** this feature is only available since php **5.4.0**. And you are not required
to use the Traits provided by extensions.
``` php
<?php
namespace IpTraceable\Fixture;
use Gedmo\IpTraceable\Traits\IpTraceableEntity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class UsingTrait
{
/**
* Hook ip-traceable behavior
* updates createdFromIp, updatedFromIp fields
*/
use IpTraceableEntity;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(length=128)
*/
private $title;
}
```
The Traits are very simplistic - if you use different field names it is recommended to simply create your
own Traits specific to your project. The ones provided by this bundle can be used as example.
## Example of implementation in Symfony2
In your Sf2 application, declare an event subscriber that automatically set IP value on IpTraceableListener.
### Code of subscriber class
``` php
<?php
namespace Acme\DemoBundle\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpFoundation\Request;
use Gedmo\IpTraceable\IpTraceableListener;
/**
* IpTraceSubscriber
*/
class IpTraceSubscriber implements EventSubscriberInterface
{
/**
* @var Request
*/
private $request;
/**
* @var IpTraceableListener
*/
private $ipTraceableListener;
public function __construct(IpTraceableListener $ipTraceableListener, Request $request = null)
{
$this->ipTraceableListener = $ipTraceableListener;
$this->request = $request;
}
/**
* Set the username from the security context by listening on core.request
*
* @param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event)
{
if (null === $this->request) {
return;
}
// If you use a cache like Varnish, you may want to set a proxy to Request::getClientIp() method
// $this->request->setTrustedProxies(array('127.0.0.1'));
// $ip = $_SERVER['REMOTE_ADDR'];
$ip = $this->request->getClientIp();
if (null !== $ip) {
$this->ipTraceableListener->setIpValue($ip);
}
}
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => 'onKernelRequest',
);
}
}
```
### Configuration for services.xml
``` xml
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="alterphp_doctrine_extensions.event_listener.ip_trace.class">Acme\DemoBundle\EventListener\IpTraceListener</parameter>
</parameters>
<services>
...
<service id="gedmo_doctrine_extensions.listener.ip_traceable" class="Gedmo\IpTraceable\IpTraceableListener" public="false">
<tag name="doctrine.event_subscriber" connection="default" />
<call method="setAnnotationReader">
<argument type="service" id="annotation_reader" />
</call>
</service>
<service id="alterphp_doctrine_extensions.event_listener.ip_trace" class="%alterphp_doctrine_extensions.event_listener.ip_trace.class%" public="false" scope="request">
<argument type="service" id="gedmo_doctrine_extensions.listener.ip_traceable" />
<argument type="service" id="request" on-invalid="null" />
<tag name="kernel.event_subscriber" />
</service>
</services>
</container>
```

View File

@@ -0,0 +1,266 @@
# Loggable behavioral extension for Doctrine2
**Loggable** behavior tracks your record changes and is able to
manage versions.
Features:
- Automatic storage of log entries in database
- ORM and ODM support using same listener
- Can be nested with other behaviors
- Objects can be reverted to previous versions
- Annotation, Yaml and Xml mapping support for extensions
Update **2011-04-04**
- Made single listener, one instance can be used for any object manager
and any number of them
**Portability:**
- **Loggable** is now available as [Bundle](http://github.com/stof/StofDoctrineExtensionsBundle)
ported to **Symfony2** by **Christophe Coevoet**, together with all other extensions
This article will cover the basic installation and functionality of **Loggable**
behavior
Content:
- [Including](#including-extension) the extension
- Entity [example](#entity-mapping)
- Document [example](#document-mapping)
- [Yaml](#yaml-mapping) mapping example
- [Xml](#xml-mapping) mapping example
- Basic usage [examples](#basic-examples)
<a name="including-extension"></a>
## Setup and autoloading
Read the [documentation](http://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/annotations.md#em-setup)
or check the [example code](http://github.com/Atlantic18/DoctrineExtensions/tree/master/example)
on how to setup and use the extensions in most optimized way.
### Loggable annotations:
- **@Gedmo\Mapping\Annotation\Loggable(logEntryClass="my\class")** this class annotation
will store logs to optionally specified **logEntryClass**. You will still need to specify versioned fields with the following annotation.
- **@Gedmo\Mapping\Annotation\Versioned** tracks annotated property for changes
### Loggable username:
In order to set the username, when adding the loggable listener you need to set it this way:
``` php
$loggableListener = new Gedmo\Loggable\LoggableListener;
$loggableListener->setAnnotationReader($cachedAnnotationReader);
$loggableListener->setUsername('admin');
$evm->addEventSubscriber($loggableListener);
```
<a name="entity-mapping"></a>
## Loggable Entity example:
**Note:** that Loggable interface is not necessary, except in cases there
you need to identify entity as being Loggable. The metadata is loaded only once when
cache is active
``` php
<?php
namespace Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @Gedmo\Loggable
*/
class Article
{
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @Gedmo\Versioned
* @ORM\Column(name="title", type="string", length=8)
*/
private $title;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
}
```
<a name="document-mapping"></a>
## Loggable Document example:
``` php
<?php
namespace Document;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
/**
* @ODM\Document(collection="articles")
* @Gedmo\Loggable
*/
class Article
{
/** @ODM\Id */
private $id;
/**
* @ODM\Field(type="string")
* @Gedmo\Versioned
*/
private $title;
public function __toString()
{
return $this->title;
}
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
}
```
<a name="yaml-mapping"></a>
## Yaml mapping example
Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml**
```
---
Entity\Article:
type: entity
table: articles
gedmo:
loggable:
# using specific personal LogEntryClass class:
logEntryClass: My\LogEntry
# without specifying the LogEntryClass class:
# loggable: true
id:
id:
type: integer
generator:
strategy: AUTO
fields:
title:
type: string
length: 64
gedmo:
- versioned
content:
type: text
```
<a name="xml-mapping"></a>
## Xml mapping example
``` xml
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">
<entity name="Mapping\Fixture\Xml\Loggable" table="loggables">
<id name="id" type="integer" column="id">
<generator strategy="AUTO"/>
</id>
<field name="title" type="string" length="128">
<gedmo:versioned/>
</field>
<many-to-one field="status" target-entity="Status">
<join-column name="status_id" referenced-column-name="id"/>
<gedmo:versioned/>
</many-to-one>
<gedmo:loggable log-entry-class="Gedmo\Loggable\Entity\LogEntry"/>
</entity>
</doctrine-mapping>
```
<a name="basic-examples"></a>
## Basic usage examples:
``` php
<?php
$article = new Entity\Article;
$article->setTitle('my title');
$em->persist($article);
$em->flush();
```
This inserted an article and inserted the logEntry for it, which contains
all new changeset. In case if there is **OneToOne or ManyToOne** relation,
it will store only identifier of that object to avoid storing proxies
Now lets update our article:
``` php
<?php
// first load the article
$article = $em->find('Entity\Article', 1 /*article id*/);
$article->setTitle('my new title');
$em->persist($article);
$em->flush();
```
This updated an article and inserted the logEntry for update action with new changeset
Now lets revert it to previous version:
``` php
<?php
// first check our log entries
$repo = $em->getRepository('Gedmo\Loggable\Entity\LogEntry'); // we use default log entry class
$article = $em->find('Entity\Article', 1 /*article id*/);
$logs = $repo->getLogEntries($article);
/* $logs contains 2 logEntries */
// lets revert to first version
$repo->revert($article, 1/*version*/);
// notice article is not persisted yet, you need to persist and flush it
echo $article->getTitle(); // prints "my title"
$em->persist($article);
$em->flush();
// if article had changed relation, it would be reverted also.
```
Easy like that, any suggestions on improvements are very welcome

View File

@@ -0,0 +1,491 @@
# Mapping extension for Doctrine2
**Mapping** extension makes it easy to map additional metadata for event listeners.
It supports **Yaml**, **Xml** and **Annotation** drivers which will be chosen depending on
currently used mapping driver for your domain objects. **Mapping** extension also
provides abstraction layer of **EventArgs** to make it possible to use single listener
for different object managers like **ODM** and **ORM**.
Features:
- Mapping drivers for annotation and yaml
- Conventional extension points for metadata extraction and object manager abstraction
- Public [Mapping repository](http://github.com/Atlantic18/DoctrineExtensions "Mapping extension on Github") is available on github
- Last update date: **2012-01-02**
This article will cover the basic installation and usage of **Mapping** extension
Content:
- [Including](#including-extension) the extension
- [Creating](#create-extension) an extension
- Defining [annotations](#annotations)
- Creating [listener](#create-listener)
- Attaching our [listener](#attach-listener) to the event manager
- [Entity](#entity-mapping) with some fields to encode
- Adapting listener to support [different](#different-managers) object managers
- [Customizing](#event-adapter-customize) event adapter for specific functions
<a name="including-extension"></a>
## Setup and autoloading
Read the [documentation](http://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/annotations.md#em-setup)
or check the [example code](http://github.com/Atlantic18/DoctrineExtensions/tree/master/example)
on how to setup and use the extensions in most optimized way.
<a name="create-extension"></a>
## Tutorial on creation of mapped extension
First, lets assume we will use **Extension** namespace for our additional
extension library. You should create an **Extension** directory in your library
or vendor directory. After some changes your project might look like:
```
project
...
bootstrap.php
vendor
Extension
...
...
```
Now you can use any namespace autoloader class and register this namespace. We
will use Doctrine\Common\ClassLoader for instance:
``` php
<?php
// path is related to bootstrap.php location for example
$classLoader = new \Doctrine\Common\ClassLoader('Extension', "vendor");
$classLoader->register();
```
Now lets create some files which are necessary for our extension:
```
project
...
bootstrap.php
vendor
Extension
Encoder
Mapping
Driver
Annotation.php
Annotations.php
EncoderListener.php
...
```
**Note:** that extension will look for mapping in **ExtensionNamespace/Mapping**
directory. And **Driver** directory should be named as Driver. These are the conventions
of **Mapping** extension.
That is all we will need for now. As you may noticed we will create an encoding
listener which could encode your fields by specified annotations. In real life it
may not be useful since object will not know how to match the value.
<a name="annotations"></a>
## Now lets define available annotations and setup drivers
Edit **Annotations.php** file:
``` php
<?php
// file: vendor/Extension/Encoder/Mapping/Annotations.php
namespace Extension\Encoder\Mapping;
use Doctrine\Common\Annotations\Annotation;
final class Encode extends Annotation
{
public $type = 'md5';
public $secret;
}
```
Edit **Annotation.php** driver file:
``` php
<?php
// file: vendor/Extension/Encoder/Mapping/Driver/Annotation.php
namespace Extension\Encoder\Mapping\Driver;
use Gedmo\Mapping\Driver;
use Doctrine\Common\Annotations\AnnotationReader;
class Annotation implements Driver
{
public function readExtendedMetadata($meta, array &$config) {
// load our available annotations
require_once __DIR__ . '/../Annotations.php';
$reader = new AnnotationReader();
$class = $meta->getReflectionClass();
// check only property annotations
foreach ($class->getProperties() as $property) {
// skip inherited properties
if ($meta->isMappedSuperclass && !$property->isPrivate() ||
$meta->isInheritedField($property->name) ||
isset($meta->associationMappings[$property->name]['inherited'])
) {
continue;
}
// now lets check if property has our annotation
if ($encode = $reader->getPropertyAnnotation($property, 'Extension\Encoder\Mapping\Encode')) {
$field = $property->getName();
// check if field is mapped
if (!$meta->hasField($field)) {
throw new \Exception("Field is not mapped as object property");
}
// allow encoding only strings
if (!in_array($encode->type, array('sha1', 'md5'))) {
throw new \Exception("Invalid encoding type supplied");
}
// validate encoding type
$mapping = $meta->getFieldMapping($field);
if ($mapping['type'] != 'string') {
throw new \Exception("Only strings can be encoded");
}
// store the metadata
$config['encode'][$field] = array(
'type' => $encode->type,
'secret' => $encode->secret
);
}
}
}
}
```
<a name="create-listener"></a>
## Finally, lets create the listener
**Note:** this version of listener will support only ORM Entities
``` php
<?php
// file: vendor/Extension/Encoder/EncoderListener.php
namespace Extension\Encoder;
use Doctrine\Common\EventArgs;
use Gedmo\Mapping\MappedEventSubscriber;
class EncoderListener extends MappedEventSubscriber
{
public function getSubscribedEvents()
{
return array(
'onFlush',
'loadClassMetadata'
);
}
public function loadClassMetadata(EventArgs $args)
{
// this will check for our metadata
$this->loadMetadataForObjectClass(
$args->getEntityManager(),
$args->getClassMetadata()
);
}
public function onFlush(EventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
// check all pending updates
foreach ($uow->getScheduledEntityUpdates() as $object) {
$meta = $em->getClassMetadata(get_class($object));
// if it has our metadata lets encode the properties
if ($config = $this->getConfiguration($em, $meta->name)) {
$this->encode($em, $object, $config);
}
}
// check all pending insertions
foreach ($uow->getScheduledEntityInsertions() as $object) {
$meta = $em->getClassMetadata(get_class($object));
// if it has our metadata lets encode the properties
if ($config = $this->getConfiguration($em, $meta->name)) {
$this->encode($em, $object, $config);
}
// recalculate changeset
$em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $object);
}
}
protected function getNamespace()
{
// mapper must know the namespace of extension
return __NAMESPACE__;
}
private function encode($em, $object, $config)
{
$meta = $em->getClassMetadata(get_class($object));
foreach ($config['encode'] as $field => $options) {
$value = $meta->getReflectionProperty($field)->getValue($object);
$method = $options['type'];
$encoded = $method($options['secret'].$value);
$meta->getReflectionProperty($field)->setValue($object, $encoded);
}
// recalculate changeset
$em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $object);
}
}
```
Our **Encoder** extension is ready, now if we want to test it, we need
to attach our **EncoderListener** to the EventManager and create an entity
with some fields to encode.
<a name="attach-listener"></a>
### Attaching the EncoderListener
``` php
<?php
$evm = new \Doctrine\Common\EventManager();
$encoderListener = new \Extension\Encoder\EncoderListener;
$evm->addEventSubscriber($encoderListener);
// now this event manager should be passed to entity manager constructor
```
<a name="entity-mapping"></a>
### Create an entity with some fields to encode
``` php
<?php
namespace YourNamespace\Entity;
use Doctrine\ORM\Mapping as ORM;
use Extension\Encoder\Mapping as EXT;
/**
* @ORM\Table(name="test_users")
* @ORM\Entity
*/
class User
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*/
private $id;
/**
* @EXT\Encode(type="sha1", secret="xxx")
* @ORM\Column(length=64)
*/
private $name;
/**
* @EXT\Encode(type="md5")
* @ORM\Column(length=32)
*/
private $password;
public function setName($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
public function setPassword($password)
{
$this->password = $password;
}
public function getPassword()
{
return $this->password;
}
}
```
If you will try to create a new **User** you will get encoded fields in database.
<a name="different-managers"></a>
## Adapting listener to support other object managers
Now the event adapter comes into play, lets slightly modify our listener:
``` php
<?php
// file: vendor/Extension/Encoder/EncoderListener.php
use Doctrine\Common\EventArgs;
use Gedmo\Mapping\MappedEventSubscriber;
use Gedmo\Mapping\Event\AdapterInterface as EventAdapterInterface;
class EncoderListener extends MappedEventSubscriber
{
public function getSubscribedEvents()
{
return array(
'onFlush',
'loadClassMetadata'
);
}
public function loadClassMetadata(EventArgs $args)
{
$ea = $this->getEventAdapter($args);
// this will check for our metadata
$this->loadMetadataForObjectClass(
$ea->getObjectManager(),
$args->getClassMetadata()
);
}
public function onFlush(EventArgs $args)
{
$ea = $this->getEventAdapter($args);
$om = $ea->getObjectManager();
$uow = $om->getUnitOfWork();
// check all pending updates
foreach ($ea->getScheduledObjectUpdates($uow) as $object) {
$meta = $om->getClassMetadata(get_class($object));
// if it has our metadata lets encode the properties
if ($config = $this->getConfiguration($om, $meta->name)) {
$this->encode($ea, $object, $config);
}
}
// check all pending insertions
foreach ($ea->getScheduledObjectInsertions($uow) as $object) {
$meta = $om->getClassMetadata(get_class($object));
// if it has our metadata lets encode the properties
if ($config = $this->getConfiguration($om, $meta->name)) {
$this->encode($ea, $object, $config);
}
// recalculate changeset
$ea->recomputeSingleObjectChangeSet($uow, $meta, $object);
}
}
protected function getNamespace()
{
// mapper must know the namespace of extension
return __NAMESPACE__;
}
private function encode(EventAdapterInterface $ea, $object, $config)
{
$om = $ea->getObjectManager();
$meta = $om->getClassMetadata(get_class($object));
$uow = $om->getUnitOfWork();
foreach ($config['encode'] as $field => $options) {
$value = $meta->getReflectionProperty($field)->getValue($object);
$method = $options['type'];
$encoded = $method($options['secret'].$value);
$meta->getReflectionProperty($field)->setValue($object, $encoded);
}
// recalculate changeset
$ea->recomputeSingleObjectChangeSet($uow, $meta, $object);
}
}
```
**Note:** event adapter uses **EventArgs** to recognize with which manager
we are dealing with. It also uses event arguments to retrieve manager and transforms
the method call in its way. You can extend the event adapter in order to add some
specific methods for each manager.
That's it, now it will work on ORM and ODM object managers.
<a name="event-adapter-customize"></a>
## Customizing event adapter for specific functions
In most cases event listener will need specific functionality which will differ
for every object manager. For instance, a query to load users will differ. The
example bellow will illustrate how to handle such situations. You will need to
extend default ORM and ODM event adapters to implement specific functions which
will be available through the event adapter. First we will need to follow the
mapping convention to use those extension points.
### Extending default event adapters
Update your directory structure:
```
project
...
bootstrap.php
vendor
Extension
Encoder
Mapping
Driver
Annotation.php
Event
Adapter
ORM.php
ODM.php
Annotations.php
EncoderListener.php
...
```
Now **Mapping** extension will automatically create event adapter instances
from the extended ones.
Create extended ORM event adapter:
``` php
<?php
// file: vendor/Extension/Encoder/Mapping/Event/Adapter/ORM.php
namespace Extension\Encoder\Mapping\Event\Adapter;
use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM;
class ORM extends BaseAdapterORM
{
public function someSpecificMethod()
{
}
}
```
Create extended ODM event adapter:
``` php
<?php
// file: vendor/Extension/Encoder/Mapping/Event/Adapter/ODM.php
namespace Extension\Encoder\Mapping\Event\Adapter;
use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM;
class ODM extends BaseAdapterODM
{
public function someSpecificMethod()
{
}
}
```
It would be useful to make a common interface for those extended adapters.
Now every possible requirement is fulfilled and this may be useful.
Any suggestions on improvements are very welcome

View File

@@ -0,0 +1,171 @@
# Reference Integrity behavior extension for Doctrine 2
**ReferenceIntegrity** behavior will automate the reference integrity for referenced documents.
It works through annotations and yaml, and supports 'nullify', 'pull' and 'restrict' which throws an exception.
So let's say you have a Type which is referenced to multiple Articles, when deleting the Type, by default the Article
would still have a reference to Type, since Mongo doesn't care. When setting the ReferenceIntegrity to 'nullify' it
would then automatically remove the reference from Article.
When the owning side (Article#types) is a ReferenceMany and ReferenceIntegrity is set to 'pull', the removed document would automatically be pulled from Article#types.
Features:
- Automatically remove referenced association
- ODM only
- ReferenceOne and ReferenceMany support
- 'nullify', 'pull' and 'restrict' support
- Annotation and Yaml mapping support for extensions
**Symfony:**
- **ReferenceIntegrity** is available as [Bundle](http://github.com/stof/StofDoctrineExtensionsBundle)
for **Symfony2**, together with all other extensions
This article will cover the basic installation and functionality of **ReferenceIntegrity** behavior
Content:
- [Including](#including-extension) the extension
- Document [example](#document-mapping)
- [Yaml](#yaml-mapping) mapping example
- Usage [examples](#advanced-examples)
<a name="including-extension"></a>
## Setup and autoloading
Read the [documentation](http://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/annotations.md#em-setup)
or check the [example code](http://github.com/Atlantic18/DoctrineExtensions/tree/master/example)
on how to setup and use the extensions in most optimized way.
<a name="document-mapping"></a>
## ReferenceIntegrity Document example:
``` php
<?php
namespace Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* @ODM\Document(collection="types")
*/
class Type
{
/**
* @ODM\Id
*/
private $id;
/**
* @ODM\Field(type="string")
*/
private $title;
/**
* @ODM\ReferenceOne(targetDocument="Article", mappedBy="type")
* @Gedmo\ReferenceIntegrity("nullify")
* @var Article
*/
protected $article;
// ...
}
```
It is necessary to have the 'mappedBy' option set, to be able to access the referenced documents.
On removal of Type, on the referenced Article the Type reference will be nullified (removed)
<a name="yaml-mapping"></a>
## Yaml mapping example:
Yaml mapped Article: **/mapping/yaml/Documents.Article.dcm.yml**
```
---
Document\Type:
type: document
collection: types
fields:
id:
id: true
title:
type: string
article:
reference: true
type: one
mappedBy: type
targetDocument: Document\Article
gedmo:
referenceIntegrity: nullify # or pull or restrict
```
It is necessary to have the 'mappedBy' option set, to be able to access the referenced documents.
<a name="advanced-examples"></a>
## Usage examples:
Few operations to see 'nullify' in action:
``` php
<?php
$article = new Article;
$article->setTitle('My Article');
$type = new Type;
$type->setTitle('Published');
$article->setType($type);
$em->persist($article);
$em->persist($type);
$em->flush();
$type = $em->getRepository('Document\Type')->findByTitle('Published');
$em->remove($type);
$em->flush();
$article = $em->getRepository('Document\Article')->findByTitle('My Article');
$article->getType(); // won't be referenced to Type anymore
```
Few operations to see 'pull' in action:
``` php
<?php
$article = new Article;
$article->setTitle('My Article');
$type1 = new Type;
$type1->setTitle('Published');
$type2 = new Type;
$type2->setTitle('Info');
$article->addType($type1);
$article->addType($type2);
$em->persist($article);
$em->persist($type1);
$em->persist($type2);
$em->flush();
$type2 = $em->getRepository('Document\Type')->findByTitle('Info');
$em->remove($type2);
$em->flush();
$article = $em->getRepository('Document\Article')->findByTitle('My Article');
$article->getTypes(); // will only contain $type1 ('Published')
```
When 'ReferenceIntegrity' is set to 'restrict' a `ReferenceIntegrityStrictException` will be thrown, only when there
is a referenced document.
Easy like that, any suggestions on improvements are very welcome

View File

@@ -0,0 +1,219 @@
# Cross Object Mapper References behavior extension for Doctrine 2
Create documents and entities that contain references to each other.
## Options
The following options are possible on reference one and many associations:
**Owning Side**
- **type** - The type of association.
- **class** - The associated class name.
- **inversedBy** - The property name for the inverse side of this association.
- **identifier** - The property name to store the associated object id in.
**Inverse Side**
- **type** - The type of association.
- **class** - The associated class name.
- **mappedBy** - The property name for the owning side of this association.
## Annotations
**@Gedmo\ReferenceMany**
``` php
<?php
/**
* @Gedmo\ReferenceMany(type="entity", class="Entity\StockItem", mappedBy="product")
*/
private $stockItems;
```
**@Gedmo\ReferenceOne**
``` php
<?php
/**
* @Gedmo\ReferenceOne(type="document", class="Document\Product", inversedBy="stockItems", identifier="productId")
*/
private $product;
```
## Example
Here is an example where you have a Product which is mapped using the Doctrine MongoDB ODM project and it contains a property `$stockItems` that is populated from the Doctrine2 ORM.
``` php
<?php
namespace Document;
use Doctrine\Common\Collections\Collection;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* @ODM\Document
*/
class Product
{
/**
* @ODM\Id
*/
private $id;
/**
* @ODM\Field(type="string")
*/
private $name;
/**
* @Gedmo\ReferenceMany(type="entity", class="Entity\StockItem", mappedBy="product")
*/
private $stockItems;
public function getId()
{
return $this->id;
}
public function setId($id)
{
$this->id = $id;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
public function getStockItems()
{
return $this->stockItems;
}
public function setStockItems(Collection $stockItems)
{
$this->stockItems = $stockItems;
}
}
```
The `StockItem` has a reference to the `Product` as well.
``` php
<?php
namespace Entity;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use References\Fixture\ODM\MongoDB\Product;
/**
* @ORM\Entity
*/
class StockItem
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @ORM\Column
*/
private $name;
/**
* @ORM\Column
*/
private $sku;
/**
* @ORM\Column(type="integer")
*/
private $quantity;
/**
* @Gedmo\ReferenceOne(type="document", class="Document\Product", inversedBy="stockItems", identifier="productId")
*/
private $product;
/**
* @ORM\Column(type="string")
*/
private $productId;
public function getId()
{
return $this->id;
}
public function setId($id)
{
$this->id = $id;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
public function getSku()
{
return $this->sku;
}
public function setSku($sku)
{
$this->sku = $sku;
}
public function getQuantity()
{
return $this->quantity;
}
public function setQuantity($quantity)
{
$this->quantity = $quantity;
}
public function setProduct(Product $product)
{
$this->product = $product;
}
public function getProduct()
{
return $this->product;
}
public function setProductId($productId)
{
$this->productId = $productId;
}
public function getProductId()
{
return $this->productId;
}
}
```

View File

@@ -0,0 +1,810 @@
# Sluggable behavior extension for Doctrine 2
**Sluggable** behavior will build the slug of predefined fields on a given field
which should store the slug
Features:
- Automatic predefined field transformation into slug
- ORM and ODM support using same listener
- Slugs can be unique and styled, even with prefixes and/or suffixes
- Can be nested with other behaviors
- Annotation, Yaml and Xml mapping support for extensions
- Multiple slugs, different slugs can link to same fields
Update **2013-10-26**
- Datetime support with default dateFormat Y-m-d-H:i
Update **2013-08-23**
- Added 'prefix' and 'suffix' configuration parameter #812
Update **2013-08-19**
- allow empty slug #807 regenerate slug only if set to `null`
Update **2013-03-10**
- Added 'unique_base' configuration parameter to the Sluggable behaviour
Update **2012-11-30**
- Recreated slug handlers, as they are used by many people
Update **2012-02-26**
- Remove slug handlers were removed because of complications it brought together
Update **2011-09-11**
- Refactored sluggable for doctrine2.2 by specifying slug fields directly in slug annotation
- Slug handler functionality, possibility to create custom ones or use built-in
tree path handler or linked slug through single valued association
- Updated documentation mapping examples for 2.1.x version or higher
Update **2011-04-04**
- Made single listener, one instance can be used for any object manager and any number of them
Update **2010-12-23**
- Full support for unique index on slug field,
no more exceptions during concurrent flushes.
**Note:**
- There is a reported [issue](https://github.com/Atlantic18/DoctrineExtensions/issues/254) that sluggable transliterator
does not work on OSX 10.6 its ok starting again from 10.7 version. To overcome the problem
you can use your [custom transliterator](#transliterator)
- Public [Sluggable repository](http://github.com/Atlantic18/DoctrineExtensions "Sluggable extension on Github") is available on github
- Last update date: **2012-02-26**
- For usage together with **SoftDeleteable** in order to take into account softdeleted entities while generating unique
slug, you must explicitly call **addManagedFilter** with a name of softdeleteable filter, so it can be disabled during
slug updates. The best place to do it, is when initializing sluggable listener. That will be automated in the future.
**Portability:**
- **Sluggable** is now available as [Bundle](http://github.com/stof/StofDoctrineExtensionsBundle)
ported to **Symfony2** by **Christophe Coevoet**, together with all other extensions
This article will cover the basic installation and functionality of **Sluggable**
behavior
Content:
- [Including](#including-extension) the extension
- Entity [example](#entity-mapping)
- Document [example](#document-mapping)
- [Yaml](#yaml-mapping) mapping example
- [Xml](#xml-mapping) mapping example
- Basic usage [examples](#basic-examples)
- Custom [transliterator](#transliterator)
- Advanced usage [examples](#advanced-examples)
- Using [slug handlers](#slug-handlers)
<a name="including-extension"></a>
## Setup and autoloading
Read the [documentation](http://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/annotations.md#em-setup)
or check the [example code](http://github.com/Atlantic18/DoctrineExtensions/tree/master/example)
on how to setup and use the extensions in most optimized way.
<a name="entity-mapping"></a>
## Sluggable Entity example:
### Sluggable annotations:
- **@Gedmo\Mapping\Annotation\Slug** it will use this column to store **slug** generated
**fields** option must be specified, an array of field names to slug
**Note:** that Sluggable interface is not necessary, except in cases there
you need to identify entity as being Sluggable. The metadata is loaded only once then
cache is activated
**Note:** 2.0.x version of extensions used @Gedmo\Mapping\Annotation\Sluggable to identify
the field for slug
``` php
<?php
namespace Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="articles")
* @ORM\Entity
*/
class Article
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(length=64)
*/
private $title;
/**
* @ORM\Column(length=16)
*/
private $code;
/**
* @Gedmo\Slug(fields={"title", "code"})
* @ORM\Column(length=128, unique=true)
*/
private $slug;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setCode($code)
{
$this->code = $code;
}
public function getCode()
{
return $this->code;
}
public function getSlug()
{
return $this->slug;
}
}
```
<a name="document-mapping"></a>
## Sluggable Document example:
``` php
<?php
namespace Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* @ODM\Document(collection="articles")
*/
class Article
{
/**
* @ODM\Id
*/
private $id;
/**
* @ODM\Field(type="string")
*/
private $title;
/**
* @ODM\Field(type="string")
*/
private $code;
/**
* @Gedmo\Slug(fields={"title", "code"})
* @ODM\Field(type="string")
*/
private $slug;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setCode($code)
{
$this->code = $code;
}
public function getCode()
{
return $this->code;
}
public function getSlug()
{
return $this->slug;
}
}
```
<a name="yaml-mapping"></a>
## Yaml mapping example
Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml**
```
---
Entity\Article:
type: entity
table: articles
id:
id:
type: integer
generator:
strategy: AUTO
fields:
title:
type: string
length: 64
code:
type: string
length: 16
slug:
type: string
length: 128
gedmo:
slug:
separator: _
style: camel
fields:
- title
- code
indexes:
search_idx:
columns: slug
```
<a name="xml-mapping"></a>
## Xml mapping example
**Note:** xml driver is not yet adapted for single slug mapping
``` xml
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">
<entity name="Entity\Article" table="sluggables">
<id name="id" type="integer" column="id">
<generator strategy="AUTO"/>
</id>
<field name="title" type="string" length="128"/>
<field name="code" type="string" length="16"/>
<field name="ean" type="string" length="13"/>
<field name="slug" type="string" length="156" unique="true">
<gedmo:slug unique="true" style="camel" updatable="false" separator="_" fields="title,code,ean" />
</field>
</entity>
</doctrine-mapping>
```
<a name="basic-examples"></a>
## Basic usage examples:
### To save **Article** and generate slug simply use:
``` php
<?php
$article = new Article();
$article->setTitle('the title');
$article->setCode('my code');
$this->em->persist($article);
$this->em->flush();
echo $article->getSlug();
// prints: the-title-my-code
```
### Some other configuration options for **slug** annotation:
- **fields** (required, default=[]) - list of fields for slug
- **updatable** (optional, default=true) - **true** to update the slug on sluggable field changes, **false** - otherwise
- **unique** (optional, default=true) - **true** if slug should be unique and if identical it will be prefixed, **false** - otherwise
- **unique_base** (optional, default=null) - used in conjunction with **unique**. The name of the entity property that should be used as a key when doing a uniqueness check.
- **separator** (optional, default="-") - separator which will separate words in slug
- **prefix** (optional, default="") - prefix which will be added to the generated slug
- **suffix** (optional, default="") - suffix which will be added to the generated slug
- **style** (optional, default="default") - **"default"** all letters will be lowercase, **"camel"** - first word letter will be uppercase, **"upper"**- all word letter will be uppercase and **"lower"**- all word letter will be lowercase
- **handlers** (optional, default=[]) - list of slug handlers, like tree path slug, or customized, for example see bellow
**Note**: handlers are totally optional
**TreeSlugHandler**
``` php
<?php
/**
* @Gedmo\Mapping\Annotation\Slug(handlers={
* @Gedmo\Mapping\Annotation\SlugHandler(class="Gedmo\Sluggable\Handler\TreeSlugHandler", options={
* @Gedmo\Mapping\Annotation\SlugHandlerOption(name="parentRelationField", value="parent"),
* @Gedmo\Mapping\Annotation\SlugHandlerOption(name="separator", value="/")
* })
* }, fields={"title", "code"})
* @Doctrine\ORM\Mapping\Column(length=64, unique=true)
*/
private $slug;
```
**RelativeSlugHandler**:
``` php
<?php
/**
* Person domain object class
*
* @Gedmo\Mapping\Annotation\Slug(handlers={
* @Gedmo\Mapping\Annotation\SlugHandler(class="Gedmo\Sluggable\Handler\RelativeSlugHandler", options={
* @Gedmo\Mapping\Annotation\SlugHandlerOption(name="relationField", value="category"),
* @Gedmo\Mapping\Annotation\SlugHandlerOption(name="relationSlugField", value="slug"),
* @Gedmo\Mapping\Annotation\SlugHandlerOption(name="separator", value="/")
* })
* }, fields={"title", "code"})
* @Doctrine\ORM\Mapping\Column(length=64, unique=true)
*/
private $slug;
```
If the relationSlugField you are using is not a slug field but a string field for example you can make
sure the relationSlugField is also urilized with:
``` php
<?php
/**
* Person domain object class
*
* @Gedmo\Mapping\Annotation\Slug(handlers={
* @Gedmo\Mapping\Annotation\SlugHandler(class="Gedmo\Sluggable\Handler\RelativeSlugHandler", options={
* @Gedmo\Mapping\Annotation\SlugHandlerOption(name="relationField", value="category"),
* @Gedmo\Mapping\Annotation\SlugHandlerOption(name="relationSlugField", value="title"),
* @Gedmo\Mapping\Annotation\SlugHandlerOption(name="separator", value="/"),
* @Gedmo\Mapping\Annotation\SlugHandlerOption(name="urilize", value=true)
* })
* }, fields={"title", "code"})
* @Doctrine\ORM\Mapping\Column(length=64, unique=true)
*/
private $slug;
```
This will make sure that the 'title' field in the category entity is url friendly.
**Note:** if you used **RelativeSlugHandler** - relation object should use in order to sync changes:
**InversedRelativeSlugHandler**
``` php
<?php
/**
* Category domain object class
*
* @Gedmo\Mapping\Annotation\Slug(handlers={
* @Gedmo\Mapping\Annotation\SlugHandler(class="Gedmo\Sluggable\Handler\InversedRelativeSlugHandler", options={
* @Gedmo\Mapping\Annotation\SlugHandlerOption(name="relationClass", value="App\Entity\Person"),
* @Gedmo\Mapping\Annotation\SlugHandlerOption(name="mappedBy", value="category"),
* @Gedmo\Mapping\Annotation\SlugHandlerOption(name="inverseSlugField", value="slug")
* })
* }, fields={"title"})
* @Doctrine\ORM\Mapping\Column(length=64, unique=true)
*/
private $slug;
```
### Example
``` php
<?php
class Article
{
// ...
/**
* @Gedmo\Slug(fields={"title", "created"}, style="camel", separator="_", updatable=false, unique=false, dateFormat="d/m/Y H-i-s")
* @Doctrine\ORM\Mapping\Column(length=128, unique=true)
*/
private $slug;
// ...
// ...
/**
* @Doctrine\ORM\Mapping\Column(type="datetime", name="created_at")
*/
private $createdAt;
// ...
/**
* @Doctrine\ORM\Mapping\Column(length=128)
*/
private $title;
// ...
public function __construct()
{
$this->createdAt = new \DateTime;
}
}
```
And now test the result:
``` php
<?php
$article = new Article();
$article->setTitle('the title');
$article->setCode('my code');
$this->em->persist($article);
$this->em->flush();
echo $article->getSlug();
// prints: The_Title_My_Code
```
<a name="transliterator"></a>
## Custom transliterator
To set your own custom transliterator, which would be used to generate the slug, use:
``` php
<?php
$callable = array('My\Class', 'transliterationMethod');
$sluggableListener->setTransliterator($callable);
// or use a closure
$callable = function($text, $separatorUsed, $objectBeingSlugged) {
// ...
return $transliteratedText;
};
$sluggableListener->setTransliterator($callable);
```
<a name="advanced-examples"></a>
## Advanced examples:
### Regenerating slug
In case if you want the slug to regenerate itself based on sluggable fields, set the slug to **null**.
*Note: in previous versions empty strings would also cause the slug to be regenerated. This behaviour was changed in v2.3.8.*
``` php
<?php
$entity = $em->find('Entity\Something', $id);
$entity->setSlug(null);
$em->persist($entity);
$em->flush();
```
### Setting the slug manually
Sometimes you might need to set it manually, etc if generated one does not look satisfying enough.
Sluggable will ensure uniqueness of the slug.
``` php
<?php
$entity = new SomeEntity;
$entity->setSluggableField('won\'t be taken into account');
$entity->setSlug('the required slug, set manually');
$em->persist($entity);
$em->flush();
echo $entity->getSlug(); // outputs: "the-required-slug-set-manually"
```
### Using TranslatableListener to translate our slug
If you want to attach **TranslatableListener** also add it to EventManager after
the **SluggableListener**. It is important because slug must be generated first
before the creation of it`s translation.
``` php
<?php
$evm = new \Doctrine\Common\EventManager();
$sluggableListener = new \Gedmo\Sluggable\SluggableListener();
$evm->addEventSubscriber($sluggableListener);
$translatableListener = new \Gedmo\Translatable\TranslatableListener();
$translatableListener->setTranslatableLocale('en_us');
$evm->addEventSubscriber($translatableListener);
// now this event manager should be passed to entity manager constructor
```
And the Entity should look like:
``` php
<?php
namespace Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="articles")
* @ORM\Entity
*/
class Article
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @Gedmo\Translatable
* @ORM\Column(length=64)
*/
private $title;
/**
* @Gedmo\Translatable
* @ORM\Column(length=16)
*/
private $code;
/**
* @Gedmo\Translatable
* @Gedmo\Slug(fields={"title", "code"})
* @ORM\Column(length=128, unique=true)
*/
private $slug;
/**
* @ORM\Column(type="string", length=64)
*/
private $uniqueTitle;
/**
* @Gedmo\Slug(fields={"uniqueTitle"}, prefix="some-prefix-")
* @ORM\Column(type="string", length=128, unique=true)
*/
private $uniqueSlug;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setCode($code)
{
$this->code = $code;
}
public function getCode()
{
return $this->code;
}
public function getSlug()
{
return $this->slug;
}
public function getUniqueSlug()
{
return $this->uniqueSlug;
}
}
```
Now the generated slug will be translated by Translatable behavior
<a name="slug-handlers"></a>
## Using slug handlers:
There are built-in slug handlers like described in configuration options of slug, but there
can be also customized slug handlers depending on use cases. Usually the most logic use case
is for related slug. For instance if user has a **ManyToOne relation to a **Company** we
would like to have a url like **http://example.com/knplabs/gedi where **KnpLabs**
is a company and user name is **Gedi**. In this case relation has a path separator **/**
User entity example:
``` php
<?php
namespace Sluggable\Fixture\Handler;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class User
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(length=64)
*/
private $username;
/**
* @Gedmo\Slug(handlers={
* @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\RelativeSlugHandler", options={
* @Gedmo\SlugHandlerOption(name="relationField", value="company"),
* @Gedmo\SlugHandlerOption(name="relationSlugField", value="alias"),
* @Gedmo\SlugHandlerOption(name="separator", value="/")
* })
* }, separator="-", updatable=true, fields={"username"})
* @ORM\Column(length=64, unique=true)
*/
private $slug;
/**
* @ORM\ManyToOne(targetEntity="Company")
*/
private $company;
public function setCompany(Company $company = null)
{
$this->company = $company;
}
public function getCompany()
{
return $this->company;
}
public function getId()
{
return $this->id;
}
public function setName($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
public function getSlug()
{
return $this->slug;
}
}
```
Company entity example:
``` php
<?php
namespace Sluggable\Fixture\Handler;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Company
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(length=64)
*/
private $title;
/**
* @Gedmo\Slug(handlers={
* @Gedmo\SlugHandler(class="Gedmo\Sluggable\Handler\InversedRelativeSlugHandler", options={
* @Gedmo\SlugHandlerOption(name="relationClass", value="Sluggable\Fixture\Handler\User"),
* @Gedmo\SlugHandlerOption(name="mappedBy", value="company"),
* @Gedmo\SlugHandlerOption(name="inverseSlugField", value="slug")
* })
* }, fields={"title"})
* @ORM\Column(length=64, unique=true)
*/
private $alias;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function getAlias()
{
return $this->alias;
}
}
```
For other mapping drivers see
[xml](https://github.com/Atlantic18/DoctrineExtensions/blob/master/tests/Gedmo/Mapping/Driver/Xml/Mapping.Fixture.Xml.Sluggable.dcm.xml) or [yaml](https://github.com/Atlantic18/DoctrineExtensions/blob/master/tests/Gedmo/Mapping/Driver/Yaml/Mapping.Fixture.Yaml.Category.dcm.yml) examples from tests
And the example usage:
``` php
<?php
$company = new Company;
$company->setTitle('KnpLabs');
$em->persist($company);
$gedi = new User;
$gedi->setUsername('Gedi');
$gedi->setCompany($company);
$em->persist($gedi);
$em->flush();
echo $gedi->getSlug(); // outputs "knplabs/gedi"
$company->setTitle('KnpLabs Nantes');
$em->persist($company);
$em->flush();
echo $gedi->getSlug(); // outputs "knplabs-nantes/gedi"
```
**Note:** tree slug handler, takes a parent relation to build slug recursively.
Any suggestions on improvements are very welcome

View File

@@ -0,0 +1,286 @@
# SoftDeleteable behavior extension for Doctrine 2
**SoftDeleteable** behavior allows to "soft delete" objects, filtering them
at SELECT time by marking them deleted as with a timestamp, but not explicitly removing them from the database.
Features:
- Works with DQL DELETE queries (using a Query Hint).
- All SELECT queries will be filtered, not matter from where they are executed (Repositories, DQL SELECT queries, etc).
- For now, it works only with the ORM
- Can be nested with other behaviors
- Annotation, Yaml and Xml mapping support for extensions
- Support for 'timeAware' option: When creating an entity set a date of deletion in the future and never worry about cleaning up at expiration time.
- Support for 'hardDelete' option: When deleting a second time it allows to disable hard delete.
Content:
- [Including](#including-extension) the extension
- Entity [example](#entity-mapping)
- [Yaml](#yaml-mapping) mapping example
- [Xml](#xml-mapping) mapping example
- Usage [examples](#usage)
- Using [Traits](#traits)
<a name="including-extension"></a>
## Setup and autoloading
Read the [documentation](http://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/annotations.md#em-setup)
or check the [example code](http://github.com/Atlantic18/DoctrineExtensions/tree/master/example)
on how to setup and use the extensions in most optimized way.
With SoftDeleteable there's one more step you need to do. You need to add the filter to your configuration:
``` php
$config = new Doctrine\ORM\Configuration;
// Your configs..
$config->addFilter('soft-deleteable', 'Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter');
```
And then you can access the filter from your EntityManager to enable or disable it with the following code:
``` php
// This will enable the SoftDeleteable filter, so entities which were "soft-deleted" will not appear
// in results
// You should adapt the filter name to your configuration (ex: softdeleteable)
$em->getFilters()->enable('soft-deleteable');
// This will disable the SoftDeleteable filter, so entities which were "soft-deleted" will appear in results
$em->getFilters()->disable('soft-deleteable');
```
Or from your DocumentManager (ODM):
``` php
// This will enable the SoftDeleteable filter, so entities which were "soft-deleted" will not appear
// in results
// You should adapt the filter name to your configuration (ex: softdeleteable)
$em->getFilterCollection()->enable('soft-deleteable');
// This will disable the SoftDeleteable filter, so entities which were "soft-deleted" will appear in results
$em->getFilterCollection()->disable('soft-deleteable');
```
**NOTE:** by default all filters are disabled, so you must explicitly enable **soft-deleteable** filter in your setup
or whenever you need it.
<a name="entity-mapping"></a>
## SoftDeleteable Entity example:
### SoftDeleteable annotations:
- **@Gedmo\Mapping\Annotation\SoftDeleteable** this class annotation tells if a class is SoftDeleteable. It has a
mandatory parameter "fieldName", which is the name of the field to be used to hold the known "deletedAt" field. It
must be of any of the date types.
Available configuration options:
- **fieldName** - The name of the field that will be used to determine if the object is removed or not (NULL means
it's not removed. A date value means it was removed). NOTE: The field MUST be nullable.
- **hardDelete** - A boolean to enable or disable hard delete after soft delete has already been done. NOTE: Set to true by default.
**Note:** that SoftDeleteable interface is not necessary, except in cases where
you need to identify entity as being SoftDeleteable. The metadata is loaded only once then
cache is activated.
``` php
<?php
namespace Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false, hardDelete=true)
*/
class Article
{
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @ORM\Column(name="title", type="string")
*/
private $title;
/**
* @ORM\Column(name="deletedAt", type="datetime", nullable=true)
*/
private $deletedAt;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function getDeletedAt()
{
return $this->deletedAt;
}
public function setDeletedAt($deletedAt)
{
$this->deletedAt = $deletedAt;
}
}
```
<a name="yaml-mapping"></a>
## Yaml mapping example:
Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml**
```
---
Entity\Article:
type: entity
table: articles
gedmo:
soft_deleteable:
field_name: deletedAt
time_aware: false
hard_delete: true
id:
id:
type: integer
generator:
strategy: AUTO
fields:
title:
type: string
deletedAt:
type: date
nullable: true
```
<a name="xml-mapping"></a>
## Xml mapping example
``` xml
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">
<entity name="Mapping\Fixture\Xml\Timestampable" table="timestampables">
<id name="id" type="integer" column="id">
<generator strategy="AUTO"/>
</id>
<field name="title" type="string" />
<field name="deletedAt" type="datetime" nullable="true" />
<gedmo:soft-deleteable field-name="deletedAt" time-aware="false" hard-delete="true" />
</entity>
</doctrine-mapping>
```
<a name="usage"></a>
## Usage:
``` php
<?php
$article = new Article;
$article->setTitle('My Article');
$em->persist($article);
$em->flush();
// Now if we remove it, it will set the deletedAt field to the actual date
$em->remove($article);
$em->flush();
$repo = $em->getRepository('Article');
$art = $repo->findOneBy(array('title' => 'My Article'));
// It should NOT return the article now
$this->assertNull($art);
// But if we disable the filter, the article should appear now
$em->getFilters()->disable('soft-deleteable');
$art = $repo->findOneBy(array('title' => 'My Article'));
$this->assertTrue(is_object($art));
// Enable / Disable filter filter, for specified entity (default is enabled for all)
$filter = $em->getFilters()->enable('soft-deleteable');
$filter->disableForEntity('Entity\Article');
$filter->enableForEntity('Entity\Article');
// Undelete the entity by setting the deletedAt field to null
$article->setDeletedAt(null);
```
Easy like that, any suggestions on improvements are very welcome.
<a name="traits"></a>
## Traits
You can use softDeleteable traits for quick **deletedAt** timestamp definitions
when using annotation mapping.
There is also a trait without annotations for easy integration purposes.
**Note:** this feature is only available since php **5.4.0**. And you are not required
to use the Traits provided by extensions.
``` php
<?php
namespace SoftDeleteable\Fixture;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Gedmo\SoftDeleteable\Traits\SoftDeleteableEntity;
/**
* @ORM\Entity
* @Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false, hardDelete=true)
*/
class UsingTrait
{
/**
* Hook SoftDeleteable behavior
* updates deletedAt field
*/
use SoftDeleteableEntity;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(length=128)
*/
private $title;
}
```
Traits are very simple and if you use different field names I recommend to simply create your
own ones based per project. These ones are standing as an example.

View File

@@ -0,0 +1,328 @@
# Sortable behavior extension for Doctrine2
**Sortable** behavior will maintain a position field for ordering
entities.
Features:
- Automatic handling of position index
- Group entity ordering by one or more fields
- Can be nested with other behaviors
- Annotation, Yaml and Xml mapping support for extensions
**Note:**
- Public [Sortable repository](http://github.com/Atlantic18/DoctrineExtensions "Sortable extension on Github") is available on github
- Last update date: **2012-01-02**
**Portability:**
- **Sortable** is now available as [Bundle](http://github.com/stof/StofDoctrineExtensionsBundle)
ported to **Symfony2** by **Christophe Coevoet**, together with all other extensions
This article will cover the basic installation and functionality of **Sortable**
behavior
Content:
- [Including](#including-extension) the extension
- Entity [example](#entity-mapping)
- [Yaml](#yaml-mapping) mapping example
- [Xml](#xml-mapping) mapping example
- Basic usage [examples](#basic-examples)
- Custom comparison [method](#custom-comparisons)
<a name="including-extension"></a>
## Setup and autoloading
Read the [documentation](http://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/annotations.md#em-setup)
or check the [example code](http://github.com/Atlantic18/DoctrineExtensions/tree/master/example)
on how to setup and use the extensions in most optimized way.
<a name="entity-mapping"></a>
## Sortable Entity example:
### Sortable annotations:
- **@Gedmo\Mapping\Annotation\SortableGroup** it will use this field for **grouping**
- **@Gedmo\Mapping\Annotation\SortablePosition** it will use this column to store **position** index
**Note:** that Sortable interface is not necessary, except in cases there
you need to identify entity as being Sortable. The metadata is loaded only once then
cache is activated
**Note:** that you should register SortableRepository (or a subclass) as the repository in the Entity
annotation to benefit from its query methods.
``` php
<?php
namespace Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="items")
* @ORM\Entity(repositoryClass="Gedmo\Sortable\Entity\Repository\SortableRepository")
*/
class Item
{
/** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */
private $id;
/**
* @ORM\Column(name="name", type="string", length=64)
*/
private $name;
/**
* @Gedmo\SortablePosition
* @ORM\Column(name="position", type="integer")
*/
private $position;
/**
* @Gedmo\SortableGroup
* @ORM\Column(name="category", type="string", length=128)
*/
private $category;
public function getId()
{
return $this->id;
}
public function setName($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
public function setPosition($position)
{
$this->position = $position;
}
public function getPosition()
{
return $this->position;
}
public function setCategory($category)
{
$this->category = $category;
}
public function getCategory()
{
return $this->category;
}
}
```
<a name="yaml-mapping"></a>
## Yaml mapping example
Yaml mapped Item: **/mapping/yaml/Entity.Item.dcm.yml**
```
---
Entity\Item:
type: entity
table: items
id:
id:
type: integer
generator:
strategy: AUTO
fields:
name:
type: string
length: 64
position:
type: integer
gedmo:
- sortablePosition
category:
type: string
length: 128
gedmo:
- sortableGroup
```
<a name="xml-mapping"></a>
## Xml mapping example
``` xml
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">
<entity name="Entity\Item" table="items">
<id name="id" type="integer" column="id">
<generator strategy="AUTO"/>
</id>
<field name="name" type="string" length="128">
</field>
<field name="position" type="integer">
<gedmo:sortable-position/>
</field>
<field name="category" type="string" length="128">
<gedmo:sortable-group />
</field>
</entity>
</doctrine-mapping>
```
<a name="basic-examples"></a>
## Basic usage examples:
### To save **Items** at the end of the sorting list simply do:
``` php
<?php
// By default, items are appended to the sorting list
$item1 = new Item();
$item1->setName('item 1');
$item1->setCategory('category 1');
$this->em->persist($item1);
$item2 = new Item();
$item2->setName('item 2');
$item2->setCategory('category 1');
$this->em->persist($item2);
$this->em->flush();
echo $item1->getPosition();
// prints: 0
echo $item2->getPosition();
// prints: 1
```
### Save **Item** at a given position:
``` php
<?php
$item1 = new Item();
$item1->setName('item 1');
$item1->setCategory('category 1');
$this->em->persist($item1);
$item2 = new Item();
$item2->setName('item 2');
$item2->setCategory('category 1');
$this->em->persist($item2);
$item0 = new Item();
$item0->setName('item 0');
$item0->setCategory('category 1');
$item0->setPosition(0);
$this->em->persist($item0);
$this->em->flush();
$repo = $this->em->getRepository('Entity\\Item');
$items = $repo->getBySortableGroupsQuery(array('category' => 'category 1'))->getResult();
foreach ($items as $item) {
echo "{$item->getPosition()}: {$item->getName()}\n";
}
// prints:
// 0: item 0
// 1: item 1
// 2: item 2
```
### Reordering the sorted list:
``` php
<?php
$item1 = new Item();
$item1->setName('item 1');
$item1->setCategory('category 1');
$this->em->persist($item1);
$item2 = new Item();
$item2->setName('item 2');
$item2->setCategory('category 1');
$this->em->persist($item2);
$this->em->flush();
// Update the position of item2
$item2->setPosition(0);
$this->em->persist($item2);
$this->em->flush();
$repo = $this->em->getRepository('Entity\\Item');
$items = $repo->getBySortableGroupsQuery(array('category' => 'category 1'))->getResult();
foreach ($items as $item) {
echo "{$item->getPosition()}: {$item->getName()}\n";
}
// prints:
// 0: item 2
// 1: item 1
```
### Using a foreign_key / relation as SortableGroup
If you want to use a foreign key / relation as sortable group, you have to put @Gedmo\SortableGroup annotation on ManyToOne annotation:
```
/**
* @Gedmo\SortableGroup
* @ORM\ManyToOne(targetEntity="Item", inversedBy="children")
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="SET NULL")
*/
private $parent;
```
To move an item at the end of the list, you can set the position to `-1`:
```
$item2->setPosition(-1);
```
<a name="custom-comparisons"></a>
## Custom comparison:
Sortable works by comparing objects in the same group to see how they should be positioned. From time to time you may want to customize the way these
objects are compared by simply implementing the Doctrine\Common\Comparable interface
``` php
<?php
namespace Entity;
use Doctrine\Common\Comparable;
/**
* @ORM\Table(name="items")
* @ORM\Entity(repositoryClass="Gedmo\Sortable\Entity\Repository\SortableRepository")
*/
class Item implements Comparable
{
public function compareTo($other)
{
// return 1 if this object is considered greater than the compare value
// return -1 if this object is considered less than the compare value
// return 0 if this object is considered equal to the compare value
}
}
```

View File

@@ -0,0 +1,500 @@
# Install Gedmo Doctrine2 extensions in Symfony2
Configure full featured [Doctrine2 extensions](http://github.com/Atlantic18/DoctrineExtensions) for your symfony2 project.
This post will show you - how to create a simple configuration file to manage extensions with
ability to use all features it provides.
Interested? then bear with me! and don't be afraid, we're not diving into security component :)
This post will put some light over the shed of extension installation and mapping configuration
of Doctrine2. It does not require any additional dependencies and gives you full power
over management of extensions.
Content:
- [Symfony2](#sf2-app) application
- Extensions metadata [mapping](#ext-mapping)
- Extension [listeners](#ext-listeners)
- Usage [example](#ext-example)
- Some [tips](#more-tips)
- [Alternative](#alternative) over configuration
<a name="sf2-app"></a>
## Symfony2 application
First of all, we will need a symfony2 startup application, let's say [symfony-standard edition
with composer](http://github.com/KnpLabs/symfony-with-composer). Follow the standard setup:
- `git clone git://github.com/KnpLabs/symfony-with-composer.git example`
- `cd example && rm -rf .git && php bin/vendors install`
- ensure your application loads and meets requirements, by following the url: **http://your_virtual_host/app_dev.php**
Now let's add the **gedmo/doctrine-extensions** into **composer.json**
```json
{
"require": {
"php": ">=5.3.2",
"symfony/symfony": ">=2.0.9,<2.1.0-dev",
"doctrine/orm": ">=2.1.0,<2.2.0-dev",
"twig/extensions": "*",
"symfony/assetic-bundle": "*",
"sensio/generator-bundle": "2.0.*",
"sensio/framework-extra-bundle": "2.0.*",
"sensio/distribution-bundle": "2.0.*",
"jms/security-extra-bundle": "1.0.*",
"gedmo/doctrine-extensions": "dev-master"
},
"autoload": {
"psr-0": {
"Acme": "src/"
}
}
}
```
Update vendors, run: **php composer.phar update gedmo/doctrine-extensions**
Initially in this package you have **doctrine2 orm** included, so we will base our setup
and configuration for this specific connection. Do not forget to configure your database
connection parameters, edit **app/config/parameters.yml**
<a name="ext-mapping"></a>
## Mapping
Let's start from the mapping. In case you use the **translatable**, **tree** or **loggable**
extension you will need to map those abstract mapped superclasses for your ORM to be aware of.
To do so, add some mapping info to your **doctrine.orm** configuration, edit **app/config/config.yml**:
```yaml
doctrine:
dbal:
# your dbal config here
orm:
auto_generate_proxy_classes: %kernel.debug%
auto_mapping: true
# only these lines are added additionally
mappings:
translatable:
type: annotation
alias: Gedmo
prefix: Gedmo\Translatable\Entity
# make sure vendor library location is correct
dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Entity"
```
After that, running **php app/console doctrine:mapping:info** you should see the output:
```
Found 3 entities mapped in entity manager default:
[OK] Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation
[OK] Gedmo\Translatable\Entity\MappedSuperclass\AbstractTranslation
[OK] Gedmo\Translatable\Entity\Translation
```
Well, we mapped only **translatable** for now, it really depends on your needs, which extensions
your application uses.
**Note:** there is **Gedmo\Translatable\Entity\Translation** which is not a super class, in that case
if you create a doctrine schema, it will add **ext_translations** table, which might not be useful
to you also. To skip mapping of these entities, you can map **only superclasses**
```yaml
mappings:
translatable:
type: annotation
alias: Gedmo
prefix: Gedmo\Translatable\Entity
# make sure vendor library location is correct
dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Entity/MappedSuperclass"
```
The configuration above, adds a **/MappedSuperclass** into directory depth, after running
**php app/console doctrine:mapping:info** you should only see now:
```
Found 2 entities mapped in entity manager default:
[OK] Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation
[OK] Gedmo\Translatable\Entity\MappedSuperclass\AbstractTranslation
```
This is very useful for advanced requirements and quite simple to understand. So now let's map
everything the extensions provide:
```yaml
# only orm config branch of doctrine
orm:
auto_generate_proxy_classes: %kernel.debug%
auto_mapping: true
# only these lines are added additionally
mappings:
translatable:
type: annotation
alias: Gedmo
prefix: Gedmo\Translatable\Entity
# make sure vendor library location is correct
dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Entity"
loggable:
type: annotation
alias: Gedmo
prefix: Gedmo\Loggable\Entity
dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Loggable/Entity"
tree:
type: annotation
alias: Gedmo
prefix: Gedmo\Tree\Entity
dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Tree/Entity"
```
<a name="ext-listeners"></a>
## Doctrine extension listener services
Next, the heart of extensions are behavioral listeners which pours all the sugar. We will
create a **yml** service file in our config directory. The setup can be different, your config could be located
in the bundle, it depends on your preferences. Edit **app/config/doctrine_extensions.yml**
```yaml
# services to handle doctrine extensions
# import it in config.yml
services:
# KernelRequest listener
extension.listener:
class: Acme\DemoBundle\Listener\DoctrineExtensionListener
calls:
- [ setContainer, [ "@service_container" ] ]
tags:
# translatable sets locale after router processing
- { name: kernel.event_listener, event: kernel.request, method: onLateKernelRequest, priority: -10 }
# loggable hooks user username if one is in security context
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
# translatable sets locale such as default application locale before command execute
- { name: kernel.event_listener, event: console.command, method: onConsoleCommand, priority: -10 }
# Doctrine Extension listeners to handle behaviors
gedmo.listener.tree:
class: Gedmo\Tree\TreeListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ "@annotation_reader" ] ]
gedmo.listener.translatable:
class: Gedmo\Translatable\TranslatableListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ "@annotation_reader" ] ]
- [ setDefaultLocale, [ %locale% ] ]
- [ setTranslationFallback, [ false ] ]
gedmo.listener.timestampable:
class: Gedmo\Timestampable\TimestampableListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ "@annotation_reader" ] ]
gedmo.listener.sluggable:
class: Gedmo\Sluggable\SluggableListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ "@annotation_reader" ] ]
gedmo.listener.sortable:
class: Gedmo\Sortable\SortableListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ "@annotation_reader" ] ]
gedmo.listener.loggable:
class: Gedmo\Loggable\LoggableListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ "@annotation_reader" ] ]
gedmo.listener.blameable:
class: Gedmo\Blameable\BlameableListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ "@annotation_reader" ] ]
```
So what does it include in general? Well, it creates services for all extension listeners.
You can remove some which you do not use, or change them as you need. **Translatable** for instance,
sets the default locale to the value of your `%locale%` parameter, you can configure it differently.
**Note:** In case you noticed, there is **Acme\DemoBundle\Listener\DoctrineExtensionListener**.
You will need to create this listener class if you use **loggable** or **translatable**
behaviors. This listener will set the **locale used** from request and **username** to
loggable. So, to finish the setup create **Acme\DemoBundle\Listener\DoctrineExtensionListener**
```php
<?php
// file: src/Acme/DemoBundle/Listener/DoctrineExtensionListener.php
namespace Acme\DemoBundle\Listener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Kernel;
class DoctrineExtensionListener implements ContainerAwareInterface
{
/**
* @var ContainerInterface
*/
protected $container;
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
public function onLateKernelRequest(GetResponseEvent $event)
{
$translatable = $this->container->get('gedmo.listener.translatable');
$translatable->setTranslatableLocale($event->getRequest()->getLocale());
}
public function onConsoleCommand()
{
$this->container->get('gedmo.listener.translatable')
->setTranslatableLocale($this->container->get('translator')->getLocale());
}
public function onKernelRequest(GetResponseEvent $event)
{
if (Kernel::MAJOR_VERSION == 2 && Kernel::MINOR_VERSION < 6) {
$securityContext = $this->container->get('security.context', ContainerInterface::NULL_ON_INVALID_REFERENCE);
if (null !== $securityContext && null !== $securityContext->getToken() && $securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
# for loggable behavior
$loggable = $this->container->get('gedmo.listener.loggable');
$loggable->setUsername($securityContext->getToken()->getUsername());
# for blameable behavior
$blameable = $this->container->get('gedmo.listener.blameable');
$blameable->setUserValue($securityContext->getToken()->getUser());
}
}
else {
$tokenStorage = $this->container->get('security.token_storage')->getToken();
$authorizationChecker = $this->container->get('security.authorization_checker');
if (null !== $tokenStorage && $authorizationChecker->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
# for loggable behavior
$loggable = $this->container->get('gedmo.listener.loggable');
$loggable->setUsername($tokenStorage->getUser());
# for blameable behavior
$blameable = $this->container->get('gedmo.listener.blameable');
$blameable->setUserValue($tokenStorage->getUser());
}
}
}
}
```
Do not forget to import **doctrine_extensions.yml** in your **app/config/config.yml**:
```yaml
# file: app/config/config.yml
imports:
- { resource: parameters.yml }
- { resource: security.yml }
- { resource: doctrine_extensions.yml }
# ... configuration follows
```
<a name="ext-example"></a>
## Example
After that, you have your extensions set up and ready to be used! Too easy right? Well,
if you do not believe me, let's create a simple entity in our **Acme** project:
```php
<?php
// file: src/Acme/DemoBundle/Entity/BlogPost.php
namespace Acme\DemoBundle\Entity;
use Gedmo\Mapping\Annotation as Gedmo; // gedmo annotations
use Doctrine\ORM\Mapping as ORM; // doctrine orm annotations
/**
* @ORM\Entity
*/
class BlogPost
{
/**
* @Gedmo\Slug(fields={"title"}, updatable=false, separator="_")
* @ORM\Id
* @ORM\Column(length=32, unique=true)
*/
private $id;
/**
* @Gedmo\Translatable
* @ORM\Column(length=64)
*/
private $title;
/**
* @Gedmo\Timestampable(on="create")
* @ORM\Column(name="created", type="datetime")
*/
private $created;
/**
* @ORM\Column(name="updated", type="datetime")
* @Gedmo\Timestampable(on="update")
*/
private $updated;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function getCreated()
{
return $this->created;
}
public function getUpdated()
{
return $this->updated;
}
}
```
Now, let's have some fun:
- if you have not created the database yet, run `php app/console doctrine:database:create`
- create the schema `php app/console doctrine:schema:create`
Everything will work just fine, you can modify the **Acme\DemoBundle\Controller\DemoController**
and add an action to test how it works:
```php
// file: src/Acme/DemoBundle/Controller/DemoController.php
// include this code portion
/**
* @Route("/posts", name="_demo_posts")
*/
public function postsAction()
{
$em = $this->getDoctrine()->getEntityManager();
$repository = $em->getRepository('AcmeDemoBundle:BlogPost');
// create some posts in case if there aren't any
if (!$repository->findOneById('hello_world')) {
$post = new \Acme\DemoBundle\Entity\BlogPost();
$post->setTitle('Hello world');
$next = new \Acme\DemoBundle\Entity\BlogPost();
$next->setTitle('Doctrine extensions');
$em->persist($post);
$em->persist($next);
$em->flush();
}
$posts = $em
->createQuery('SELECT p FROM AcmeDemoBundle:BlogPost p')
->getArrayResult()
;
die(var_dump($posts));
}
```
Now if you follow the url: **http://your_virtual_host/app_dev.php/demo/posts** you
should see a print of posts, this is only an extension demo, we will not create a template.
<a name="more-tips"></a>
## More tips
Regarding, the setup, I do not think it's too complicated to use, in general it is simple
enough, and lets you understand at least small parts on how you can hook mappings into doctrine, and
how easily extension services are added. This configuration does not hide anything behind
curtains and allows you to modify the configuration as you require.
### Multiple entity managers
If you use more than one entity manager, you can simply tag the listener
with other the manager name:
```yaml
services:
# tree behavior
gedmo.listener.tree:
class: Gedmo\Tree\TreeListener
tags:
- { name: doctrine.event_subscriber, connection: default }
# additional ORM subscriber
- { name: doctrine.event_subscriber, connection: other_connection }
# ODM MongoDb subscriber, where **default** is manager name
- { name: doctrine_mongodb.odm.event_subscriber }
calls:
- [ setAnnotationReader, [ @annotation_reader ] ]
```
Regarding, mapping of ODM mongodb, it's basically the same:
```yaml
doctrine_mongodb:
default_database: 'my_database'
default_connection: 'default'
default_document_manager: 'default'
connections:
default: ~
document_managers:
default:
connection: 'default'
auto_mapping: true
mappings:
translatable:
type: annotation
alias: GedmoDocument
prefix: Gedmo\Translatable\Document
# make sure vendor library location is correct
dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Document"
```
This also shows, how to make mappings based on single manager. All what differs is that **Document**
instead of **Entity** is used. I haven't tested it with mongo though.
**Note:** [extension repository](http://github.com/Atlantic18/DoctrineExtensions) contains all
[documentation](http://github.com/Atlantic18/DoctrineExtensions/tree/master/doc) you may need
to understand how you can use it in your projects.
<a name="alternative"></a>
## Alternative over configuration
You can use [StofDoctrineExtensionsBundle](http://github.com/stof/StofDoctrineExtensionsBundle) which is a wrapper of these extensions
## Troubleshooting
- Make sure there are no *.orm.yml or *.orm.xml files for your Entities in your bundles Resources/config/doctrine directory. With those files in place the annotations won't be taken into account.

View File

@@ -0,0 +1,471 @@
# Install Gedmo Doctrine2 extensions in Symfony 4
Configure full featured [Doctrine2 extensions](http://github.com/Atlantic18/DoctrineExtensions) for your symfony 4 project.
This post will show you - how to create a simple configuration file to manage extensions with
ability to use all features it provides.
Interested? then bear with me! and don't be afraid, we're not diving into security component :)
This post will put some light over the shed of extension installation and mapping configuration
of Doctrine2. It does not require any additional dependencies and gives you full power
over management of extensions.
Content:
- [Symfony 4](#sf4-app) application
- Extensions metadata [mapping](#ext-mapping)
- Extensions filters [filtering](#ext-filtering)
- Extension [listeners](#ext-listeners)
- Usage [example](#ext-example)
- Some [tips](#more-tips)
- [Alternative](#alternative) over configuration
<a name="sf4-app"></a>
## Symfony 4 application
First of all, we will need a symfony 4 startup application, let's say [symfony-standard edition
with composer](https://symfony.com/doc/current/best_practices/creating-the-project.html)
- `composer create-project symfony/skeleton [project name]`
Now let's add the **gedmo/doctrine-extensions**
You can find the doctrine-extensions project on packagist: https://packagist.org/packages/gedmo/doctrine-extensions
To add it to your project:
- `composer require gedmo/doctrine-extensions`
<a name="ext-mapping"></a>
## Mapping
Let's start from the mapping. In case you use the **translatable**, **tree** or **loggable**
extension you will need to map those abstract mapped superclasses for your ORM to be aware of.
To do so, add some mapping info to your **doctrine.orm** configuration, edit **config/doctrine.yaml**:
```yaml
doctrine:
dbal:
# your dbal config here
orm:
auto_generate_proxy_classes: %kernel.debug%
auto_mapping: true
# only these lines are added additionally
mappings:
translatable:
type: annotation
alias: Gedmo
prefix: Gedmo\Translatable\Entity
# make sure vendor library location is correct
dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Entity"
```
After that, running **php bin/console doctrine:mapping:info** you should see the output:
```
Found 3 entities mapped in entity manager default:
[OK] Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation
[OK] Gedmo\Translatable\Entity\MappedSuperclass\AbstractTranslation
[OK] Gedmo\Translatable\Entity\Translation
```
Well, we mapped only **translatable** for now, it really depends on your needs, which extensions
your application uses.
**Note:** there is **Gedmo\Translatable\Entity\Translation** which is not a super class, in that case
if you create a doctrine schema, it will add **ext_translations** table, which might not be useful
to you also. To skip mapping of these entities, you can map **only superclasses**
```yaml
mappings:
translatable:
type: annotation
alias: Gedmo
prefix: Gedmo\Translatable\Entity
# make sure vendor library location is correct
dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Entity/MappedSuperclass"
```
The configuration above, adds a **/MappedSuperclass** into directory depth, after running
**php bin/console doctrine:mapping:info** you should only see now:
```
Found 2 entities mapped in entity manager default:
[OK] Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation
[OK] Gedmo\Translatable\Entity\MappedSuperclass\AbstractTranslation
```
This is very useful for advanced requirements and quite simple to understand. So now let's map
everything the extensions provide:
```yaml
# only orm config branch of doctrine
orm:
auto_generate_proxy_classes: %kernel.debug%
auto_mapping: true
# only these lines are added additionally
mappings:
translatable:
type: annotation
alias: Gedmo
prefix: Gedmo\Translatable\Entity
# make sure vendor library location is correct
dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Entity"
loggable:
type: annotation
alias: Gedmo
prefix: Gedmo\Loggable\Entity
dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Loggable/Entity"
tree:
type: annotation
alias: Gedmo
prefix: Gedmo\Tree\Entity
dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Tree/Entity"
```
<a name="ext-filtering"></a>
## Filters
The **softdeleteable** ORM filter also needs to be configured, so that soft deleted records are filtered when querying.
To do so, add this filter info to your **doctrine.orm** configuration, edit **config/doctrine.yaml**:
```yaml
doctrine:
dbal:
# your dbal config here
orm:
auto_generate_proxy_classes: %kernel.debug%
auto_mapping: true
# only these lines are added additionally
filters:
softdeleteable:
class: Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter
```
<a name="ext-listeners"></a>
## Doctrine extension listener services
Next, the heart of extensions are behavioral listeners which pours all the sugar. We will
create a **yaml** service file in our config directory. The setup can be different, your config could be located
in the bundle, it depends on your preferences. Edit **config/packages/doctrine_extensions.yaml**
```yaml
# services to handle doctrine extensions
# import it in config/packages/doctrine_extensions.yaml
services:
# Doctrine Extension listeners to handle behaviors
gedmo.listener.tree:
class: Gedmo\Tree\TreeListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ "@annotation_reader" ] ]
Gedmo\Translatable\TranslatableListener:
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ "@annotation_reader" ] ]
- [ setDefaultLocale, [ %locale% ] ]
- [ setTranslationFallback, [ false ] ]
gedmo.listener.timestampable:
class: Gedmo\Timestampable\TimestampableListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ "@annotation_reader" ] ]
gedmo.listener.sluggable:
class: Gedmo\Sluggable\SluggableListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ "@annotation_reader" ] ]
gedmo.listener.sortable:
class: Gedmo\Sortable\SortableListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ "@annotation_reader" ] ]
gedmo.listener.softdeleteable:
class: Gedmo\SoftDeleteable\SoftDeleteableListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ "@annotation_reader" ] ]
Gedmo\Loggable\LoggableListener:
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ "@annotation_reader" ] ]
Gedmo\Blameable\BlameableListener:
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ "@annotation_reader" ] ]
```
So what does it include in general? Well, it creates services for all extension listeners.
You can remove some which you do not use, or change them as you need. **Translatable** for instance,
sets the default locale to the value of your `%locale%` parameter, you can configure it differently.
**Note:** In case you noticed, there is **EventSubscriber\DoctrineExtensionSubscriber**.
You will need to create this subscriber class if you use **loggable** , **translatable** or **blameable**
behaviors. This listener will set the **locale used** from request and **username** to
loggable and blameable. So, to finish the setup create **EventSubscriber\DoctrineExtensionSubscriber**
```php
<?php
namespace App\EventSubscriber;
use Gedmo\Blameable\BlameableListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class DoctrineExtensionSubscriber implements EventSubscriberInterface
{
/**
* @var BlameableListener
*/
private $blameableListener;
/**
* @var TokenStorageInterface
*/
private $tokenStorage;
/**
* @var TranslatableListener
*/
private $translatableListener;
/**
* @var LoggableListener
*/
private $loggableListener;
public function __construct(
BlameableListener $blameableListener,
TokenStorageInterface $tokenStorage,
TranslatableListener $translatableListener,
LoggableListener $loggableListener
) {
$this->blameableListener = $blameableListener;
$this->tokenStorage = $tokenStorage;
$this->translatableListener = $translatableListener;
$this->loggableListener = $loggableListener;
}
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => 'onKernelRequest',
KernelEvents::FINISH_REQUEST => 'onLateKernelRequest'
];
}
public function onKernelRequest(): void
{
if ($this->tokenStorage !== null &&
$this->tokenStorage->getToken() !== null &&
$this->tokenStorage->getToken()->isAuthenticated() === true
) {
$this->blameableListener->setUserValue($this->tokenStorage->getToken()->getUser());
}
}
public function onLateKernelRequest(FinishRequestEvent $event): void
{
$this->translatableListener->setTranslatableLocale($event->getRequest()->getLocale());
}
}
```
<a name="ext-example"></a>
## Example
After that, you have your extensions set up and ready to be used! Too easy right? Well,
if you do not believe me, let's create a simple entity in our project:
```php
<?php
// file: src/Entity/BlogPost.php
namespace App\Entity;
use Gedmo\Mapping\Annotation as Gedmo; // gedmo annotations
use Doctrine\ORM\Mapping as ORM; // doctrine orm annotations
/**
* @ORM\Entity
* @Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false)
*/
class BlogPost
{
/**
* @Gedmo\Slug(fields={"title"}, updatable=false, separator="_")
* @ORM\Id
* @ORM\Column(length=32, unique=true)
*/
private $id;
/**
* @Gedmo\Translatable
* @ORM\Column(length=64)
*/
private $title;
/**
* @Gedmo\Timestampable(on="create")
* @ORM\Column(name="created", type="datetime")
*/
private $created;
/**
* @ORM\Column(name="updated", type="datetime")
* @Gedmo\Timestampable(on="update")
*/
private $updated;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
private $deletedAt;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function getCreated()
{
return $this->created;
}
public function getUpdated()
{
return $this->updated;
}
public function getDeletedAt(): ?Datetime
{
return $this->deletedAt;
}
public function setDeletedAt(?Datetime $deletedAt): void
{
$this->deletedAt = $deletedAt;
}
}
```
Now, let's have some fun:
- if you have not created the database yet, run `php bin/console doctrine:database:create`
- create the schema `php bin/console doctrine:schema:create`
Everything will work just fine, you can modify the **App\Controller\DemoController**
and add an action to test how it works:
```php
// file: src/Controller/DemoController.php
// include this code portion
/**
* @Route("/posts", name="_demo_posts")
*/
public function postsAction()
{
$em = $this->getDoctrine()->getManager();
$repository = $em->getRepository(App\Entity\BlogPost::class);
// create some posts in case if there aren't any
if (!$repository->find('hello_world')) {
$post = new App\Entity\BlogPost();
$post->setTitle('Hello world');
$next = new App\Entity\BlogPost();
$next->setTitle('Doctrine extensions');
$em->persist($post);
$em->persist($next);
$em->flush();
}
$posts = $repository->findAll();
dd($posts);
}
```
Now if you follow the url: **http://your_virtual_host/demo/posts** you
should see a print of posts, this is only an extension demo, we will not create a template.
<a name="more-tips"></a>
## More tips
Regarding, the setup, I do not think it's too complicated to use, in general it is simple
enough, and lets you understand at least small parts on how you can hook mappings into doctrine, and
how easily extension services are added. This configuration does not hide anything behind
curtains and allows you to modify the configuration as you require.
### Multiple entity managers
If you use more than one entity manager, you can simply tag the subscriber
with other the manager name:
Regarding, mapping of ODM mongodb, it's basically the same:
```yaml
doctrine_mongodb:
default_database: 'my_database'
default_connection: 'default'
default_document_manager: 'default'
connections:
default: ~
document_managers:
default:
connection: 'default'
auto_mapping: true
mappings:
translatable:
type: annotation
alias: GedmoDocument
prefix: Gedmo\Translatable\Document
# make sure vendor library location is correct
dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Document"
```
This also shows, how to make mappings based on single manager. All what differs is that **Document**
instead of **Entity** is used. I haven't tested it with mongo though.
**Note:** [extension repository](http://github.com/Atlantic18/DoctrineExtensions) contains all
[documentation](http://github.com/Atlantic18/DoctrineExtensions/tree/master/doc) you may need
to understand how you can use it in your projects.
<a name="alternative"></a>
## Alternative over configuration
You can use [StofDoctrineExtensionsBundle](http://github.com/stof/StofDoctrineExtensionsBundle) which is a wrapper of these extensions
## Troubleshooting
- Make sure there are no *.orm.yml or *.orm.xml files for your Entities in your bundles Resources/config/doctrine directory. With those files in place the annotations won't be taken into account.

View File

@@ -0,0 +1,674 @@
# Timestampable behavior extension for Doctrine 2
**Timestampable** behavior will automate the update of date fields
on your Entities or Documents. It works through annotations and can update
fields on creation, update, property subset update, or even on specific property value change.
Features:
- Automatic predefined date field update on creation, update, property subset update, and even on record property changes
- ORM and ODM support using same listener
- Specific annotations for properties, and no interface required
- Can react to specific property or relation changes to specific value
- Can be nested with other behaviors
- Annotation, Yaml and Xml mapping support for extensions
Update **2012-06-26**
- Allow multiple values for on="change"
Update **2012-03-10**
- Add [Timestampable traits](#traits)
Update **2011-04-04**
- Made single listener, one instance can be used for any object manager
and any number of them
**Note:**
- Last update date: **2012-01-02**
**Portability:**
- **Timestampable** is now available as [Bundle](http://github.com/stof/StofDoctrineExtensionsBundle)
ported to **Symfony2** by **Christophe Coevoet**, together with all other extensions
This article will cover the basic installation and functionality of **Timestampable** behavior
Content:
- [Including](#including-extension) the extension
- Entity [example](#entity-mapping)
- Document [example](#document-mapping)
- [Yaml](#yaml-mapping) mapping example
- [Xml](#xml-mapping) mapping example
- Advanced usage [examples](#advanced-examples)
- Using [Traits](#traits)
<a name="including-extension"></a>
## Setup and autoloading
Read the [documentation](http://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/annotations.md#em-setup)
or check the [example code](http://github.com/Atlantic18/DoctrineExtensions/tree/master/example)
on how to setup and use the extensions in most optimized way.
<a name="entity-mapping"></a>
## Timestampable Entity example:
### Timestampable annotations:
- **@Gedmo\Mapping\Annotation\Timestampable** this annotation tells that this column is timestampable
by default it updates this column on update. If column is not date, datetime or time
type it will trigger an exception.
Available configuration options:
- **on** - is main option and can be **create, update, change** this tells when it
should be updated
- **field** - only valid if **on="change"** is specified, tracks property or a list of properties for changes
- **value** - only valid if **on="change"** is specified and the tracked field is a single field (not an array), if the tracked field has this **value**
**Note:** that Timestampable interface is not necessary, except in cases where
you need to identify entity as being Timestampable. The metadata is loaded only once then
cache is activated
``` php
<?php
namespace Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Article
{
/** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */
private $id;
/**
* @ORM\Column(type="string", length=128)
*/
private $title;
/**
* @ORM\Column(name="body", type="string")
*/
private $body;
/**
* @var \DateTime $created
*
* @Gedmo\Timestampable(on="create")
* @ORM\Column(type="datetime")
*/
private $created;
/**
* @var \DateTime $updated
*
* @Gedmo\Timestampable(on="update")
* @ORM\Column(type="datetime")
*/
private $updated;
/**
* @var \DateTime $contentChanged
*
* @ORM\Column(name="content_changed", type="datetime", nullable=true)
* @Gedmo\Timestampable(on="change", field={"title", "body"})
*/
private $contentChanged;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setBody($body)
{
$this->body = $body;
}
public function getBody()
{
return $this->body;
}
public function getCreated()
{
return $this->created;
}
public function getUpdated()
{
return $this->updated;
}
public function getContentChanged()
{
return $this->contentChanged;
}
}
```
<a name="document-mapping"></a>
## Timestampable Document example:
``` php
<?php
namespace Document;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
/**
* @ODM\Document(collection="articles")
*/
class Article
{
/** @ODM\Id */
private $id;
/**
* @ODM\Field(type="string")
*/
private $title;
/**
* @ODM\Field(type="string")
*/
private $body;
/**
* @var date $created
*
* @ODM\Date
* @Gedmo\Timestampable(on="create")
*/
private $created;
/**
* @var date $updated
*
* @ODM\Date
* @Gedmo\Timestampable
*/
private $updated;
/**
* @var \DateTime $contentChanged
*
* @ODM\Date
* @Gedmo\Timestampable(on="change", field={"title", "body"})
*/
private $contentChanged;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setBody($body)
{
$this->body = $body;
}
public function getBody()
{
return $this->body;
}
public function getCreated()
{
return $this->created;
}
public function getUpdated()
{
return $this->updated;
}
public function getContentChanged()
{
return $this->contentChanged;
}
}
```
Now on update and creation these annotated fields will be automatically updated
<a name="yaml-mapping"></a>
## Yaml mapping example:
Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml**
```yaml
---
Entity\Article:
type: entity
table: articles
id:
id:
type: integer
generator:
strategy: AUTO
fields:
title:
type: string
length: 64
created:
type: date
gedmo:
timestampable:
on: create
updated:
type: datetime
gedmo:
timestampable:
on: update
```
<a name="xml-mapping"></a>
## Xml mapping example
``` xml
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">
<entity name="Mapping\Fixture\Xml\Timestampable" table="timestampables">
<id name="id" type="integer" column="id">
<generator strategy="AUTO"/>
</id>
<field name="created" type="datetime">
<gedmo:timestampable on="create"/>
</field>
<field name="updated" type="datetime">
<gedmo:timestampable on="update"/>
</field>
<field name="published" type="datetime" nullable="true">
<gedmo:timestampable on="change" field="status.title" value="Published"/>
</field>
<many-to-one field="status" target-entity="Status">
<join-column name="status_id" referenced-column-name="id"/>
</many-to-one>
</entity>
</doctrine-mapping>
```
<a name="advanced-examples"></a>
## Advanced examples:
### Using dependency of property changes
Add another entity which would represent Article Type:
``` php
<?php
namespace Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Type
{
/** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */
private $id;
/**
* @ORM\Column(type="string", length=128)
*/
private $title;
/**
* @ORM\OneToMany(targetEntity="Article", mappedBy="type")
*/
private $articles;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
}
```
Now update the Article Entity to reflect published date on Type change:
``` php
<?php
namespace Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Article
{
/** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */
private $id;
/**
* @ORM\Column(type="string", length=128)
*/
private $title;
/**
* @var \DateTime $created
*
* @Gedmo\Timestampable(on="create")
* @ORM\Column(type="datetime")
*/
private $created;
/**
* @var \DateTime $updated
*
* @Gedmo\Timestampable(on="update")
* @ORM\Column(type="datetime")
*/
private $updated;
/**
* @ORM\ManyToOne(targetEntity="Type", inversedBy="articles")
*/
private $type;
/**
* @var \DateTime $published
*
* @ORM\Column(type="datetime", nullable=true)
* @Gedmo\Timestampable(on="change", field="type.title", value="Published")
*
* or for example
* @Gedmo\Timestampable(on="change", field="type.title", value={"Published", "Closed"})
*/
private $published;
public function setType($type)
{
$this->type = $type;
}
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function getCreated()
{
return $this->created;
}
public function getUpdated()
{
return $this->updated;
}
public function getPublished()
{
return $this->published;
}
}
```
Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml**
``` yaml
---
Entity\Article:
type: entity
table: articles
id:
id:
type: integer
generator:
strategy: AUTO
fields:
title:
type: string
length: 64
created:
type: date
gedmo:
timestampable:
on: create
updated:
type: datetime
gedmo:
timestampable:
on: update
published:
type: datetime
gedmo:
timestampable:
on: change
field: type.title
value: Published
manyToOne:
type:
targetEntity: Entity\Type
inversedBy: articles
```
Now few operations to get it all done:
``` php
<?php
$article = new Article;
$article->setTitle('My Article');
$em->persist($article);
$em->flush();
// article: $created, $updated were set
$type = new Type;
$type->setTitle('Published');
$article = $em->getRepository('Entity\Article')->findByTitle('My Article');
$article->setType($type);
$em->persist($article);
$em->persist($type);
$em->flush();
// article: $published, $updated were set
$article->getPublished()->format('Y-m-d'); // the date article type changed to published
```
Easy like that, any suggestions on improvements are very welcome
### Creating a UTC DateTime type that stores your datetimes in UTC
First, we define our custom data type (note the type name is datetime and the type extends DateTimeType which simply overrides the default Doctrine type):
``` php
<?php
namespace Acme\DoctrineExtensions\DBAL\Types;
use Doctrine\DBAL\Types\DateTimeType;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
class UTCDateTimeType extends DateTimeType
{
static private $utc = null;
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if ($value === null) {
return null;
}
if (is_null(self::$utc)) {
self::$utc = new \DateTimeZone('UTC');
}
$value->setTimeZone(self::$utc);
return $value->format($platform->getDateTimeFormatString());
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
if ($value === null) {
return null;
}
if (is_null(self::$utc)) {
self::$utc = new \DateTimeZone('UTC');
}
$val = \DateTime::createFromFormat($platform->getDateTimeFormatString(), $value, self::$utc);
if (!$val) {
throw ConversionException::conversionFailed($value, $this->getName());
}
return $val;
}
}
```
Now in Symfony2, we register and override the **datetime** type. **WARNING:** this will override the **datetime** type for all your entities and for all entities in external bundles or extensions, so if you have some entities that require the standard **datetime** type from Doctrine, you must modify the above type and use a different name (such as **utcdatetime**). Additionally, you'll need to modify **Timestampable** so that it includes **utcdatetime** as a valid type.
``` yaml
doctrine:
dbal:
types:
datetime: Acme\DoctrineExtensions\DBAL\Types\UTCDateTimeType
```
And our Entity properties look as expected:
``` php
<?php
/**
* @var \DateTime $dateCreated
*
* @ORM\Column(name="date_created", type="datetime")
* @Gedmo\Timestampable(on="create")
*/
private $dateCreated;
/**
* @var \DateTime $dateLastModified
*
* @Gedmo\Timestampable(on="update")
* @ORM\Column(name="date_last_modified", type="datetime")
*/
private $dateLastModified;
```
Now, in our view (suppose we are using Symfony2 and Twig), we can display the datetime (which is persisted in UTC format) in our user's time zone:
``` twig
{{ myEntity.dateCreated | date("d/m/Y g:i a", app.user.timezone) }}
```
Or if the user does not have a timezone, we could expand that to use a system/app/PHP default timezone.
[doctrine_custom_datetime_type]: http://www.doctrine-project.org/docs/orm/2.0/en/cookbook/working-with-datetime.html#handling-different-timezones-with-the-datetime-type "Handling different Timezones with the DateTime Type"
This example is based off [Handling different Timezones with the DateTime Type][doctrine_custom_datetime_type] - however that example may be outdated because it contains some obviously invalid PHP from the TimeZone class.
<a name="traits"></a>
## Traits
You can use timestampable traits for quick **createdAt** **updatedAt** timestamp definitions
when using annotation mapping.
There is also a trait without annotations for easy integration purposes.
**Note:** this feature is only available since php **5.4.0**. And you are not required
to use the Traits provided by extensions.
``` php
<?php
namespace Timestampable\Fixture;
use Gedmo\Timestampable\Traits\TimestampableEntity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class UsingTrait
{
/**
* Hook timestampable behavior
* updates createdAt, updatedAt fields
*/
use TimestampableEntity;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(length=128)
*/
private $title;
}
```
Traits are very simple and if you use different field names I recommend to simply create your
own ones based per project. These ones are standing as an example.

View File

@@ -0,0 +1,234 @@
### Making safe transactions in concurrent environment
Some extensions are using atomic updates, which need some extra attention
in order to maintain data integrity. If you are using one of these extensions
listed below, you should read further and take the appropriate actions:
- Sortable
- Tree - NestedSet strategy
- Tree - MaterializedPath strategy
So let me explain first, why and what actions are needed to be applied to
maintain your data integrity.
Imagine two concurrent requests are being issued with some entity updates which does
some actions for one or more of these extensions listed. One request starts a transaction
to do atomic updates and another at the same time, while the first transaction executes
starts the second transaction. The second transaction might be performing updates based
on data which is outdated or even still running in the first transaction. The possibility
to have broken data increases with concurrency.
We need to lock one transaction so the other would wait until the first finishes and then we can
begin the second one which in turn would lock the third one if there would be any.
**NOTE:** it is not enough to simply have a transaction.
So how we can achieve this? The simplest solution is [pessimistic locking](http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/transactions-and-concurrency.html#pessimistic-locking) which is supported by ORM.
So how we can use it correctly to maintain our transactions safe from one another. Lets say we have two entity types in
our application:
- **Shop** - lets say our ecommerce platform we are creating, supports multiple shops.
- **Category** - every shop might have a different category set for products and other features.
So the **Category** should be a **nested set tree** strategy based, where atomic updates might be executed
if a category is being moved, inserted or removed.
To start with, I'll make the simple definitions of these two entities:
``` php
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Shop
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*/
private $id;
/**
* @ORM\Column(length=64)
*/
private $name;
public function setName($name)
{
$this->name = $name;
return $this;
}
public function getName()
{
return $this->name;
}
}
```
It should have owner and so many more attributes, but lets keep it simple. Here follows Category:
``` php
<?php
namespace App\Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @Gedmo\Tree(type="nested")
* @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository")
*/
class Category
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*/
private $id;
/**
* @ORM\Column(length=64)
*/
private $title;
/**
* @Gedmo\TreeLeft
* @ORM\Column(type="integer")
*/
private $lft;
/**
* @Gedmo\TreeRight
* @ORM\Column(type="integer")
*/
private $rgt;
/**
* @Gedmo\TreeParent
* @ORM\ManyToOne(targetEntity="Category")
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE")
*/
private $parent;
/**
* @ORM\ManyToOne(targetEntity="Shop")
*/
private $shop;
/**
* @Gedmo\TreeRoot
* @ORM\Column(type="integer", nullable=true)
*/
private $root;
/**
* @Gedmo\TreeLevel
* @ORM\Column(name="lvl", type="integer")
*/
private $level;
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setParent(Category $parent = null)
{
$this->parent = $parent;
}
public function getParent()
{
return $this->parent;
}
public function getLevel()
{
return $this->level;
}
public function setShop(Shop $shop)
{
$this->shop = $shop;
return $this;
}
public function getShop()
{
return $this->shop;
}
}
```
**NOTE:** it would be perfect if we could use tree root as a shop relation. But it is not currently supported and
might be available only in next versions.
Now everytime we do **insert**, **move** or **remove** actions for Category:
``` php
<?php
use Doctrine\DBAL\LockMode;
class CategoryController extends Controller
{
function postCategoryAction($currentShopId)
{
$em = $this->getEntityManager();
$conn = $em->getConnection();
$categoryRepository = $em->getRepository("App\Entity\Category");
// start transaction
$conn->beginTransaction();
try {
// select shop for update - locks it for any read attempts until this transaction ends
$shop = $em->find("App\Entity\Shop", $currentShopId, LockMode::PESSIMISTIC_WRITE);
// create a new category
$category = new Category;
$category->setTitle($_POST["title"]);
$category->setShop($shop);
$parent = $categoryRepository->findOneById($_POST["parent_id"]);
// persist and flush
$categoryRepository->persistAsFirstChildOf($category, $parent);
$em->flush();
$conn->commit();
} catch (Exception $e) {
$conn->rollback();
throw $e;
}
// if all went well, we can set flash message or whatever
// other operations which attempts to select in lock mode, will wait till this transaction ends.
}
}
```
You may separate locking transaction to run callback function and make it as a service to abstract and prevent
code duplication. Anyway, my advice would be to use only one transaction per request and best inside controller
directly, where you would ensure that all operations performed during the action can be safely rolled back.
Also to use this kind of locking, you need an entity which is necessary to read on concurrent request which attempts
to update the same tree. In this example, **Shop** entity fits the bill perfectly. Otherwise you need to find a way to
safely lock the tree table.
The point of this example is: that concurrently atomic updates, might cause other parallel actions to use outdated
information, based on which it may perform falsely calculated consequent updates. And you need to prevent this from
happening in order to maintain your data. Extensions and ORM cannot perform such actions automatically.

View File

@@ -0,0 +1,894 @@
# Translatable behavior extension for Doctrine 2
**Translatable** behavior offers a very handy solution for translating specific record fields
in different languages. Further more, it loads the translations automatically for a locale
currently used, which can be set to **Translatable Listener** on it`s initialization or later
for other cases through the **Entity** itself
Features:
- Automatic storage of translations in database
- ORM and ODM support using same listener
- Automatic translation of Entity or Document fields when loaded
- ORM query can use **hint** to translate all records without issuing additional queries
- Can be nested with other behaviors
- Annotation, Yaml and Xml mapping support for extensions
**2012-01-28**
- Created personal translation which maps through real foreign key
constraint. This dramatically improves the management of translations
**2012-01-04**
- Refactored translatable to be able to persist, update many translations
using repository, [issue #224](https://github.com/Atlantic18/DoctrineExtensions/issues/224)
**2011-12-11**
- Added more useful translation query hints: Override translatable locale, inner join
translations instead left join, override translation fallback
**2011-11-08**
- Thanks to [@acasademont](https://github.com/acasademont) Translatable now does not store translations for default locale. It is always left as original record value.
So be sure you do not change your default locale per project or per data migration. This way
it is more rational and unnecessary to store it additionally in translation table.
Update **2011-04-21**
- Implemented multiple translation persistence through repository
Update **2011-04-16**
- Made an ORM query **hint** to hook into any select type query, which will join the translations
and let you **filter, order or search** by translated fields directly. It also will translate
all selected **collections or simple components** without issuing additional queries. It also
supports translation fallbacks
- For performance reasons, translation fallbacks are disabled by default
Update **2011-04-04**
- Made single listener, one instance can be used for any object manager
and any number of them
**Note list:**
- Public [Translatable repository](http://github.com/Atlantic18/DoctrineExtensions "Translatable extension on Github") is available on github
- Using other extensions on the same Entity fields may result in unexpected way
- May impact your application performance since it does an additional query for translation if loaded without query hint
- Last update date: **2012-02-15**
**Portability:**
- **Translatable** is now available as [Bundle](http://github.com/stof/StofDoctrineExtensionsBundle)
ported to **Symfony2** by **Christophe Coevoet**, together with all other extensions
This article will cover the basic installation and functionality of **Translatable** behavior
Content:
- [Including](#including-extension) the extension
- Entity [example](#entity-domain-object)
- Document [example](#document-domain-object)
- [Yaml](#yaml-mapping) mapping example
- [Xml](#xml-mapping) mapping example
- Basic usage [examples](#basic-examples)
- [Persisting](#multi-translations) multiple translations
- Using ORM query [hint](#orm-query-hint)
- Advanced usage [examples](#advanced-examples)
- Personal [translations](#personal-translations)
<a name="including-extension"></a>
## Setup and autoloading
Read the [documentation](http://github.com/Atlantic18/DoctrineExtensions/blob/master/doc/annotations.md#em-setup)
or check the [example code](http://github.com/Atlantic18/DoctrineExtensions/tree/master/example)
on how to setup and use the extensions in most optimized way.
### Translatable annotations:
- **@Gedmo\Mapping\Annotation\Translatable** it will **translate** this field
- **@Gedmo\Mapping\Annotation\TranslationEntity(class="my\class")** it will use this class to store **translations** generated
- **@Gedmo\Mapping\Annotation\Locale or @Gedmo\Mapping\Annotation\Language** this will identify this column as **locale** or **language**
used to override the global locale
<a name="entity-domain-object"></a>
## Translatable Entity example:
**Note:** that Translatable interface is not necessary, except in cases where
you need to identify an entity as being Translatable. The metadata is loaded only once when
cache is activated
``` php
<?php
namespace Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Translatable\Translatable;
/**
* @ORM\Table(name="articles")
* @ORM\Entity
*/
class Article implements Translatable
{
/** @ORM\Id @ORM\GeneratedValue @ORM\Column(type="integer") */
private $id;
/**
* @Gedmo\Translatable
* @ORM\Column(name="title", type="string", length=128)
*/
private $title;
/**
* @Gedmo\Translatable
* @ORM\Column(name="content", type="text")
*/
private $content;
/**
* @Gedmo\Locale
* Used locale to override Translation listener`s locale
* this is not a mapped field of entity metadata, just a simple property
*/
private $locale;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setContent($content)
{
$this->content = $content;
}
public function getContent()
{
return $this->content;
}
public function setTranslatableLocale($locale)
{
$this->locale = $locale;
}
}
```
<a name="document-domain-object"></a>
## Translatable Document example:
``` php
<?php
namespace Document;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use Gedmo\Translatable\Translatable;
/**
* @ODM\Document(collection="articles")
*/
class Article implements Translatable
{
/** @ODM\Id */
private $id;
/**
* @Gedmo\Translatable
* @ODM\Field(type="string")
*/
private $title;
/**
* @Gedmo\Translatable
* @ODM\Field(type="string")
*/
private $content;
/**
* @Gedmo\Locale
* Used locale to override Translation listener`s locale
* this is not a mapped field of entity metadata, just a simple property
*/
private $locale;
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setContent($content)
{
$this->content = $content;
}
public function getContent()
{
return $this->content;
}
public function setTranslatableLocale($locale)
{
$this->locale = $locale;
}
}
```
<a name="yaml-mapping"></a>
## Yaml mapping example
Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml**
```
---
Entity\Article:
type: entity
table: articles
gedmo:
translation:
locale: localeField
# using specific personal translation class:
# entity: Translatable\Fixture\CategoryTranslation
id:
id:
type: integer
generator:
strategy: AUTO
fields:
title:
type: string
length: 64
gedmo:
- translatable
content:
type: text
gedmo:
- translatable
```
<a name="xml-mapping"></a>
## Xml mapping example
``` xml
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">
<entity name="Mapping\Fixture\Xml\Translatable" table="translatables">
<id name="id" type="integer" column="id">
<generator strategy="AUTO"/>
</id>
<field name="title" type="string" length="128">
<gedmo:translatable/>
</field>
<field name="content" type="text">
<gedmo:translatable/>
</field>
<gedmo:translation entity="Gedmo\Translatable\Entity\Translation" locale="locale"/>
</entity>
</doctrine-mapping>
```
<a name="basic-examples"></a>
## Basic usage examples:
Currently a global locale used for translations is "en_us" which was
set in **TranslationListener** globally. To save article with its translations:
``` php
<?php
$article = new Entity\Article;
$article->setTitle('my title in en');
$article->setContent('my content in en');
$em->persist($article);
$em->flush();
```
This inserted an article and inserted the translations for it in "en_us" locale
only if **en_us** is not the [default locale](#advanced-examples) in case if default locale
matches current locale - it uses original record value as translation
Now lets update our article in different locale:
``` php
<?php
// first load the article
$article = $em->find('Entity\Article', 1 /*article id*/);
$article->setTitle('my title in de');
$article->setContent('my content in de');
$article->setTranslatableLocale('de_de'); // change locale
$em->persist($article);
$em->flush();
```
This updated an article and inserted the translations for it in "de_de" locale
To see and load all translations of **Translatable** Entity:
``` php
<?php
// reload in different language
$article = $em->find('Entity\Article', 1 /*article id*/);
$article->setLocale('ru_ru');
$em->refresh($article);
$article = $em->find('Entity\Article', 1 /*article id*/);
$repository = $em->getRepository('Gedmo\Translatable\Entity\Translation');
$translations = $repository->findTranslations($article);
/* $translations contains:
Array (
[de_de] => Array
(
[title] => my title in de
[content] => my content in de
)
[en_us] => Array
(
[title] => my title in en
[content] => my content in en
)
)*/
```
As far as our global locale is now "en_us" and updated article has "de_de" values.
Lets try to load it and it should be translated in English
``` php
<?php
$article = $em->getRepository('Entity\Article')->find(1/* id of article */);
echo $article->getTitle();
// prints: "my title in en"
echo $article->getContent();
// prints: "my content in en"
```
<a name="multi-translations"></a>
## Persisting multiple translations
Usually it is more convenient to persist more translations when creating
or updating a record. **Translatable** allows to do that through translation repository.
All additional translations will be tracked by listener and when the flush will be executed,
it will update or persist all additional translations.
**Note:** these translations will not be processed as ordinary fields of your object,
in case if you translate a **slug** additional translation will not know how to generate
the slug, so the value as an additional translation should be processed when creating it.
### Example of multiple translations:
``` php
<?php
// persisting multiple translations, assume default locale is EN
$repository = $em->getRepository('Gedmo\\Translatable\\Entity\\Translation');
// it works for ODM also
$article = new Article;
$article->setTitle('My article en');
$article->setContent('content en');
$repository->translate($article, 'title', 'de', 'my article de')
->translate($article, 'content', 'de', 'content de')
->translate($article, 'title', 'ru', 'my article ru')
->translate($article, 'content', 'ru', 'content ru')
;
$em->persist($article);
$em->flush();
// updating same article also having one new translation
$repo
->translate($article, 'title', 'lt', 'title lt')
->translate($article, 'content', 'lt', 'content lt')
->translate($article, 'title', 'ru', 'title ru change')
->translate($article, 'content', 'ru', 'content ru change')
->translate($article, 'title', 'en', 'title en (default locale) update')
->translate($article, 'content', 'en', 'content en (default locale) update')
;
$em->flush();
```
<a name="orm-query-hint"></a>
## Using ORM query hint
By default, behind the scenes, when you load a record - translatable hooks into **postLoad**
event and issues additional query to translate all fields. Imagine that, when you load a collection,
it may issue a lot of queries just to translate those fields. Including array hydration,
it is not possible to hook any **postLoad** event since it is not an
entity being hydrated. These are the main reasons why **TranslationWalker** was created.
**TranslationWalker** uses a query **hint** to hook into any **select type query**,
and when you execute the query, no matter which hydration method you use, it automatically
joins the translations for all fields, so you could use ordering filtering or whatever you
want on **translated fields** instead of original record fields.
And in result there is only one query for all this happiness.
If you use translation [fallbacks](#advanced-examples) it will be also in the same single
query and during the hydration process it will replace the empty fields in case if they
do not have a translation in currently used locale.
Now enough talking, here is an example:
``` php
<?php
$dql = <<<___SQL
SELECT a, c, u
FROM Article a
LEFT JOIN a.comments c
JOIN c.author u
WHERE a.title LIKE '%translated_title%'
ORDER BY a.title
___SQL;
$query = $em->createQuery($dql);
// set the translation query hint
$query->setHint(
\Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER,
'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'
);
$articles = $query->getResult(); // object hydration
$articles = $query->getArrayResult(); // array hydration
```
And even a subselect:
``` php
<?php
$dql = <<<___SQL
SELECT a, c, u
FROM Article a
LEFT JOIN a.comments c
JOIN c.author u
WHERE a.id IN (
SELECT a2.id
FROM Article a2
WHERE a2.title LIKE '%something_translated%'
AND a2.status = 1
)
ORDER BY a.title
___SQL;
$query = $em->createQuery($dql);
$query->setHint(
\Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER,
'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'
);
```
**NOTE:** if you use memcache or apc. You should set locale and other options like fallbacks
to query through hints. Otherwise the query will be cached with a first used locale
``` php
<?php
// locale
$query->setHint(
\Gedmo\Translatable\TranslatableListener::HINT_TRANSLATABLE_LOCALE,
'en' // take locale from session or request etc.
);
// fallback
$query->setHint(
\Gedmo\Translatable\TranslatableListener::HINT_FALLBACK,
1 // fallback to default values in case if record is not translated
);
$articles = $query->getResult(); // object hydration
```
There's no need for any words anymore.. right?
I recommend you to use it extensively since it is a way better performance, even in
cases where you need to load single translated entity.
**Note**: Even in **COUNT** select statements translations are joined to leave a
possibility to filter by translated field, if you do not need it, just do not set
the **hint**. Also take into account that it is not possible to translate components
in **JOIN WITH** statement, example
```
JOIN a.comments c WITH c.message LIKE '%will_not_be_translated%'`
```
**Note**: any **find** related method calls cannot hook this hint automagically, we
will use a different approach when **persister overriding feature** will be
available in **Doctrine**
In case if **translation query walker** is used, you can additionally override:
### Overriding translation fallback
``` php
<?php
$query->setHint(\Gedmo\Translatable\TranslatableListener::HINT_FALLBACK, 1);
```
will fallback to default locale translations instead of empty values if used.
And will override the translation listener setting for fallback.
``` php
<?php
$query->setHint(\Gedmo\Translatable\TranslatableListener::HINT_FALLBACK, 0);
```
will do the opposite.
### Using inner join strategy
``` php
<?php
$query->setHint(\Gedmo\Translatable\TranslatableListener::HINT_INNER_JOIN, true);
```
will use **INNER** joins
for translations instead of **LEFT** joins, so that in case if you do not want untranslated
records in your result set for instance.
### Overriding translatable locale
``` php
<?php
$query->setHint(\Gedmo\Translatable\TranslatableListener::HINT_TRANSLATABLE_LOCALE, 'en');
```
would override the translation locale used to translate the resultset.
**Note:** all these query hints lasts only for the specific query.
<a name="advanced-examples"></a>
## Advanced examples:
### Default locale
In some cases we need a default translation as a fallback if record does not have
a translation on globally used locale. In that case Translation Listener takes the
current value of Entity. So if **default locale** is specified and it matches the
locale in which record is being translated - it will not create extra translation
but use original values instead. If translation fallback is set to **false** it
will fill untranslated values as blanks
To set the default locale:
``` php
<?php
$translatableListener->setDefaultLocale('en_us');
```
To set translation fallback:
``` php
<?php
$translatableListener->setTranslationFallback(true); // default is false
```
**Note**: Default locale should be set on the **TranslatableListener** initialization
once, since it can impact your current records if it will be changed. As it
will not store extra record in translation table by default.
If you need to store translation in default locale, set:
``` php
<?php
$translatableListener->setPersistDefaultLocaleTranslation(true); // default is false
```
This would always store translations in all locales, also keeping original record
translated field values in default locale set.
### Translation Entity
In some cases if there are thousands of records or even more.. we would like to
have a single table for translations of this Entity in order to increase the performance
on translation loading speed. This example will show how to specify a different Entity for
your translations by extending the mapped superclass.
ArticleTranslation Entity:
``` php
<?php
namespace Entity\Translation;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Translatable\Entity\MappedSuperclass\AbstractTranslation;
/**
* @ORM\Table(name="article_translations", indexes={
* @ORM\Index(name="article_translation_idx", columns={"locale", "object_class", "field", "foreign_key"})
* })
* @ORM\Entity(repositoryClass="Gedmo\Translatable\Entity\Repository\TranslationRepository")
*/
class ArticleTranslation extends AbstractTranslation
{
/**
* All required columns are mapped through inherited superclass
*/
}
```
**Note:** We specified the repository class to be used from extension.
It is handy for specific methods common to the Translation Entity
**Note:** This Entity will be used instead of default Translation Entity
only if we specify a class annotation @Gedmo\TranslationEntity(class="my\translation\entity"):
``` php
<?php
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table(name="articles")
* @ORM\Entity
* @Gedmo\TranslationEntity(class="Entity\Translation\ArticleTranslation")
*/
class Article
{
// ...
}
```
Now all translations of Article will be stored and queried from specific table
<a name="personal-translations"></a>
## Personal translations
Translatable has **AbstractPersonalTranslation** mapped superclass, which must
be extended and mapped based on your **entity** which you want to translate.
Note: translations are not automapped because of user preference based on cascades
or other possible choices, which user can make.
Personal translations uses foreign key constraint which is fully managed by ORM and
allows to have a collection of related translations. User can use it anyway he likes, etc.:
implementing array access on entity, using left join to fill collection and so on.
Note: that [query hint](#orm-query-hint) will work on personal translations the same way.
You can always use a left join like for standard doctrine collections.
Usage example:
``` php
<?php
namespace Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @Gedmo\TranslationEntity(class="Entity\CategoryTranslation")
*/
class Category
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*/
private $id;
/**
* @Gedmo\Translatable
* @ORM\Column(length=64)
*/
private $title;
/**
* @Gedmo\Translatable
* @ORM\Column(type="text", nullable=true)
*/
private $description;
/**
* @ORM\OneToMany(
* targetEntity="CategoryTranslation",
* mappedBy="object",
* cascade={"persist", "remove"}
* )
*/
private $translations;
public function __construct()
{
$this->translations = new ArrayCollection();
}
public function getTranslations()
{
return $this->translations;
}
public function addTranslation(CategoryTranslation $t)
{
if (!$this->translations->contains($t)) {
$this->translations[] = $t;
$t->setObject($this);
}
}
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setDescription($description)
{
$this->description = $description;
}
public function getDescription()
{
return $this->description;
}
public function __toString()
{
return $this->getTitle();
}
}
```
Now the translation entity for the Category:
``` php
<?php
namespace Entity;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation;
/**
* @ORM\Entity
* @ORM\Table(name="category_translations",
* uniqueConstraints={@ORM\UniqueConstraint(name="lookup_unique_idx", columns={
* "locale", "object_id", "field"
* })}
* )
*/
class CategoryTranslation extends AbstractPersonalTranslation
{
/**
* Convenient constructor
*
* @param string $locale
* @param string $field
* @param string $value
*/
public function __construct($locale, $field, $value)
{
$this->setLocale($locale);
$this->setField($field);
$this->setContent($value);
}
/**
* @ORM\ManyToOne(targetEntity="Category", inversedBy="translations")
* @ORM\JoinColumn(name="object_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $object;
}
```
Some example code to persist with translations:
``` php
<?php
// assumes default locale is "en"
$food = new Entity\Category;
$food->setTitle('Food');
$food->addTranslation(new Entity\CategoryTranslation('lt', 'title', 'Maistas'));
$fruits = new Entity\Category;
$fruits->setParent($food);
$fruits->setTitle('Fruits');
$fruits->addTranslation(new Entity\CategoryTranslation('lt', 'title', 'Vaisiai'));
$fruits->addTranslation(new Entity\CategoryTranslation('ru', 'title', 'rus trans'));
$em->persist($food);
$em->persist($fruits);
$em->flush();
```
This would create translations for english and lithuanian, and for fruits, **ru** additionally.
Easy like that, any suggestions on improvements are very welcome
### Example code to use Personal Translations with (Symfony2 Sonata) i18n Forms:
Suppose you have a Sonata Backend with a simple form like:
``` php
<?php
protected function configureFormFields(FormMapper $formMapper) {
$formMapper
->with('General')
->add('title', 'text')
->end()
;
}
```
Then you can turn it into an i18n Form by providing the following changes.
``` php
<?php
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->with('General')
->add('title', 'translatable_field', array(
'field' => 'title',
'personal_translation' => 'ExampleBundle\Entity\Translation\ProductTranslation',
'property_path' => 'translations',
))
->end()
;
}
```
To accomplish this you can add the following code in your bundle:
https://gist.github.com/2437078
<Bundle>/Form/TranslatedFieldType.php
<Bundle>/Form/EventListener/addTranslatedFieldSubscriber.php
<Bundle>/Resources/services.yml
Then you can change to your needs:
``` php
'field' => 'title', //you need to provide which field you wish to translate
'personal_translation' => 'ExampleBundle\Entity\Translation\ProductTranslation', //the personal translation entity
```
### Translations field type using Personal Translations with Symfony2:
You can use [A2lixTranslationFormBundle](https://github.com/a2lix/TranslationFormBundle) to facilitate your translations.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,467 @@
# Uploadable behavior extension for Doctrine 2
**Uploadable** behavior provides the tools to manage the persistence of files with
Doctrine 2, including automatic handling of moving, renaming and removal of files and other features.
Features:
- Extension moves, removes and renames files according to configuration automatically
- Lots of options: Allow overwrite, append a number if file exists, filename generators, post-move callbacks, etc.
- It can be extended to work not only with uploaded files, but with files coming from any source (an URL, another
file in the same server, etc).
- Validation of size and mime type
Content:
- [Including](#including-extension) the extension
- Entity [example](#entity-mapping)
- [Yaml](#yaml-mapping) mapping example
- [Xml](#xml-mapping) mapping example
- Usage [examples](#usage)
- [Using](#additional-usages) the extension to handle not only uploaded files
- [Custom](#custom-mime-type-guessers) mime type guessers
<a name="including-extension"></a>
## Setup and autoloading
Read the [documentation](http://github.com/Atlantic18/DoctrineExtensions/blob/v2.4.x/doc/annotations.md#em-setup)
or check the [example code](http://github.com/Atlantic18/DoctrineExtensions/tree/v2.4.x/example)
on how to setup and use the extensions in most optimized way.
<a name="entity-mapping"></a>
## Uploadable Entity example:
### Uploadable annotations:
1. **@Gedmo\Mapping\Annotation\Uploadable** this class annotation tells if a class is Uploadable. Available configuration options:
* **allowOverwrite** - If this option is true, it will overwrite a file if it already exists. If you set "false", an
exception will be thrown. Default: false
* **appendNumber** - If this option is true and "allowOverwrite" is false, in the case that the file already exists,
it will append a number to the filename. Example: if you're uploading a file named "test.txt", if the file already
exists and this option is true, the extension will modify the name of the uploaded file to "test-1.txt", where "1"
could be any number. The extension will check if the file exists until it finds a filename with a number as its postfix that is not used.
If you use a filename generator and this option is true, it will append a number to the filename anyway if a file with
the same name already exists.
Default value: false
* **path** - This option expects a string containing the path where the files represented by this entity will be moved.
Default: "". Path can be set in other ways: From the listener or from a method. More details later.
* **pathMethod** - Similar to option "path", but this time it represents the name of a method on the entity that
will return the path to which the files represented by this entity will be moved. This is useful in several cases.
For example, you can set specific paths for specific entities, or you can get the path from other sources (like a
framework configuration) instead of hardcoding it in the entity. Default: "". As first argument this method takes
default path, so you can return path relative to default.
* **callback** - This option allows you to set a method name. If this option is set, the method will be called after
the file is moved. Default value: "". As first argument, this method can receive an array with information about the uploaded file, which
includes the following keys:
1. **fileName**: The filename.
2. **fileExtension**: The extension of the file (including the dot). Example: .jpg
3. **fileWithoutExt**: The filename without the extension.
4. **filePath**: The file path. Example: /my/path/filename.jpg
5. **fileMimeType**: The mime-type of the file. Example: text/plain.
6. **fileSize**: Size of the file in bytes. Example: 140000.
* **filenameGenerator**: This option allows you to set a filename generator for the file. There are two already included
by the extension: **SHA1**, which generates a sha1 filename for the file, and **ALPHANUMERIC**, which "normalizes"
the filename, leaving only alphanumeric characters in the filename, and replacing anything else with a "-". You can
even create your own FilenameGenerator class (implementing the Gedmo\Uploadable\FilenameGenerator\FilenameGeneratorInterface) and set this option with the
fully qualified class name. The other option available is "NONE" which, as you may guess, means no generation for the
filename will occur. Default: "NONE".
* **maxSize**: This option allows you to set a maximum size for the file in bytes. If file size exceeds the value
set in this configuration, an exception of type "UploadableMaxSizeException" will be thrown. By default, its value is set to 0, meaning
that no size validation will occur.
* **allowedTypes**: With this option you can set a comma-separated list of allowed mime types for the file. The extension
will use a simple mime type guesser to guess the file type, and then it will compare it to the list of allowed types.
If the mime type is not valid, then an exception of type "UploadableInvalidMimeTypeException" will be thrown. If you
set this option, you can't set the **disallowedTypes** option described next. By default, no validation of mime type
occurs. If you want to use a custom mime type guesser, see [this](#custom-mime-type-guessers).
* **disallowedTypes**: Similar to the option **allowedTypes**, but with this one you configure a "black list" of
mime types. If the mime type of the file is on this list, n exception of type "UploadableInvalidMimeTypeException" will be thrown. If you
set this option, you can't set the **allowedTypes** option described above. By default, no validation of mime type
occurs. If you want to use a custom mime type guesser, see [this](#custom-mime-type-guessers).
2. **@Gedmo\Mapping\Annotation\UploadableFilePath**: This annotation is used to set which field will receive the path
to the file. The field MUST be of type "string". Either this one or UploadableFileName annotation is REQUIRED to be set.
3. **@Gedmo\Mapping\Annotation\UploadableFileName**: This annotation is used to set which field will receive the name
of the file. The field MUST be of type "string". Either this one or UploadableFilePath annotation is REQUIRED to be set.
4. **@Gedmo\Mapping\Annotation\UploadableFileMimeType**: This is an optional annotation used to set which field will
receive the mime type of the file as its value. This field MUST be of type "string".
5. **@Gedmo\Mapping\Annotation\UploadableFileSize**: This is an optional annotation used to set which field will
receive the size in bytes of the file as its value. This field MUST be of type "decimal".
### Notes about setting the path where the files will be moved:
You have three choices to configure the path. You can set a default path on the listener, which will be used on every
entity which doesn't have a path or pathMethod defined:
``` php
$listener->setDefaultPath('/my/path');
```
You can use the Uploadable "path" option to set the path:
``` php
/**
* @ORM\Entity
* @Gedmo\Uploadable(path="/my/path")
*/
class File
{
//...
}
```
Or you can use the Uploadable "pathMethod" option to set the name of the method which will return the path:
``` php
/**
* @ORM\Entity
* @Gedmo\Uploadable(pathMethod="getPath")
*/
class File
{
public function getPath()
{
return '/my/path';
}
}
```
### Note regarding the Uploadable interface:
The Uploadable interface is not necessary, except in cases there
you need to identify an entity as Uploadable. The metadata is loaded only once then
you need to identify an entity as Uploadable. The metadata is loaded only once then
cache is activated
### Minimum configuration needed:
``` php
<?php
namespace Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
// If you don't set the path here, remember that you must set it on the listener!
/**
* @ORM\Entity
* @Gedmo\Uploadable
*/
class File
{
// Other fields..
/**
* @ORM\Column(name="path", type="string")
* @Gedmo\UploadableFilePath
*/
private $path;
}
```
### Example of an entity with all the configurations set:
``` php
<?php
namespace Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @Gedmo\Uploadable(path="/my/path", callback="myCallbackMethod", filenameGenerator="SHA1", allowOverwrite=true, appendNumber=true)
*/
class File
{
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @ORM\Column(name="path", type="string")
* @Gedmo\UploadableFilePath
*/
private $path;
/**
* @ORM\Column(name="name", type="string")
* @Gedmo\UploadableFileName
*/
private $name;
/**
* @ORM\Column(name="mime_type", type="string")
* @Gedmo\UploadableFileMimeType
*/
private $mimeType;
/**
* @ORM\Column(name="size", type="decimal")
* @Gedmo\UploadableFileSize
*/
private $size;
public function myCallbackMethod(array $info)
{
// Do some stuff with the file..
}
// Other methods..
}
```
<a name="yaml-mapping"></a>
## Yaml mapping example:
Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml**
```
---
Entity\File:
type: entity
table: files
gedmo:
uploadable:
allowOverwrite: true
appendNumber: true
path: '/my/path'
pathMethod: getPath
callback: callbackMethod
filenameGenerator: SHA1
id:
id:
type: integer
generator:
strategy: AUTO
fields:
path:
type: string
gedmo:
- uploadableFilePath
name:
type: string
gedmo:
- uploadableFileName
mimeType:
type: string
gedmo:
- uploadableFileMimeType
size:
type: decimal
gedmo:
- uploadableFileSize
```
<a name="xml-mapping"></a>
## Xml mapping example
``` xml
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:gedmo="http://gediminasm.org/schemas/orm/doctrine-extensions-mapping">
<entity name="Entity\File" table="files">
<id name="id" type="integer" column="id">
<generator strategy="AUTO"/>
</id>
<field name="mimeType" column="mime" type="string">
<gedmo:uploadable-file-mime-type />
</field>
<field name="size" column="size" type="decimal">
<gedmo:uploadable-file-size />
</field>
<field name="name" column="name" type="string">
<gedmo:uploadable-file-name />
</field>
<field name="path" column="path" type="string">
<gedmo:uploadable-file-path />
</field>
<gedmo:uploadable
allow-overwrite="true"
append-number="true"
path="/my/path"
path-method="getPath"
callback="callbackMethod"
filename-generator="SHA1" />
</entity>
</doctrine-mapping>
```
<a name="usage"></a>
## Usage:
``` php
<?php
// Example setting the path directly on the listener:
$listener->setDefaultPath('/my/app/web/upload');
if (isset($_FILES['images']) && is_array($_FILES['images'])) {
foreach ($_FILES['images'] as $fileInfo) {
$file = new File();
$listener->addEntityFileInfo($file, $fileInfo);
// You can set the file info directly with a FileInfoInterface object, like this:
//
// $listener->addEntityFileInfo($file, new FileInfoArray($fileInfo));
//
// Or create your own class which implements FileInfoInterface
//
// $listener->addEntityFileInfo($file, new MyOwnFileInfo($fileInfo));
$em->persist($file);
}
}
$em->flush();
```
Easy like that, any suggestions on improvements are very welcome.
<a name="additional-usages"></a>
### Using the extension to handle not only uploaded files
Maybe you want to handle files obtained from an URL, or even files that are already located in the same server than your app.
This can be handled in a very simple way. First, you need to create a class that implements the FileInfoInterface
interface. As an example:
``` php
use Gedmo\Uploadable\FileInfo\FileInfoInterface;
class CustomFileInfo implements FileInfoInterface
{
protected $path;
protected $size;
protected $type;
protected $filename;
protected $error = 0;
public function __construct($path)
{
$this->path = $path;
// Now, process the file and fill the rest of the properties.
}
// This returns the actual path of the file
public function getTmpName()
{
return $path;
}
// This returns the filename
public function getName()
{
return $this->name;
}
// This returns the file size in bytes
public function getSize()
{
return $this->size;
}
// This returns the mime type
public function getType()
{
return $this->type;
}
public function getError()
{
// This should return 0, as it's only used to return the codes from PHP file upload errors.
return $this->error;
}
// If this method returns true, it will produce that the extension uses "move_uploaded_file" function to move
// the file. If it returns false, the extension will use the "copy" function.
public function isUploadedFile()
{
return false;
}
}
```
Or you could simply extend the FileInfoArray class and do the following:
``` php
use Gedmo\Uploadable\FileInfo\FileInfoArray;
class CustomFileInfo extends FileInfoArray
{
public function __construct($path)
{
// There's already a $fileInfo property, which needs to be an array with the
// following keys: tmp_name, name, size, type, error
$this->fileInfo = array(
'tmp_name' => '',
'name' => '',
'size' => 0,
'type' => '',
'error' => 0
);
// Now process the file at $path and fill the keys with the correct values.
//
// In this example we use a $path as the first argument, but it could be an URL
// to the file we need to obtain, etc.
}
public function isUploadedFile()
{
// Remember to set this to false so we use "copy" instead of "move_uploaded_file"
return false;
}
}
```
And that's it. Then, instead of getting the file info from the $_FILES array, you would do:
``` php
// We set the default path in the listener again
$listener->setDefaultPath('/my/path');
$file = new File();
$listener->addEntityFileInfo($file, new CustomFileInfo('/path/to/file.txt'));
$em->persist($file);
$em->flush();
```
<a name="custom-mime-type-guessers"></a>
### Custom Mime type guessers
If you want to use your own mime type guesser, you need to implement the interface "Gedmo\Uploadable\MimeType\MimeTypeGuesserInterface",
which has only one method: "guess($filePath)". Then, you can set the mime type guesser used on the listener in the following
way:
``` php
$listener->setMimeTypeGuesser(new MyCustomMimeTypeGuesser());
```

View File

@@ -0,0 +1,97 @@
## Using Gedmo Doctrine Extensions in Zend Framework 2
Assuming you are familiar with [DoctrineModule](https://github.com/doctrine/DoctrineModule) (if not, you should definitely start there!), integrating Doctrine Extensions with Zend Framework 2 application is super-easy.
### Composer
Add DoctrineModule, DoctrineORMModule and DoctrineExtensions to composer.json file:
```json
{
"require": {
"php": ">=5.3.3",
"zendframework/zendframework": "2.1.*",
"doctrine/doctrine-module": "0.*",
"doctrine/doctrine-orm-module": "0.*",
"gedmo/doctrine-extensions": "2.3.*",
}
}
```
Then run `composer.phar update`.
### Configuration
Once libraries are installed, you can tell Doctrine which behaviors you want to use, by declaring appropriate subscribers in Event Manager settings. Together with [entity mapping options](https://github.com/doctrine/DoctrineORMModule#entities-settings), your module configuration file should look like following:
```php
return array(
'doctrine' => array(
'eventmanager' => array(
'orm_default' => array(
'subscribers' => array(
// pick any listeners you need
'Gedmo\Tree\TreeListener',
'Gedmo\Timestampable\TimestampableListener',
'Gedmo\Sluggable\SluggableListener',
'Gedmo\Loggable\LoggableListener',
'Gedmo\Sortable\SortableListener'
),
),
),
'driver' => array(
'my_driver' => array(
'class' => 'Doctrine\ORM\Mapping\Driver\AnnotationDriver',
'cache' => 'array',
'paths' => array(__DIR__ . '/../src/MyModule/Entity')
),
'orm_default' => array(
'drivers' => array(
'MyModule\Entity' => 'my_driver'
),
),
),
),
);
```
That's it! From now on you can use Gedmo annotations, just as it is described in [documentation](https://github.com/mtymek/DoctrineExtensions/blob/master/doc/annotations.md).
#### Note: You may need to provide additional settings for some of the available listeners.
For instance, `Translatable` requires additional metadata driver in order to manage translation tables:
```php
return array(
'doctrine' => array(
'eventmanager' => array(
'orm_default' => array(
'subscribers' => array(
'Gedmo\Translatable\TranslatableListener',
),
),
),
'driver' => array(
'my_driver' => array(
'class' => 'Doctrine\ORM\Mapping\Driver\AnnotationDriver',
'cache' => 'array',
'paths' => array(__DIR__ . '/../src/MyModule/Entity')
),
'translatable_metadata_driver' => array(
'class' => 'Doctrine\ORM\Mapping\Driver\AnnotationDriver',
'cache' => 'array',
'paths' => array(
'vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Entity',
),
),
'orm_default' => array(
'drivers' => array(
'MyModule\Entity' => 'my_driver',
'Gedmo\Translatable\Entity' => 'translatable_metadata_driver',
),
),
),
),
);
```

View File

@@ -0,0 +1,227 @@
<?php
namespace Gedmo;
use Doctrine\Common\EventArgs;
use Doctrine\Common\NotifyPropertyChanged;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\ORM\UnitOfWork;
use Gedmo\Exception\UnexpectedValueException;
use Gedmo\Mapping\Event\AdapterInterface;
use Gedmo\Mapping\MappedEventSubscriber;
/**
* The Timestampable listener handles the update of
* dates on creation and update.
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
abstract class AbstractTrackingListener extends MappedEventSubscriber
{
/**
* Specifies the list of events to listen
*
* @return array
*/
public function getSubscribedEvents()
{
return array(
'prePersist',
'onFlush',
'loadClassMetadata',
);
}
/**
* Maps additional metadata for the Entity
*
* @param EventArgs $eventArgs
*
* @return void
*/
public function loadClassMetadata(EventArgs $eventArgs)
{
$ea = $this->getEventAdapter($eventArgs);
$this->loadMetadataForObjectClass($ea->getObjectManager(), $eventArgs->getClassMetadata());
}
/**
* Looks for Timestampable objects being updated
* to update modification date
*
* @param EventArgs $args
*
* @return void
*/
public function onFlush(EventArgs $args)
{
$ea = $this->getEventAdapter($args);
$om = $ea->getObjectManager();
$uow = $om->getUnitOfWork();
// check all scheduled updates
$all = array_merge($ea->getScheduledObjectInsertions($uow), $ea->getScheduledObjectUpdates($uow));
foreach ($all as $object) {
$meta = $om->getClassMetadata(get_class($object));
if (!$config = $this->getConfiguration($om, $meta->name)) {
continue;
}
$changeSet = $ea->getObjectChangeSet($uow, $object);
$needChanges = false;
if ($uow->isScheduledForInsert($object) && isset($config['create'])) {
foreach ($config['create'] as $field) {
// Field can not exist in change set, when persisting embedded document without parent for example
$new = array_key_exists($field, $changeSet) ? $changeSet[$field][1] : false;
if ($new === null) { // let manual values
$needChanges = true;
$this->updateField($object, $ea, $meta, $field);
}
}
}
if (isset($config['update'])) {
foreach ($config['update'] as $field) {
$isInsertAndNull = $uow->isScheduledForInsert($object)
&& array_key_exists($field, $changeSet)
&& $changeSet[$field][1] === null;
if (!isset($changeSet[$field]) || $isInsertAndNull) { // let manual values
$needChanges = true;
$this->updateField($object, $ea, $meta, $field);
}
}
}
if (!$uow->isScheduledForInsert($object) && isset($config['change'])) {
foreach ($config['change'] as $options) {
if (isset($changeSet[$options['field']])) {
continue; // value was set manually
}
if (!is_array($options['trackedField'])) {
$singleField = true;
$trackedFields = array($options['trackedField']);
} else {
$singleField = false;
$trackedFields = $options['trackedField'];
}
foreach ($trackedFields as $trackedField) {
$trackedChild = null;
$tracked = null;
$parts = explode('.', $trackedField);
if (isset($parts[1])) {
$tracked = $parts[0];
$trackedChild = $parts[1];
}
if (!isset($tracked) || array_key_exists($trackedField, $changeSet)) {
$tracked = $trackedField;
$trackedChild = null;
}
if (isset($changeSet[$tracked])) {
$changes = $changeSet[$tracked];
if (isset($trackedChild)) {
$changingObject = $changes[1];
if (!is_object($changingObject)) {
throw new UnexpectedValueException(
"Field - [{$tracked}] is expected to be object in class - {$meta->name}"
);
}
$objectMeta = $om->getClassMetadata(get_class($changingObject));
$om->initializeObject($changingObject);
$value = $objectMeta->getReflectionProperty($trackedChild)->getValue($changingObject);
} else {
$value = $changes[1];
}
if (($singleField && in_array($value, (array) $options['value'])) || $options['value'] === null) {
$needChanges = true;
$this->updateField($object, $ea, $meta, $options['field']);
}
}
}
}
}
if ($needChanges) {
$ea->recomputeSingleObjectChangeSet($uow, $meta, $object);
}
}
}
/**
* Checks for persisted Timestampable objects
* to update creation and modification dates
*
* @param EventArgs $args
*
* @return void
*/
public function prePersist(EventArgs $args)
{
$ea = $this->getEventAdapter($args);
$om = $ea->getObjectManager();
$object = $ea->getObject();
$meta = $om->getClassMetadata(get_class($object));
if ($config = $this->getConfiguration($om, $meta->getName())) {
if (isset($config['update'])) {
foreach ($config['update'] as $field) {
if ($meta->getReflectionProperty($field)->getValue($object) === null) { // let manual values
$this->updateField($object, $ea, $meta, $field);
}
}
}
if (isset($config['create'])) {
foreach ($config['create'] as $field) {
if ($meta->getReflectionProperty($field)->getValue($object) === null) { // let manual values
$this->updateField($object, $ea, $meta, $field);
}
}
}
}
}
/**
* Get value for update field
*
* @param ClassMetadata $meta
* @param string $field
* @param AdapterInterface $eventAdapter
*/
abstract protected function getFieldValue($meta, $field, $eventAdapter);
/**
* Updates a field
*
* @param object $object
* @param AdapterInterface $eventAdapter
* @param ClassMetadata $meta
* @param string $field
*/
protected function updateField($object, $eventAdapter, $meta, $field)
{
/** @var \Doctrine\Orm\Mapping\ClassMetadata|\Doctrine\ODM\MongoDB\Mapping\ClassMetadata $meta */
$property = $meta->getReflectionProperty($field);
$oldValue = $property->getValue($object);
$newValue = $this->getFieldValue($meta, $field, $eventAdapter);
// if field value is reference, persist object
if ($meta->hasAssociation($field) && is_object($newValue) && !$eventAdapter->getObjectManager()->contains($newValue)) {
$uow = $eventAdapter->getObjectManager()->getUnitOfWork();
// Check to persist only when the entity isn't already managed, persists always for MongoDB
if(!($uow instanceof UnitOfWork) || $uow->getEntityState($newValue) !== UnitOfWork::STATE_MANAGED) {
$eventAdapter->getObjectManager()->persist($newValue);
}
}
$property->setValue($object, $newValue);
if ($object instanceof NotifyPropertyChanged) {
$uow = $eventAdapter->getObjectManager()->getUnitOfWork();
$uow->propertyChanged($object, $field, $oldValue, $newValue);
}
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Gedmo\Blameable;
/**
* This interface is not necessary but can be implemented for
* Entities which in some cases needs to be identified as
* Blameable
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
interface Blameable
{
// blameable expects annotations on properties
/**
* @gedmo:Blameable(on="create")
* fields which should be updated on insert only
*/
/**
* @gedmo:Blameable(on="update")
* fields which should be updated on update and insert
*/
/**
* @gedmo:Blameable(on="change", field="field", value="value")
* fields which should be updated on changed "property"
* value and become equal to given "value"
*/
/**
* @gedmo:Blameable(on="change", field="field")
* fields which should be updated on changed "property"
*/
/**
* @gedmo:Blameable(on="change", fields={"field1", "field2"})
* fields which should be updated if at least one of the given fields changed
*/
/**
* example
*
* @gedmo:Blameable(on="create")
* @Column(type="string")
* $created
*/
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Gedmo\Blameable;
use Doctrine\Common\NotifyPropertyChanged;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Gedmo\AbstractTrackingListener;
use Gedmo\Exception\InvalidArgumentException;
use Gedmo\Timestampable\TimestampableListener;
use Gedmo\Blameable\Mapping\Event\BlameableAdapter;
/**
* The Blameable listener handles the update of
* dates on creation and update.
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class BlameableListener extends AbstractTrackingListener
{
protected $user;
/**
* Get the user value to set on a blameable field
*
* @param object $meta
* @param string $field
*
* @return mixed
*/
public function getFieldValue($meta, $field, $eventAdapter)
{
if ($meta->hasAssociation($field)) {
if (null !== $this->user && ! is_object($this->user)) {
throw new InvalidArgumentException("Blame is reference, user must be an object");
}
return $this->user;
}
// ok so its not an association, then it is a string
if (is_object($this->user)) {
if (method_exists($this->user, 'getUsername')) {
return (string) $this->user->getUsername();
}
if (method_exists($this->user, '__toString')) {
return $this->user->__toString();
}
throw new InvalidArgumentException("Field expects string, user must be a string, or object should have method getUsername or __toString");
}
return $this->user;
}
/**
* Set a user value to return
*
* @param mixed $user
*/
public function setUserValue($user)
{
$this->user = $user;
}
/**
* {@inheritDoc}
*/
protected function getNamespace()
{
return __NAMESPACE__;
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Gedmo\Blameable\Mapping\Driver;
use Gedmo\Mapping\Driver\AbstractAnnotationDriver;
use Gedmo\Exception\InvalidMappingException;
/**
* This is an annotation mapping driver for Blameable
* behavioral extension. Used for extraction of extended
* metadata from Annotations specifically for Blameable
* extension.
*
* @author David Buchmann <mail@davidbu.ch>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class Annotation extends AbstractAnnotationDriver
{
/**
* Annotation field is blameable
*/
const BLAMEABLE = 'Gedmo\\Mapping\\Annotation\\Blameable';
/**
* List of types which are valid for blame
*
* @var array
*/
protected $validTypes = array(
'one',
'string',
'int',
);
/**
* {@inheritDoc}
*/
public function readExtendedMetadata($meta, array &$config)
{
$class = $this->getMetaReflectionClass($meta);
// property annotations
foreach ($class->getProperties() as $property) {
if ($meta->isMappedSuperclass && !$property->isPrivate() ||
$meta->isInheritedField($property->name) ||
isset($meta->associationMappings[$property->name]['inherited'])
) {
continue;
}
if ($blameable = $this->reader->getPropertyAnnotation($property, self::BLAMEABLE)) {
$field = $property->getName();
if (!$meta->hasField($field) && !$meta->hasAssociation($field)) {
throw new InvalidMappingException("Unable to find blameable [{$field}] as mapped property in entity - {$meta->name}");
}
if ($meta->hasField($field)) {
if ( !$this->isValidField($meta, $field)) {
throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' or a one-to-many relation in class - {$meta->name}");
}
} else {
// association
if (! $meta->isSingleValuedAssociation($field)) {
throw new InvalidMappingException("Association - [{$field}] is not valid, it must be a one-to-many relation or a string field - {$meta->name}");
}
}
if (!in_array($blameable->on, array('update', 'create', 'change'))) {
throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}");
}
if ($blameable->on == 'change') {
if (!isset($blameable->field)) {
throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}");
}
if (is_array($blameable->field) && isset($blameable->value)) {
throw new InvalidMappingException("Blameable extension does not support multiple value changeset detection yet.");
}
$field = array(
'field' => $field,
'trackedField' => $blameable->field,
'value' => $blameable->value,
);
}
// properties are unique and mapper checks that, no risk here
$config[$blameable->on][] = $field;
}
}
}
}

View File

@@ -0,0 +1,129 @@
<?php
namespace Gedmo\Blameable\Mapping\Driver;
use Gedmo\Mapping\Driver\Xml as BaseXml;
use Gedmo\Exception\InvalidMappingException;
/**
* This is a xml mapping driver for Blameable
* behavioral extension. Used for extraction of extended
* metadata from xml specifically for Blameable
* extension.
*
* @author David Buchmann <mail@davidbu.ch>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class Xml extends BaseXml
{
/**
* List of types which are valid for blame
*
* @var array
*/
private $validTypes = array(
'one',
'string',
'int',
);
/**
* {@inheritDoc}
*/
public function readExtendedMetadata($meta, array &$config)
{
/**
* @var \SimpleXmlElement $mapping
*/
$mapping = $this->_getMapping($meta->name);
if (isset($mapping->field)) {
/**
* @var \SimpleXmlElement $fieldMapping
*/
foreach ($mapping->field as $fieldMapping) {
$fieldMappingDoctrine = $fieldMapping;
$fieldMapping = $fieldMapping->children(self::GEDMO_NAMESPACE_URI);
if (isset($fieldMapping->blameable)) {
/**
* @var \SimpleXmlElement $data
*/
$data = $fieldMapping->blameable;
$field = $this->_getAttribute($fieldMappingDoctrine, 'name');
if (!$this->isValidField($meta, $field)) {
throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' or a reference in class - {$meta->name}");
}
if (!$this->_isAttributeSet($data, 'on') || !in_array($this->_getAttribute($data, 'on'), array('update', 'create', 'change'))) {
throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}");
}
if ($this->_getAttribute($data, 'on') == 'change') {
if (!$this->_isAttributeSet($data, 'field')) {
throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}");
}
$trackedFieldAttribute = $this->_getAttribute($data, 'field');
$valueAttribute = $this->_isAttributeSet($data, 'value') ? $this->_getAttribute($data, 'value' ) : null;
if (is_array($trackedFieldAttribute) && null !== $valueAttribute) {
throw new InvalidMappingException("Blameable extension does not support multiple value changeset detection yet.");
}
$field = array(
'field' => $field,
'trackedField' => $trackedFieldAttribute,
'value' => $valueAttribute,
);
}
$config[$this->_getAttribute($data, 'on')][] = $field;
}
}
}
if (isset($mapping->{'many-to-one'})) {
foreach ($mapping->{'many-to-one'} as $fieldMapping) {
$field = $this->_getAttribute($fieldMapping, 'field');
$fieldMapping = $fieldMapping->children(self::GEDMO_NAMESPACE_URI);
if (isset($fieldMapping->blameable)) {
$data = $fieldMapping->blameable;
if (! $meta->isSingleValuedAssociation($field)) {
throw new InvalidMappingException("Association - [{$field}] is not valid, it must be a one-to-many relation or a string field - {$meta->name}");
}
if (!$this->_isAttributeSet($data, 'on') || !in_array($this->_getAttribute($data, 'on'), array('update', 'create', 'change'))) {
throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}");
}
if ($this->_getAttribute($data, 'on') == 'change') {
if (!$this->_isAttributeSet($data, 'field')) {
throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}");
}
$trackedFieldAttribute = $this->_getAttribute($data, 'field');
$valueAttribute = $this->_isAttributeSet($data, 'value') ? $this->_getAttribute($data, 'value' ) : null;
if (is_array($trackedFieldAttribute) && null !== $valueAttribute) {
throw new InvalidMappingException("Blameable extension does not support multiple value changeset detection yet.");
}
$field = array(
'field' => $field,
'trackedField' => $trackedFieldAttribute,
'value' => $valueAttribute,
);
}
$config[$this->_getAttribute($data, 'on')][] = $field;
}
}
}
}
/**
* Checks if $field type is valid
*
* @param object $meta
* @param string $field
*
* @return boolean
*/
protected function isValidField($meta, $field)
{
$mapping = $meta->getFieldMapping($field);
return $mapping && in_array($mapping['type'], $this->validTypes);
}
}

View File

@@ -0,0 +1,129 @@
<?php
namespace Gedmo\Blameable\Mapping\Driver;
use Gedmo\Mapping\Driver\File;
use Gedmo\Mapping\Driver;
use Gedmo\Exception\InvalidMappingException;
/**
* This is a yaml mapping driver for Blameable
* behavioral extension. Used for extraction of extended
* metadata from yaml specifically for Blameable
* extension.
*
* @author David Buchmann <mail@davidbu.ch>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class Yaml extends File implements Driver
{
/**
* File extension
* @var string
*/
protected $_extension = '.dcm.yml';
/**
* List of types which are valid for blameable
*
* @var array
*/
private $validTypes = array(
'one',
'string',
'int',
);
/**
* {@inheritDoc}
*/
public function readExtendedMetadata($meta, array &$config)
{
$mapping = $this->_getMapping($meta->name);
if (isset($mapping['fields'])) {
foreach ($mapping['fields'] as $field => $fieldMapping) {
if (isset($fieldMapping['gedmo']['blameable'])) {
$mappingProperty = $fieldMapping['gedmo']['blameable'];
if (!$this->isValidField($meta, $field)) {
throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' or a reference in class - {$meta->name}");
}
if (!isset($mappingProperty['on']) || !in_array($mappingProperty['on'], array('update', 'create', 'change'))) {
throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}");
}
if ($mappingProperty['on'] == 'change') {
if (!isset($mappingProperty['field'])) {
throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}");
}
$trackedFieldAttribute = $mappingProperty['field'];
$valueAttribute = isset($mappingProperty['value']) ? $mappingProperty['value'] : null;
if (is_array($trackedFieldAttribute) && null !== $valueAttribute) {
throw new InvalidMappingException("Blameable extension does not support multiple value changeset detection yet.");
}
$field = array(
'field' => $field,
'trackedField' => $trackedFieldAttribute,
'value' => $valueAttribute,
);
}
$config[$mappingProperty['on']][] = $field;
}
}
}
if (isset($mapping['manyToOne'])) {
foreach ($mapping['manyToOne'] as $field => $fieldMapping) {
if (isset($fieldMapping['gedmo']['blameable'])) {
$mappingProperty = $fieldMapping['gedmo']['blameable'];
if (! $meta->isSingleValuedAssociation($field)) {
throw new InvalidMappingException("Association - [{$field}] is not valid, it must be a one-to-many relation or a string field - {$meta->name}");
}
if (!isset($mappingProperty['on']) || !in_array($mappingProperty['on'], array('update', 'create', 'change'))) {
throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}");
}
if ($mappingProperty['on'] == 'change') {
if (!isset($mappingProperty['field'])) {
throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}");
}
$trackedFieldAttribute = $mappingProperty['field'];
$valueAttribute = isset($mappingProperty['value']) ? $mappingProperty['value'] : null;
if (is_array($trackedFieldAttribute) && null !== $valueAttribute) {
throw new InvalidMappingException("Blameable extension does not support multiple value changeset detection yet.");
}
$field = array(
'field' => $field,
'trackedField' => $trackedFieldAttribute,
'value' => $valueAttribute,
);
}
$config[$mappingProperty['on']][] = $field;
}
}
}
}
/**
* {@inheritDoc}
*/
protected function _loadMappingFile($file)
{
return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file));
}
/**
* Checks if $field type is valid
*
* @param \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $meta
* @param string $field
*
* @return boolean
*/
protected function isValidField($meta, $field)
{
$mapping = $meta->getFieldMapping($field);
return $mapping && in_array($mapping['type'], $this->validTypes);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Gedmo\Blameable\Mapping\Event\Adapter;
use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM;
use Gedmo\Blameable\Mapping\Event\BlameableAdapter;
/**
* Doctrine event adapter for ODM adapted
* for Blameable behavior.
*
* @author David Buchmann <mail@davidbu.ch>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
final class ODM extends BaseAdapterODM implements BlameableAdapter
{
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Gedmo\Blameable\Mapping\Event\Adapter;
use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM;
use Gedmo\Blameable\Mapping\Event\BlameableAdapter;
/**
* Doctrine event adapter for ORM adapted
* for Blameable behavior.
*
* @author David Buchmann <mail@davidbu.ch>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
final class ORM extends BaseAdapterORM implements BlameableAdapter
{
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Gedmo\Blameable\Mapping\Event;
use Gedmo\Mapping\Event\AdapterInterface;
/**
* Doctrine event adapter interface
* for Blameable behavior.
*
* @author David Buchmann <mail@davidbu.ch>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
interface BlameableAdapter extends AdapterInterface
{
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Gedmo\Blameable\Traits;
/**
* Blameable Trait, usable with PHP >= 5.4
*
* @author David Buchmann <mail@davidbu.ch>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
trait Blameable
{
/**
* @var string
*/
private $createdBy;
/**
* @var string
*/
private $updatedBy;
/**
* Sets createdBy.
*
* @param string $createdBy
* @return $this
*/
public function setCreatedBy($createdBy)
{
$this->createdBy = $createdBy;
return $this;
}
/**
* Returns createdBy.
*
* @return string
*/
public function getCreatedBy()
{
return $this->createdBy;
}
/**
* Sets updatedBy.
*
* @param string $updatedBy
* @return $this
*/
public function setUpdatedBy($updatedBy)
{
$this->updatedBy = $updatedBy;
return $this;
}
/**
* Returns updatedBy.
*
* @return string
*/
public function getUpdatedBy()
{
return $this->updatedBy;
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Gedmo\Blameable\Traits;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* Blameable Trait, usable with PHP >= 5.4
*
* @author David Buchmann <mail@davidbu.ch>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
trait BlameableDocument
{
/**
* @var string
* @Gedmo\Blameable(on="create")
* @ODM\Field(type="string")
*/
protected $createdBy;
/**
* @var string
* @Gedmo\Blameable(on="update")
* @ODM\Field(type="string")
*/
protected $updatedBy;
/**
* Sets createdBy.
*
* @param string $createdBy
* @return $this
*/
public function setCreatedBy($createdBy)
{
$this->createdBy = $createdBy;
return $this;
}
/**
* Returns createdBy.
*
* @return string
*/
public function getCreatedBy()
{
return $this->createdBy;
}
/**
* Sets updatedBy.
*
* @param string $updatedBy
* @return $this
*/
public function setUpdatedBy($updatedBy)
{
$this->updatedBy = $updatedBy;
return $this;
}
/**
* Returns updatedBy.
*
* @return string
*/
public function getUpdatedBy()
{
return $this->updatedBy;
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Gedmo\Blameable\Traits;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* Blameable Trait, usable with PHP >= 5.4
*
* @author David Buchmann <mail@davidbu.ch>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
trait BlameableEntity
{
/**
* @var string
* @Gedmo\Blameable(on="create")
* @ORM\Column(nullable=true)
*/
protected $createdBy;
/**
* @var string
* @Gedmo\Blameable(on="update")
* @ORM\Column(nullable=true)
*/
protected $updatedBy;
/**
* Sets createdBy.
*
* @param string $createdBy
* @return $this
*/
public function setCreatedBy($createdBy)
{
$this->createdBy = $createdBy;
return $this;
}
/**
* Returns createdBy.
*
* @return string
*/
public function getCreatedBy()
{
return $this->createdBy;
}
/**
* Sets updatedBy.
*
* @param string $updatedBy
* @return $this
*/
public function setUpdatedBy($updatedBy)
{
$this->updatedBy = $updatedBy;
return $this;
}
/**
* Returns updatedBy.
*
* @return string
*/
public function getUpdatedBy()
{
return $this->updatedBy;
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Gedmo;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\ORM\Mapping\Driver as DriverORM;
use Doctrine\ODM\MongoDB\Mapping\Driver as DriverMongodbODM;
use Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain;
use Doctrine\Common\Annotations\Reader;
use Doctrine\Common\Annotations\CachedReader;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Cache\ArrayCache;
/**
* Version class allows to checking the dependencies required
* and the current version of doctrine extensions
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
final class DoctrineExtensions
{
/**
* Current version of extensions
*/
const VERSION = 'v2.4.42';
/**
* Hooks all extensions metadata mapping drivers
* into given $driverChain of drivers for ORM
*
* @param MappingDriverChain $driverChain
* @param Reader|null $reader
*/
public static function registerMappingIntoDriverChainORM(MappingDriverChain $driverChain, Reader $reader = null)
{
self::registerAnnotations();
if (!$reader) {
$reader = new CachedReader(new AnnotationReader(), new ArrayCache());
}
$annotationDriver = new DriverORM\AnnotationDriver($reader, array(
__DIR__.'/Translatable/Entity',
__DIR__.'/Loggable/Entity',
__DIR__.'/Tree/Entity',
));
$driverChain->addDriver($annotationDriver, 'Gedmo');
}
/**
* Hooks only superclass metadata mapping drivers
* into given $driverChain of drivers for ORM
*
* @param MappingDriverChain $driverChain
* @param Reader|null $reader
*/
public static function registerAbstractMappingIntoDriverChainORM(MappingDriverChain $driverChain, Reader $reader = null)
{
self::registerAnnotations();
if (!$reader) {
$reader = new CachedReader(new AnnotationReader(), new ArrayCache());
}
$annotationDriver = new DriverORM\AnnotationDriver($reader, array(
__DIR__.'/Translatable/Entity/MappedSuperclass',
__DIR__.'/Loggable/Entity/MappedSuperclass',
__DIR__.'/Tree/Entity/MappedSuperclass',
));
$driverChain->addDriver($annotationDriver, 'Gedmo');
}
/**
* Hooks all extensions metadata mapping drivers
* into given $driverChain of drivers for ODM MongoDB
*
* @param MappingDriverChain $driverChain
* @param Reader|null $reader
*/
public static function registerMappingIntoDriverChainMongodbODM(MappingDriverChain $driverChain, Reader $reader = null)
{
self::registerAnnotations();
if (!$reader) {
$reader = new CachedReader(new AnnotationReader(), new ArrayCache());
}
$annotationDriver = new DriverMongodbODM\AnnotationDriver($reader, array(
__DIR__.'/Translatable/Document',
__DIR__.'/Loggable/Document',
));
$driverChain->addDriver($annotationDriver, 'Gedmo');
}
/**
* Hooks only superclass metadata mapping drivers
* into given $driverChain of drivers for ODM MongoDB
*
* @param MappingDriverChain $driverChain
* @param Reader|null $reader
*/
public static function registerAbstractMappingIntoDriverChainMongodbODM(MappingDriverChain $driverChain, Reader $reader = null)
{
self::registerAnnotations();
if (!$reader) {
$reader = new CachedReader(new AnnotationReader(), new ArrayCache());
}
$annotationDriver = new DriverMongodbODM\AnnotationDriver($reader, array(
__DIR__.'/Translatable/Document/MappedSuperclass',
__DIR__.'/Loggable/Document/MappedSuperclass',
));
$driverChain->addDriver($annotationDriver, 'Gedmo');
}
/**
* Includes all extension annotations once
*/
public static function registerAnnotations()
{
AnnotationRegistry::registerFile(__DIR__.'/Mapping/Annotation/All.php');
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Gedmo;
/**
* Common package exception interface to allow
* users of caching only this package specific
* exceptions thrown
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
interface Exception
{
/**
* Following best practices for PHP5.3 package exceptions.
* All exceptions thrown in this package will have to implement this interface
*/
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* BadMethodCallException
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class BadMethodCallException
extends \BadMethodCallException
implements Exception
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* FeatureNotImplementedException
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class FeatureNotImplementedException
extends \RuntimeException
implements Exception
{
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* InvalidArgumentException
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class InvalidArgumentException
extends \InvalidArgumentException
implements Exception
{
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* InvalidMappingException
*
* Triggered when mapping user argument is not
* valid or incomplete.
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class InvalidMappingException
extends InvalidArgumentException
implements Exception
{
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* ReferenceIntegrityStrictException
*
* @author Evert Harmeling <evert.harmeling@freshheads.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class ReferenceIntegrityStrictException extends RuntimeException
{
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* RuntimeException
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class RuntimeException
extends \RuntimeException
implements Exception
{
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* TreeLockingException
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class TreeLockingException extends RuntimeException
{
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* UnexpectedValueException
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class UnexpectedValueException
extends \UnexpectedValueException
implements Exception
{
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* UnsupportedObjectManager
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class UnsupportedObjectManagerException
extends InvalidArgumentException
implements Exception
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* UploadableCantWriteException
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class UploadableCantWriteException
extends UploadableException
implements Exception
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* UploadableCouldntGuessMimeTypeException
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class UploadableCouldntGuessMimeTypeException
extends UploadableException
implements Exception
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* UploadableDirectoryNotFoundException
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class UploadableDirectoryNotFoundException
extends UploadableException
implements Exception
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* UploadableException
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class UploadableException
extends RuntimeException
implements Exception
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* UploadableExtensionException
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class UploadableExtensionException
extends UploadableException
implements Exception
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* UploadableFileAlreadyExistsException
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class UploadableFileAlreadyExistsException
extends UploadableException
implements Exception
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* UploadableFileNotReadableException
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class UploadableFileNotReadableException
extends UploadableException
implements Exception
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* UploadableFormSizeException
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class UploadableFormSizeException
extends UploadableException
implements Exception
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* UploadableIniSizeException
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class UploadableIniSizeException
extends UploadableException
implements Exception
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* UploadableInvalidFileException
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class UploadableInvalidFileException
extends UploadableException
implements Exception
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* UploadableInvalidMimeTypeException
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class UploadableInvalidMimeTypeException
extends UploadableException
implements Exception
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* UploadableInvalidPathException
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class UploadableInvalidPathException
extends UploadableException
implements Exception
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* UploadableMaxSizeException
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class UploadableMaxSizeException
extends UploadableException
implements Exception
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* UploadableNoFileException
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class UploadableNoFileException
extends UploadableException
implements Exception
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* UploadableNoPathDefinedException
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class UploadableNoPathDefinedException
extends UploadableException
implements Exception
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* UploadableNoTmpDirException
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class UploadableNoTmpDirException
extends UploadableException
implements Exception
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* UploadablePartialException
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class UploadablePartialException
extends UploadableException
implements Exception
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Exception;
use Gedmo\Exception;
/**
* UploadableUploadException
*
* @author Gustavo Falco <comfortablynumb84@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class UploadableUploadException
extends UploadableException
implements Exception
{
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Gedmo\IpTraceable;
/**
* This interface is not necessary but can be implemented for
* Entities which in some cases needs to be identified as
* IpTraceable
*
* @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
interface IpTraceable
{
// ipTraceable expects annotations on properties
/**
* @gedmo:IpTraceable(on="create")
* strings which should be updated on insert only
*/
/**
* @gedmo:IpTraceable(on="update")
* strings which should be updated on update and insert
*/
/**
* @gedmo:IpTraceable(on="change", field="field", value="value")
* strings which should be updated on changed "property"
* value and become equal to given "value"
*/
/**
* @gedmo:IpTraceable(on="change", field="field")
* strings which should be updated on changed "property"
*/
/**
* @gedmo:IpTraceable(on="change", fields={"field1", "field2"})
* strings which should be updated if at least one of the given fields changed
*/
/**
* example
*
* @gedmo:IpTraceable(on="create")
* @Column(type="string")
* $created
*/
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Gedmo\IpTraceable;
use Gedmo\AbstractTrackingListener;
use Gedmo\Exception\InvalidArgumentException;
use Gedmo\Mapping\Event\AdapterInterface;
/**
* The IpTraceable listener handles the update of
* IPs on creation and update.
*
* @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class IpTraceableListener extends AbstractTrackingListener
{
/**
* @var string|null
*/
protected $ip;
/**
* Get the ipValue value to set on a ip field
*
* @param object $meta
* @param string $field
* @param AdapterInterface $eventAdapter
*
* @return null|string
*/
public function getFieldValue($meta, $field, $eventAdapter)
{
return $this->ip;
}
/**
* Set a ip value to return
*
* @param string $ip
* @throws InvalidArgumentException
*/
public function setIpValue($ip = null)
{
if (isset($ip) && filter_var($ip, FILTER_VALIDATE_IP) === false) {
throw new InvalidArgumentException("ip address is not valid $ip");
}
$this->ip = $ip;
}
/**
* {@inheritDoc}
*/
protected function getNamespace()
{
return __NAMESPACE__;
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Gedmo\IpTraceable\Mapping\Driver;
use Gedmo\Mapping\Driver\AbstractAnnotationDriver;
use Gedmo\Exception\InvalidMappingException;
/**
* This is an annotation mapping driver for IpTraceable
* behavioral extension. Used for extraction of extended
* metadata from Annotations specifically for IpTraceable
* extension.
*
* @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class Annotation extends AbstractAnnotationDriver
{
/**
* Annotation field is ipTraceable
*/
const IP_TRACEABLE = 'Gedmo\\Mapping\\Annotation\\IpTraceable';
/**
* List of types which are valid for IP
*
* @var array
*/
protected $validTypes = array(
'string',
);
/**
* {@inheritDoc}
*/
public function readExtendedMetadata($meta, array &$config)
{
$class = $this->getMetaReflectionClass($meta);
// property annotations
foreach ($class->getProperties() as $property) {
if ($meta->isMappedSuperclass && !$property->isPrivate() ||
$meta->isInheritedField($property->name) ||
isset($meta->associationMappings[$property->name]['inherited'])
) {
continue;
}
if ($ipTraceable = $this->reader->getPropertyAnnotation($property, self::IP_TRACEABLE)) {
$field = $property->getName();
if (!$meta->hasField($field)) {
throw new InvalidMappingException("Unable to find ipTraceable [{$field}] as mapped property in entity - {$meta->name}");
}
if ($meta->hasField($field) && !$this->isValidField($meta, $field)) {
throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' - {$meta->name}");
}
if (!in_array($ipTraceable->on, array('update', 'create', 'change'))) {
throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}");
}
if ($ipTraceable->on == 'change') {
if (!isset($ipTraceable->field)) {
throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}");
}
if (is_array($ipTraceable->field) && isset($ipTraceable->value)) {
throw new InvalidMappingException("IpTraceable extension does not support multiple value changeset detection yet.");
}
$field = array(
'field' => $field,
'trackedField' => $ipTraceable->field,
'value' => $ipTraceable->value,
);
}
// properties are unique and mapper checks that, no risk here
$config[$ipTraceable->on][] = $field;
}
}
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace Gedmo\IpTraceable\Mapping\Driver;
use Gedmo\Mapping\Driver\Xml as BaseXml;
use Gedmo\Exception\InvalidMappingException;
/**
* This is a xml mapping driver for IpTraceable
* behavioral extension. Used for extraction of extended
* metadata from xml specifically for IpTraceable
* extension.
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @author Miha Vrhovnik <miha.vrhovnik@gmail.com>
* @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class Xml extends BaseXml
{
/**
* List of types which are valid for IP
*
* @var array
*/
private $validTypes = array(
'string',
);
/**
* {@inheritDoc}
*/
public function readExtendedMetadata($meta, array &$config)
{
/**
* @var \SimpleXmlElement $mapping
*/
$mapping = $this->_getMapping($meta->name);
if (isset($mapping->field)) {
/**
* @var \SimpleXmlElement $fieldMapping
*/
foreach ($mapping->field as $fieldMapping) {
$fieldMappingDoctrine = $fieldMapping;
$fieldMapping = $fieldMapping->children(self::GEDMO_NAMESPACE_URI);
if (isset($fieldMapping->{'ip-traceable'})) {
/**
* @var \SimpleXmlElement $data
*/
$data = $fieldMapping->{'ip-traceable'};
$field = $this->_getAttribute($fieldMappingDoctrine, 'name');
if (!$this->isValidField($meta, $field)) {
throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' in class - {$meta->name}");
}
if (!$this->_isAttributeSet($data, 'on') || !in_array($this->_getAttribute($data, 'on'), array('update', 'create', 'change'))) {
throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}");
}
if ($this->_getAttribute($data, 'on') == 'change') {
if (!$this->_isAttributeSet($data, 'field')) {
throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}");
}
$trackedFieldAttribute = $this->_getAttribute($data, 'field');
$valueAttribute = $this->_isAttributeSet($data, 'value') ? $this->_getAttribute($data, 'value' ) : null;
if (is_array($trackedFieldAttribute) && null !== $valueAttribute) {
throw new InvalidMappingException("IpTraceable extension does not support multiple value changeset detection yet.");
}
$field = array(
'field' => $field,
'trackedField' => $trackedFieldAttribute,
'value' => $valueAttribute,
);
}
$config[$this->_getAttribute($data, 'on')][] = $field;
}
}
}
if (isset($mapping->{'many-to-one'})) {
foreach ($mapping->{'many-to-one'} as $fieldMapping) {
$field = $this->_getAttribute($fieldMapping, 'field');
$fieldMapping = $fieldMapping->children(self::GEDMO_NAMESPACE_URI);
if (isset($fieldMapping->{'ip-traceable'})) {
/**
* @var \SimpleXmlElement $data
*/
$data = $fieldMapping->{'ip-traceable'};
if (! $meta->isSingleValuedAssociation($field)) {
throw new InvalidMappingException("Association - [{$field}] is not valid, it must be a one-to-many relation or a string field - {$meta->name}");
}
if (!$this->_isAttributeSet($data, 'on') || !in_array($this->_getAttribute($data, 'on'), array('update', 'create', 'change'))) {
throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}");
}
if ($this->_getAttribute($data, 'on') == 'change') {
if (!$this->_isAttributeSet($data, 'field')) {
throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}");
}
$trackedFieldAttribute = $this->_getAttribute($data, 'field');
$valueAttribute = $this->_isAttributeSet($data, 'value') ? $this->_getAttribute($data, 'value' ) : null;
if (is_array($trackedFieldAttribute) && null !== $valueAttribute) {
throw new InvalidMappingException("IpTraceable extension does not support multiple value changeset detection yet.");
}
$field = array(
'field' => $field,
'trackedField' => $trackedFieldAttribute,
'value' => $valueAttribute,
);
}
$config[$this->_getAttribute($data, 'on')][] = $field;
}
}
}
}
/**
* Checks if $field type is valid
*
* @param object $meta
* @param string $field
*
* @return boolean
*/
protected function isValidField($meta, $field)
{
$mapping = $meta->getFieldMapping($field);
return $mapping && in_array($mapping['type'], $this->validTypes);
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace Gedmo\IpTraceable\Mapping\Driver;
use Gedmo\Mapping\Driver\File;
use Gedmo\Mapping\Driver;
use Gedmo\Exception\InvalidMappingException;
/**
* This is a yaml mapping driver for IpTraceable
* behavioral extension. Used for extraction of extended
* metadata from yaml specifically for IpTraceable
* extension.
*
* @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class Yaml extends File implements Driver
{
/**
* File extension
* @var string
*/
protected $_extension = '.dcm.yml';
/**
* List of types which are valid for IP
*
* @var array
*/
private $validTypes = array(
'string',
);
/**
* {@inheritDoc}
*/
public function readExtendedMetadata($meta, array &$config)
{
$mapping = $this->_getMapping($meta->name);
if (isset($mapping['fields'])) {
foreach ($mapping['fields'] as $field => $fieldMapping) {
if (isset($fieldMapping['gedmo']['ipTraceable'])) {
$mappingProperty = $fieldMapping['gedmo']['ipTraceable'];
if (!$this->isValidField($meta, $field)) {
throw new InvalidMappingException("Field - [{$field}] type is not valid and must be 'string' in class - {$meta->name}");
}
if (!isset($mappingProperty['on']) || !in_array($mappingProperty['on'], array('update', 'create', 'change'))) {
throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}");
}
if ($mappingProperty['on'] == 'change') {
if (!isset($mappingProperty['field'])) {
throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}");
}
$trackedFieldAttribute = $mappingProperty['field'];
$valueAttribute = isset($mappingProperty['value']) ? $mappingProperty['value'] : null;
if (is_array($trackedFieldAttribute) && null !== $valueAttribute) {
throw new InvalidMappingException("IpTraceable extension does not support multiple value changeset detection yet.");
}
$field = array(
'field' => $field,
'trackedField' => $trackedFieldAttribute,
'value' => $valueAttribute,
);
}
$config[$mappingProperty['on']][] = $field;
}
}
}
if (isset($mapping['manyToOne'])) {
foreach ($mapping['manyToOne'] as $field => $fieldMapping) {
if (isset($fieldMapping['gedmo']['ipTraceable'])) {
$mappingProperty = $fieldMapping['gedmo']['ipTraceable'];
if (! $meta->isSingleValuedAssociation($field)) {
throw new InvalidMappingException("Association - [{$field}] is not valid, it must be a one-to-many relation or a string field - {$meta->name}");
}
if (!isset($mappingProperty['on']) || !in_array($mappingProperty['on'], array('update', 'create', 'change'))) {
throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->name}");
}
if ($mappingProperty['on'] == 'change') {
if (!isset($mappingProperty['field'])) {
throw new InvalidMappingException("Missing parameters on property - {$field}, field must be set on [change] trigger in class - {$meta->name}");
}
$trackedFieldAttribute = $mappingProperty['field'];
$valueAttribute = isset($mappingProperty['value']) ? $mappingProperty['value'] : null;
if (is_array($trackedFieldAttribute) && null !== $valueAttribute) {
throw new InvalidMappingException("IpTraceable extension does not support multiple value changeset detection yet.");
}
$field = array(
'field' => $field,
'trackedField' => $trackedFieldAttribute,
'value' => $valueAttribute,
);
}
$config[$mappingProperty['on']][] = $field;
}
}
}
}
/**
* {@inheritDoc}
*/
protected function _loadMappingFile($file)
{
return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file));
}
/**
* Checks if $field type is valid
*
* @param object $meta
* @param string $field
*
* @return boolean
*/
protected function isValidField($meta, $field)
{
$mapping = $meta->getFieldMapping($field);
return $mapping && in_array($mapping['type'], $this->validTypes);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Gedmo\IpTraceable\Mapping\Event\Adapter;
use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM;
use Gedmo\IpTraceable\Mapping\Event\IpTraceableAdapter;
/**
* Doctrine event adapter for ODM adapted
* for IpTraceable behavior
*
* @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
final class ODM extends BaseAdapterODM implements IpTraceableAdapter
{
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Gedmo\IpTraceable\Mapping\Event\Adapter;
use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM;
use Gedmo\IpTraceable\Mapping\Event\IpTraceableAdapter;
/**
* Doctrine event adapter for ORM adapted
* for IpTraceable behavior
*
* @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
final class ORM extends BaseAdapterORM implements IpTraceableAdapter
{
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Gedmo\IpTraceable\Mapping\Event;
use Gedmo\Mapping\Event\AdapterInterface;
/**
* Doctrine event adapter interface
* for IpTraceable behavior
*
* @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
interface IpTraceableAdapter extends AdapterInterface
{
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Gedmo\IpTraceable\Traits;
/**
* IpTraceable Trait, usable with PHP >= 5.4
*
* @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
trait IpTraceable
{
/**
* @var string
*/
protected $createdFromIp;
/**
* @var string
*/
protected $updatedFromIp;
/**
* Sets createdFromIp.
*
* @param string $createdFromIp
* @return $this
*/
public function setCreatedFromIp($createdFromIp)
{
$this->createdFromIp = $createdFromIp;
return $this;
}
/**
* Returns createdFromIp.
*
* @return string
*/
public function getCreatedFromIp()
{
return $this->createdFromIp;
}
/**
* Sets updatedFromIp.
*
* @param string $updatedFromIp
* @return $this
*/
public function setUpdatedFromIp($updatedFromIp)
{
$this->updatedFromIp = $updatedFromIp;
return $this;
}
/**
* Returns updatedFromIp.
*
* @return string
*/
public function getUpdatedFromIp()
{
return $this->updatedFromIp;
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Gedmo\IpTraceable\Traits;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* IpTraceable Trait, usable with PHP >= 5.4
*
* @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
trait IpTraceableDocument
{
/**
* @var string
* @Gedmo\IpTraceable(on="create")
* @ODM\Field(type="string")
*/
protected $createdFromIp;
/**
* @var string
* @Gedmo\IpTraceable(on="update")
* @ODM\Field(type="string")
*/
protected $updatedFromIp;
/**
* Sets createdFromIp.
*
* @param string $createdFromIp
* @return $this
*/
public function setCreatedFromIp($createdFromIp)
{
$this->createdFromIp = $createdFromIp;
return $this;
}
/**
* Returns createdFromIp.
*
* @return string
*/
public function getCreatedFromIp()
{
return $this->createdFromIp;
}
/**
* Sets updatedFromIp.
*
* @param string $updatedFromIp
* @return $this
*/
public function setUpdatedFromIp($updatedFromIp)
{
$this->updatedFromIp = $updatedFromIp;
return $this;
}
/**
* Returns updatedFromIp.
*
* @return string
*/
public function getUpdatedFromIp()
{
return $this->updatedFromIp;
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Gedmo\IpTraceable\Traits;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* IpTraceable Trait, usable with PHP >= 5.4
*
* @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
trait IpTraceableEntity
{
/**
* @var string
* @Gedmo\IpTraceable(on="create")
* @ORM\Column(length=45, nullable=true)
*/
protected $createdFromIp;
/**
* @var string
* @Gedmo\IpTraceable(on="update")
* @ORM\Column(length=45, nullable=true)
*/
protected $updatedFromIp;
/**
* Sets createdFromIp.
*
* @param string $createdFromIp
* @return $this
*/
public function setCreatedFromIp($createdFromIp)
{
$this->createdFromIp = $createdFromIp;
return $this;
}
/**
* Returns createdFromIp.
*
* @return string
*/
public function getCreatedFromIp()
{
return $this->createdFromIp;
}
/**
* Sets updatedFromIp.
*
* @param string $updatedFromIp
* @return $this
*/
public function setUpdatedFromIp($updatedFromIp)
{
$this->updatedFromIp = $updatedFromIp;
return $this;
}
/**
* Returns updatedFromIp.
*
* @return string
*/
public function getUpdatedFromIp()
{
return $this->updatedFromIp;
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Gedmo\Loggable\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoODM;
/**
* Gedmo\Loggable\Document\LogEntry
*
* @MongoODM\Document(
* repositoryClass="Gedmo\Loggable\Document\Repository\LogEntryRepository",
* indexes={
* @MongoODM\Index(keys={"objectId"="asc", "objectClass"="asc", "version"="asc"}),
* @MongoODM\Index(keys={"loggedAt"="asc"}),
* @MongoODM\Index(keys={"objectClass"="asc"}),
* @MongoODM\Index(keys={"username"="asc"})
* }
* )
*/
class LogEntry extends MappedSuperclass\AbstractLogEntry
{
/**
* All required columns are mapped through inherited superclass
*/
}

View File

@@ -0,0 +1,217 @@
<?php
namespace Gedmo\Loggable\Document\MappedSuperclass;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoODM;
/**
* Gedmo\Loggable\Document\MappedSuperclass\AbstractLogEntry
*
* @MongoODM\MappedSuperclass
*/
abstract class AbstractLogEntry
{
/**
* @var integer $id
*
* @MongoODM\Id
*/
protected $id;
/**
* @var string $action
*
* @MongoODM\Field(type="string")
*/
protected $action;
/**
* @var \DateTime $loggedAt
*
* @MongoODM\Field(type="date")
*/
protected $loggedAt;
/**
* @var string $objectId
*
* @MongoODM\Field(type="string", nullable=true)
*/
protected $objectId;
/**
* @var string $objectClass
*
* @MongoODM\Field(type="string")
*/
protected $objectClass;
/**
* @var integer $version
*
* @MongoODM\Field(type="int")
*/
protected $version;
/**
* @var string $data
*
* @MongoODM\Field(type="hash", nullable=true)
*/
protected $data;
/**
* @var string $data
*
* @MongoODM\Field(type="string", nullable=true)
*/
protected $username;
/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Get action
*
* @return string
*/
public function getAction()
{
return $this->action;
}
/**
* Set action
*
* @param string $action
*/
public function setAction($action)
{
$this->action = $action;
}
/**
* Get object class
*
* @return string
*/
public function getObjectClass()
{
return $this->objectClass;
}
/**
* Set object class
*
* @param string $objectClass
*/
public function setObjectClass($objectClass)
{
$this->objectClass = $objectClass;
}
/**
* Get object id
*
* @return string
*/
public function getObjectId()
{
return $this->objectId;
}
/**
* Set object id
*
* @param string $objectId
*/
public function setObjectId($objectId)
{
$this->objectId = $objectId;
}
/**
* Get username
*
* @return string
*/
public function getUsername()
{
return $this->username;
}
/**
* Set username
*
* @param string $username
*/
public function setUsername($username)
{
$this->username = $username;
}
/**
* Get loggedAt
*
* @return \DateTime
*/
public function getLoggedAt()
{
return $this->loggedAt;
}
/**
* Set loggedAt to "now"
*/
public function setLoggedAt()
{
$this->loggedAt = new \DateTime();
}
/**
* Get data
*
* @return array or null
*/
public function getData()
{
return $this->data;
}
/**
* Set data
*
* @param array $data
*/
public function setData($data)
{
$this->data = $data;
}
/**
* Set current version
*
* @param integer $version
*/
public function setVersion($version)
{
$this->version = $version;
}
/**
* Get current version
*
* @return integer
*/
public function getVersion()
{
return $this->version;
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace Gedmo\Loggable\Document\Repository;
use Gedmo\Loggable\Document\LogEntry;
use Gedmo\Tool\Wrapper\MongoDocumentWrapper;
use Gedmo\Loggable\LoggableListener;
use Doctrine\ODM\MongoDB\DocumentRepository;
use Doctrine\ODM\MongoDB\Cursor;
/**
* The LogEntryRepository has some useful functions
* to interact with log entries.
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class LogEntryRepository extends DocumentRepository
{
/**
* Currently used loggable listener
*
* @var LoggableListener
*/
private $listener;
/**
* Loads all log entries for the
* given $document
*
* @param object $document
*
* @return LogEntry[]
*/
public function getLogEntries($document)
{
$wrapped = new MongoDocumentWrapper($document, $this->dm);
$objectId = $wrapped->getIdentifier();
$qb = $this->createQueryBuilder();
$qb->field('objectId')->equals($objectId);
$qb->field('objectClass')->equals($wrapped->getMetadata()->name);
$qb->sort('version', 'DESC');
$q = $qb->getQuery();
$result = $q->execute();
if ($result instanceof Cursor) {
$result = $result->toArray();
}
return $result;
}
/**
* Reverts given $document to $revision by
* restoring all fields from that $revision.
* After this operation you will need to
* persist and flush the $document.
*
* @param object $document
* @param integer $version
*
* @throws \Gedmo\Exception\UnexpectedValueException
*
* @return void
*/
public function revert($document, $version = 1)
{
$wrapped = new MongoDocumentWrapper($document, $this->dm);
$objectMeta = $wrapped->getMetadata();
$objectId = $wrapped->getIdentifier();
$qb = $this->createQueryBuilder();
$qb->field('objectId')->equals($objectId);
$qb->field('objectClass')->equals($objectMeta->name);
$qb->field('version')->lte(intval($version));
$qb->sort('version', 'ASC');
$q = $qb->getQuery();
$logs = $q->execute();
if ($logs instanceof Cursor) {
$logs = $logs->toArray();
}
if ($logs) {
$data = array();
while (($log = array_shift($logs))) {
$data = array_merge($data, $log->getData());
}
$this->fillDocument($document, $data, $objectMeta);
} else {
throw new \Gedmo\Exception\UnexpectedValueException('Count not find any log entries under version: '.$version);
}
}
/**
* Fills a documents versioned fields with data
*
* @param object $document
* @param array $data
*/
protected function fillDocument($document, array $data)
{
$wrapped = new MongoDocumentWrapper($document, $this->dm);
$objectMeta = $wrapped->getMetadata();
$config = $this->getLoggableListener()->getConfiguration($this->dm, $objectMeta->name);
$fields = $config['versioned'];
foreach ($data as $field => $value) {
if (!in_array($field, $fields)) {
continue;
}
$mapping = $objectMeta->getFieldMapping($field);
// Fill the embedded document
if ($wrapped->isEmbeddedAssociation($field)) {
if (!empty($value)) {
$embeddedMetadata = $this->dm->getClassMetadata($mapping['targetDocument']);
$document = $embeddedMetadata->newInstance();
$this->fillDocument($document, $value);
$value = $document;
}
} elseif ($objectMeta->isSingleValuedAssociation($field)) {
$value = $value ? $this->dm->getReference($mapping['targetDocument'], $value) : null;
}
$wrapped->setPropertyValue($field, $value);
unset($fields[$field]);
}
/*
if (count($fields)) {
throw new \Gedmo\Exception\UnexpectedValueException('Cound not fully revert the document to version: '.$version);
}
*/
}
/**
* Get the currently used LoggableListener
*
* @throws \Gedmo\Exception\RuntimeException - if listener is not found
*
* @return LoggableListener
*/
private function getLoggableListener()
{
if (is_null($this->listener)) {
foreach ($this->dm->getEventManager()->getListeners() as $event => $listeners) {
foreach ($listeners as $hash => $listener) {
if ($listener instanceof LoggableListener) {
$this->listener = $listener;
break;
}
}
if ($this->listener) {
break;
}
}
if (is_null($this->listener)) {
throw new \Gedmo\Exception\RuntimeException('The loggable listener could not be found');
}
}
return $this->listener;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Gedmo\Loggable\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Gedmo\Loggable\Entity\LogEntry
*
* @ORM\Table(
* name="ext_log_entries",
* options={"row_format":"DYNAMIC"},
* indexes={
* @ORM\Index(name="log_class_lookup_idx", columns={"object_class"}),
* @ORM\Index(name="log_date_lookup_idx", columns={"logged_at"}),
* @ORM\Index(name="log_user_lookup_idx", columns={"username"}),
* @ORM\Index(name="log_version_lookup_idx", columns={"object_id", "object_class", "version"})
* }
* )
* @ORM\Entity(repositoryClass="Gedmo\Loggable\Entity\Repository\LogEntryRepository")
*/
class LogEntry extends MappedSuperclass\AbstractLogEntry
{
/**
* All required columns are mapped through inherited superclass
*/
}

View File

@@ -0,0 +1,219 @@
<?php
namespace Gedmo\Loggable\Entity\MappedSuperclass;
use Doctrine\ORM\Mapping as ORM;
/**
* Gedmo\Loggable\Entity\AbstractLog
*
* @ORM\MappedSuperclass
*/
abstract class AbstractLogEntry
{
/**
* @var integer $id
*
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*/
protected $id;
/**
* @var string $action
*
* @ORM\Column(type="string", length=8)
*/
protected $action;
/**
* @var \DateTime $loggedAt
*
* @ORM\Column(name="logged_at", type="datetime")
*/
protected $loggedAt;
/**
* @var string $objectId
*
* @ORM\Column(name="object_id", length=64, nullable=true)
*/
protected $objectId;
/**
* @var string $objectClass
*
* @ORM\Column(name="object_class", type="string", length=255)
*/
protected $objectClass;
/**
* @var integer $version
*
* @ORM\Column(type="integer")
*/
protected $version;
/**
* @var array $data
*
* @ORM\Column(type="array", nullable=true)
*/
protected $data;
/**
* @var string $data
*
* @ORM\Column(length=255, nullable=true)
*/
protected $username;
/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Get action
*
* @return string
*/
public function getAction()
{
return $this->action;
}
/**
* Set action
*
* @param string $action
*/
public function setAction($action)
{
$this->action = $action;
}
/**
* Get object class
*
* @return string
*/
public function getObjectClass()
{
return $this->objectClass;
}
/**
* Set object class
*
* @param string $objectClass
*/
public function setObjectClass($objectClass)
{
$this->objectClass = $objectClass;
}
/**
* Get object id
*
* @return string
*/
public function getObjectId()
{
return $this->objectId;
}
/**
* Set object id
*
* @param string $objectId
*/
public function setObjectId($objectId)
{
$this->objectId = $objectId;
}
/**
* Get username
*
* @return string
*/
public function getUsername()
{
return $this->username;
}
/**
* Set username
*
* @param string $username
*/
public function setUsername($username)
{
$this->username = $username;
}
/**
* Get loggedAt
*
* @return \DateTime
*/
public function getLoggedAt()
{
return $this->loggedAt;
}
/**
* Set loggedAt to "now"
*/
public function setLoggedAt()
{
$this->loggedAt = new \DateTime();
}
/**
* Get data
*
* @return array
*/
public function getData()
{
return $this->data;
}
/**
* Set data
*
* @param array $data
*/
public function setData($data)
{
$this->data = $data;
}
/**
* Set current version
*
* @param integer $version
*/
public function setVersion($version)
{
$this->version = $version;
}
/**
* Get current version
*
* @return integer
*/
public function getVersion()
{
return $this->version;
}
}

View File

@@ -0,0 +1,164 @@
<?php
namespace Gedmo\Loggable\Entity\Repository;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query;
use Gedmo\Loggable\Entity\LogEntry;
use Gedmo\Tool\Wrapper\EntityWrapper;
use Doctrine\ORM\EntityRepository;
use Gedmo\Loggable\LoggableListener;
/**
* The LogEntryRepository has some useful functions
* to interact with log entries.
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class LogEntryRepository extends EntityRepository
{
/**
* Currently used loggable listener
*
* @var LoggableListener
*/
private $listener;
/**
* Loads all log entries for the given entity
*
* @param object $entity
*
* @return LogEntry[]
*/
public function getLogEntries($entity)
{
$q = $this->getLogEntriesQuery($entity);
return $q->getResult();
}
/**
* Get the query for loading of log entries
*
* @param object $entity
*
* @return Query
*/
public function getLogEntriesQuery($entity)
{
$wrapped = new EntityWrapper($entity, $this->_em);
$objectClass = $wrapped->getMetadata()->name;
$meta = $this->getClassMetadata();
$dql = "SELECT log FROM {$meta->name} log";
$dql .= " WHERE log.objectId = :objectId";
$dql .= " AND log.objectClass = :objectClass";
$dql .= " ORDER BY log.version DESC";
$objectId = (string) $wrapped->getIdentifier();
$q = $this->_em->createQuery($dql);
$q->setParameters(compact('objectId', 'objectClass'));
return $q;
}
/**
* Reverts given $entity to $revision by
* restoring all fields from that $revision.
* After this operation you will need to
* persist and flush the $entity.
*
* @param object $entity
* @param integer $version
*
* @throws \Gedmo\Exception\UnexpectedValueException
*
* @return void
*/
public function revert($entity, $version = 1)
{
$wrapped = new EntityWrapper($entity, $this->_em);
$objectMeta = $wrapped->getMetadata();
$objectClass = $objectMeta->name;
$meta = $this->getClassMetadata();
$dql = "SELECT log FROM {$meta->name} log";
$dql .= " WHERE log.objectId = :objectId";
$dql .= " AND log.objectClass = :objectClass";
$dql .= " AND log.version <= :version";
$dql .= " ORDER BY log.version ASC";
$objectId = (string) $wrapped->getIdentifier();
$q = $this->_em->createQuery($dql);
$q->setParameters(compact('objectId', 'objectClass', 'version'));
$logs = $q->getResult();
if ($logs) {
$config = $this->getLoggableListener()->getConfiguration($this->_em, $objectMeta->name);
$fields = $config['versioned'];
$filled = false;
while (($log = array_pop($logs)) && !$filled) {
if ($data = $log->getData()) {
foreach ($data as $field => $value) {
if (in_array($field, $fields)) {
$this->mapValue($objectMeta, $field, $value);
$wrapped->setPropertyValue($field, $value);
unset($fields[array_search($field, $fields)]);
}
}
}
$filled = count($fields) === 0;
}
/*if (count($fields)) {
throw new \Gedmo\Exception\UnexpectedValueException('Could not fully revert the entity to version: '.$version);
}*/
} else {
throw new \Gedmo\Exception\UnexpectedValueException('Could not find any log entries under version: '.$version);
}
}
/**
* @param ClassMetadata $objectMeta
* @param string $field
* @param mixed $value
*/
protected function mapValue(ClassMetadata $objectMeta, $field, &$value)
{
if (!$objectMeta->isSingleValuedAssociation($field)) {
return;
}
$mapping = $objectMeta->getAssociationMapping($field);
$value = $value ? $this->_em->getReference($mapping['targetEntity'], $value) : null;
}
/**
* Get the currently used LoggableListener
*
* @throws \Gedmo\Exception\RuntimeException - if listener is not found
*
* @return LoggableListener
*/
private function getLoggableListener()
{
if (is_null($this->listener)) {
foreach ($this->_em->getEventManager()->getListeners() as $event => $listeners) {
foreach ($listeners as $hash => $listener) {
if ($listener instanceof LoggableListener) {
$this->listener = $listener;
break;
}
}
if ($this->listener) {
break;
}
}
if (is_null($this->listener)) {
throw new \Gedmo\Exception\RuntimeException('The loggable listener could not be found');
}
}
return $this->listener;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Gedmo\Loggable;
/**
* This interface is not necessary but can be implemented for
* Domain Objects which in some cases needs to be identified as
* Loggable
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
interface Loggable
{
// this interface is not necessary to implement
/**
* @gedmo:Loggable
* to mark the class as loggable use class annotation @gedmo:Loggable
* this object will contain now a history
* available options:
* logEntryClass="My\LogEntryObject" (optional) defaultly will use internal object class
* example:
*
* @gedmo:Loggable(logEntryClass="My\LogEntryObject")
* class MyEntity
*/
}

View File

@@ -0,0 +1,324 @@
<?php
namespace Gedmo\Loggable;
use Doctrine\Common\EventArgs;
use Gedmo\Mapping\MappedEventSubscriber;
use Gedmo\Loggable\Mapping\Event\LoggableAdapter;
use Gedmo\Tool\Wrapper\AbstractWrapper;
/**
* Loggable listener
*
* @author Boussekeyt Jules <jules.boussekeyt@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class LoggableListener extends MappedEventSubscriber
{
/**
* Create action
*/
const ACTION_CREATE = 'create';
/**
* Update action
*/
const ACTION_UPDATE = 'update';
/**
* Remove action
*/
const ACTION_REMOVE = 'remove';
/**
* Username for identification
*
* @var string
*/
protected $username;
/**
* List of log entries which do not have the foreign
* key generated yet - MySQL case. These entries
* will be updated with new keys on postPersist event
*
* @var array
*/
protected $pendingLogEntryInserts = array();
/**
* For log of changed relations we use
* its identifiers to avoid storing serialized Proxies.
* These are pending relations in case it does not
* have an identifier yet
*
* @var array
*/
protected $pendingRelatedObjects = array();
/**
* Set username for identification
*
* @param mixed $username
*
* @throws \Gedmo\Exception\InvalidArgumentException Invalid username
*/
public function setUsername($username)
{
if (is_string($username)) {
$this->username = $username;
} elseif (is_object($username) && method_exists($username, 'getUsername')) {
$this->username = (string) $username->getUsername();
} else {
throw new \Gedmo\Exception\InvalidArgumentException("Username must be a string, or object should have method: getUsername");
}
}
/**
* {@inheritdoc}
*/
public function getSubscribedEvents()
{
return array(
'onFlush',
'loadClassMetadata',
'postPersist',
);
}
/**
* Get the LogEntry class
*
* @param LoggableAdapter $ea
* @param string $class
*
* @return string
*/
protected function getLogEntryClass(LoggableAdapter $ea, $class)
{
return isset(self::$configurations[$this->name][$class]['logEntryClass']) ?
self::$configurations[$this->name][$class]['logEntryClass'] :
$ea->getDefaultLogEntryClass();
}
/**
* Maps additional metadata
*
* @param EventArgs $eventArgs
*
* @return void
*/
public function loadClassMetadata(EventArgs $eventArgs)
{
$ea = $this->getEventAdapter($eventArgs);
$this->loadMetadataForObjectClass($ea->getObjectManager(), $eventArgs->getClassMetadata());
}
/**
* Checks for inserted object to update its logEntry
* foreign key
*
* @param EventArgs $args
*
* @return void
*/
public function postPersist(EventArgs $args)
{
$ea = $this->getEventAdapter($args);
$object = $ea->getObject();
$om = $ea->getObjectManager();
$oid = spl_object_hash($object);
$uow = $om->getUnitOfWork();
if ($this->pendingLogEntryInserts && array_key_exists($oid, $this->pendingLogEntryInserts)) {
$wrapped = AbstractWrapper::wrap($object, $om);
$logEntry = $this->pendingLogEntryInserts[$oid];
$logEntryMeta = $om->getClassMetadata(get_class($logEntry));
$id = $wrapped->getIdentifier();
$logEntryMeta->getReflectionProperty('objectId')->setValue($logEntry, $id);
$uow->scheduleExtraUpdate($logEntry, array(
'objectId' => array(null, $id),
));
$ea->setOriginalObjectProperty($uow, spl_object_hash($logEntry), 'objectId', $id);
unset($this->pendingLogEntryInserts[$oid]);
}
if ($this->pendingRelatedObjects && array_key_exists($oid, $this->pendingRelatedObjects)) {
$wrapped = AbstractWrapper::wrap($object, $om);
$identifiers = $wrapped->getIdentifier(false);
foreach ($this->pendingRelatedObjects[$oid] as $props) {
$logEntry = $props['log'];
$logEntryMeta = $om->getClassMetadata(get_class($logEntry));
$oldData = $data = $logEntry->getData();
$data[$props['field']] = $identifiers;
$logEntry->setData($data);
$uow->scheduleExtraUpdate($logEntry, array(
'data' => array($oldData, $data),
));
$ea->setOriginalObjectProperty($uow, spl_object_hash($logEntry), 'data', $data);
}
unset($this->pendingRelatedObjects[$oid]);
}
}
/**
* Handle any custom LogEntry functionality that needs to be performed
* before persisting it
*
* @param object $logEntry The LogEntry being persisted
* @param object $object The object being Logged
*/
protected function prePersistLogEntry($logEntry, $object)
{
}
/**
* Looks for loggable objects being inserted or updated
* for further processing
*
* @param EventArgs $eventArgs
*
* @return void
*/
public function onFlush(EventArgs $eventArgs)
{
$ea = $this->getEventAdapter($eventArgs);
$om = $ea->getObjectManager();
$uow = $om->getUnitOfWork();
foreach ($ea->getScheduledObjectInsertions($uow) as $object) {
$this->createLogEntry(self::ACTION_CREATE, $object, $ea);
}
foreach ($ea->getScheduledObjectUpdates($uow) as $object) {
$this->createLogEntry(self::ACTION_UPDATE, $object, $ea);
}
foreach ($ea->getScheduledObjectDeletions($uow) as $object) {
$this->createLogEntry(self::ACTION_REMOVE, $object, $ea);
}
}
/**
* {@inheritDoc}
*/
protected function getNamespace()
{
return __NAMESPACE__;
}
/**
* Returns an objects changeset data
*
* @param LoggableAdapter $ea
* @param object $object
* @param object $logEntry
*
* @return array
*/
protected function getObjectChangeSetData($ea, $object, $logEntry)
{
$om = $ea->getObjectManager();
$wrapped = AbstractWrapper::wrap($object, $om);
$meta = $wrapped->getMetadata();
$config = $this->getConfiguration($om, $meta->name);
$uow = $om->getUnitOfWork();
$newValues = array();
foreach ($ea->getObjectChangeSet($uow, $object) as $field => $changes) {
if (empty($config['versioned']) || !in_array($field, $config['versioned'])) {
continue;
}
$value = $changes[1];
if ($meta->isSingleValuedAssociation($field) && $value) {
if ($wrapped->isEmbeddedAssociation($field)) {
$value = $this->getObjectChangeSetData($ea, $value, $logEntry);
} else {
$oid = spl_object_hash($value);
$wrappedAssoc = AbstractWrapper::wrap($value, $om);
$value = $wrappedAssoc->getIdentifier(false);
if (!is_array($value) && !$value) {
$this->pendingRelatedObjects[$oid][] = array(
'log' => $logEntry,
'field' => $field,
);
}
}
}
$newValues[$field] = $value;
}
return $newValues;
}
/**
* Create a new Log instance
*
* @param string $action
* @param object $object
* @param LoggableAdapter $ea
*
* @return \Gedmo\Loggable\Entity\MappedSuperclass\AbstractLogEntry|null
*/
protected function createLogEntry($action, $object, LoggableAdapter $ea)
{
$om = $ea->getObjectManager();
$wrapped = AbstractWrapper::wrap($object, $om);
$meta = $wrapped->getMetadata();
// Filter embedded documents
if (isset($meta->isEmbeddedDocument) && $meta->isEmbeddedDocument) {
return;
}
if ($config = $this->getConfiguration($om, $meta->name)) {
$logEntryClass = $this->getLogEntryClass($ea, $meta->name);
$logEntryMeta = $om->getClassMetadata($logEntryClass);
/** @var \Gedmo\Loggable\Entity\LogEntry $logEntry */
$logEntry = $logEntryMeta->newInstance();
$logEntry->setAction($action);
$logEntry->setUsername($this->username);
$logEntry->setObjectClass($meta->name);
$logEntry->setLoggedAt();
// check for the availability of the primary key
$uow = $om->getUnitOfWork();
if ($action === self::ACTION_CREATE && $ea->isPostInsertGenerator($meta)) {
$this->pendingLogEntryInserts[spl_object_hash($object)] = $logEntry;
} else {
$logEntry->setObjectId($wrapped->getIdentifier());
}
$newValues = array();
if ($action !== self::ACTION_REMOVE && isset($config['versioned'])) {
$newValues = $this->getObjectChangeSetData($ea, $object, $logEntry);
$logEntry->setData($newValues);
}
if($action === self::ACTION_UPDATE && 0 === count($newValues)) {
return null;
}
$version = 1;
if ($action !== self::ACTION_CREATE) {
$version = $ea->getNewVersion($logEntryMeta, $object);
if (empty($version)) {
// was versioned later
$version = 1;
}
}
$logEntry->setVersion($version);
$this->prePersistLogEntry($logEntry, $object);
$om->persist($logEntry);
$uow->computeChangeSet($logEntryMeta, $logEntry);
return $logEntry;
}
return null;
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace Gedmo\Loggable\Mapping\Driver;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Gedmo\Exception\InvalidMappingException;
use Gedmo\Mapping\Driver\AbstractAnnotationDriver;
/**
* This is an annotation mapping driver for Loggable
* behavioral extension. Used for extraction of extended
* metadata from Annotations specifically for Loggable
* extension.
*
* @author Boussekeyt Jules <jules.boussekeyt@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class Annotation extends AbstractAnnotationDriver
{
/**
* Annotation to define that this object is loggable
*/
const LOGGABLE = 'Gedmo\\Mapping\\Annotation\\Loggable';
/**
* Annotation to define that this property is versioned
*/
const VERSIONED = 'Gedmo\\Mapping\\Annotation\\Versioned';
/**
* {@inheritDoc}
*/
public function validateFullMetadata(ClassMetadata $meta, array $config)
{
if ($config && is_array($meta->identifier) && count($meta->identifier) > 1) {
throw new InvalidMappingException("Loggable does not support composite identifiers in class - {$meta->name}");
}
if (isset($config['versioned']) && !isset($config['loggable'])) {
throw new InvalidMappingException("Class must be annotated with Loggable annotation in order to track versioned fields in class - {$meta->name}");
}
}
/**
* {@inheritDoc}
*/
public function readExtendedMetadata($meta, array &$config)
{
$class = $this->getMetaReflectionClass($meta);
// class annotations
if ($annot = $this->reader->getClassAnnotation($class, self::LOGGABLE)) {
$config['loggable'] = true;
if ($annot->logEntryClass) {
if (!$cl = $this->getRelatedClassName($meta, $annot->logEntryClass)) {
throw new InvalidMappingException("LogEntry class: {$annot->logEntryClass} does not exist.");
}
$config['logEntryClass'] = $cl;
}
}
// property annotations
foreach ($class->getProperties() as $property) {
$field = $property->getName();
if ($meta->isMappedSuperclass && !$property->isPrivate()) {
continue;
}
// versioned property
if ($this->reader->getPropertyAnnotation($property, self::VERSIONED)) {
if (!$this->isMappingValid($meta, $field)) {
throw new InvalidMappingException("Cannot apply versioning to field [{$field}] as it is collection in object - {$meta->name}");
}
if (isset($meta->embeddedClasses[$field])) {
$this->inspectEmbeddedForVersioned($field, $config, $meta);
continue;
}
// fields cannot be overrided and throws mapping exception
if (!(isset($config['versioned']) && in_array($field, $config['versioned']))) {
$config['versioned'][] = $field;
}
}
}
if (!$meta->isMappedSuperclass && $config) {
if (is_array($meta->identifier) && count($meta->identifier) > 1) {
throw new InvalidMappingException("Loggable does not support composite identifiers in class - {$meta->name}");
}
if ($this->isClassAnnotationInValid($meta, $config)) {
throw new InvalidMappingException("Class must be annotated with Loggable annotation in order to track versioned fields in class - {$meta->name}");
}
}
}
/**
* @param ClassMetadata $meta
* @param string $field
*
* @return bool
*/
protected function isMappingValid(ClassMetadata $meta, $field)
{
return $meta->isCollectionValuedAssociation($field) == false;
}
/**
* @param ClassMetadata $meta
* @param array $config
*
* @return bool
*/
protected function isClassAnnotationInValid(ClassMetadata $meta, array &$config)
{
return isset($config['versioned']) && !isset($config['loggable']) && (!isset($meta->isEmbeddedClass) || !$meta->isEmbeddedClass);
}
/**
* Searches properties of embedded object for versioned fields
*
* @param string $field
* @param array $config
* @param \Doctrine\ORM\Mapping\ClassMetadata $meta
*/
private function inspectEmbeddedForVersioned($field, array &$config, \Doctrine\ORM\Mapping\ClassMetadata $meta)
{
$сlass = new \ReflectionClass($meta->embeddedClasses[$field]['class']);
// property annotations
foreach ($сlass->getProperties() as $property) {
// versioned property
if ($this->reader->getPropertyAnnotation($property, self::VERSIONED)) {
$embeddedField = $field . '.' . $property->getName();
$config['versioned'][] = $embeddedField;
if (isset($meta->embeddedClasses[$embeddedField])) {
$this->inspectEmbeddedForVersioned($embeddedField, $config, $meta);
}
}
}
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace Gedmo\Loggable\Mapping\Driver;
use Gedmo\Mapping\Driver\Xml as BaseXml;
use Gedmo\Exception\InvalidMappingException;
/**
* This is a xml mapping driver for Loggable
* behavioral extension. Used for extraction of extended
* metadata from xml specifically for Loggable
* extension.
*
* @author Boussekeyt Jules <jules.boussekeyt@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @author Miha Vrhovnik <miha.vrhovnik@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class Xml extends BaseXml
{
/**
* {@inheritDoc}
*/
public function readExtendedMetadata($meta, array &$config)
{
/**
* @var \SimpleXmlElement $xml
*/
$xml = $this->_getMapping($meta->name);
$xmlDoctrine = $xml;
$xml = $xml->children(self::GEDMO_NAMESPACE_URI);
if ($xmlDoctrine->getName() == 'entity' || $xmlDoctrine->getName() == 'document' || $xmlDoctrine->getName() == 'mapped-superclass') {
if (isset($xml->loggable)) {
/**
* @var \SimpleXMLElement $data;
*/
$data = $xml->loggable;
$config['loggable'] = true;
if ($this->_isAttributeSet($data, 'log-entry-class')) {
$class = $this->_getAttribute($data, 'log-entry-class');
if (!$cl = $this->getRelatedClassName($meta, $class)) {
throw new InvalidMappingException("LogEntry class: {$class} does not exist.");
}
$config['logEntryClass'] = $cl;
}
}
}
if (isset($xmlDoctrine->field)) {
$this->inspectElementForVersioned($xmlDoctrine->field, $config, $meta);
}
if (isset($xmlDoctrine->{'many-to-one'})) {
$this->inspectElementForVersioned($xmlDoctrine->{'many-to-one'}, $config, $meta);
}
if (isset($xmlDoctrine->{'one-to-one'})) {
$this->inspectElementForVersioned($xmlDoctrine->{'one-to-one'}, $config, $meta);
}
if (isset($xmlDoctrine->{'reference-one'})) {
$this->inspectElementForVersioned($xmlDoctrine->{'reference-one'}, $config, $meta);
}
if (isset($xmlDoctrine->{'embedded'})) {
$this->inspectElementForVersioned($xmlDoctrine->{'embedded'}, $config, $meta);
}
if (!$meta->isMappedSuperclass && $config) {
if (is_array($meta->identifier) && count($meta->identifier) > 1) {
throw new InvalidMappingException("Loggable does not support composite identifiers in class - {$meta->name}");
}
if (isset($config['versioned']) && !isset($config['loggable'])) {
throw new InvalidMappingException("Class must be annotated with Loggable annotation in order to track versioned fields in class - {$meta->name}");
}
}
}
/**
* Searches mappings on element for versioned fields
*
* @param \SimpleXMLElement $element
* @param array $config
* @param object $meta
*/
private function inspectElementForVersioned(\SimpleXMLElement $element, array &$config, $meta)
{
foreach ($element as $mapping) {
$mappingDoctrine = $mapping;
/**
* @var \SimpleXmlElement $mapping
*/
$mapping = $mapping->children(self::GEDMO_NAMESPACE_URI);
$isAssoc = $this->_isAttributeSet($mappingDoctrine, 'field');
$field = $this->_getAttribute($mappingDoctrine, $isAssoc ? 'field' : 'name');
if (isset($mapping->versioned)) {
if ($isAssoc && !$meta->associationMappings[$field]['isOwningSide']) {
throw new InvalidMappingException("Cannot version [{$field}] as it is not the owning side in object - {$meta->name}");
}
$config['versioned'][] = $field;
}
}
}
}

View File

@@ -0,0 +1,149 @@
<?php
namespace Gedmo\Loggable\Mapping\Driver;
use Gedmo\Mapping\Driver\File;
use Gedmo\Mapping\Driver;
use Gedmo\Exception\InvalidMappingException;
/**
* This is a yaml mapping driver for Loggable
* behavioral extension. Used for extraction of extended
* metadata from yaml specifically for Loggable
* extension.
*
* @author Boussekeyt Jules <jules.boussekeyt@gmail.com>
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class Yaml extends File implements Driver
{
/**
* File extension
* @var string
*/
protected $_extension = '.dcm.yml';
/**
* {@inheritDoc}
*/
public function readExtendedMetadata($meta, array &$config)
{
$mapping = $this->_getMapping($meta->name);
if (isset($mapping['gedmo'])) {
$classMapping = $mapping['gedmo'];
if (isset($classMapping['loggable'])) {
$config['loggable'] = true;
if (isset ($classMapping['loggable']['logEntryClass'])) {
if (!$cl = $this->getRelatedClassName($meta, $classMapping['loggable']['logEntryClass'])) {
throw new InvalidMappingException("LogEntry class: {$classMapping['loggable']['logEntryClass']} does not exist.");
}
$config['logEntryClass'] = $cl;
}
}
}
if (isset($mapping['fields'])) {
foreach ($mapping['fields'] as $field => $fieldMapping) {
if (isset($fieldMapping['gedmo'])) {
if (in_array('versioned', $fieldMapping['gedmo'])) {
if ($meta->isCollectionValuedAssociation($field)) {
throw new InvalidMappingException("Cannot apply versioning to field [{$field}] as it is collection in object - {$meta->name}");
}
// fields cannot be overrided and throws mapping exception
$config['versioned'][] = $field;
}
}
}
}
if (isset($mapping['attributeOverride'])) {
foreach ($mapping['attributeOverride'] as $field => $fieldMapping) {
if (isset($fieldMapping['gedmo'])) {
if (in_array('versioned', $fieldMapping['gedmo'])) {
if ($meta->isCollectionValuedAssociation($field)) {
throw new InvalidMappingException("Cannot apply versioning to field [{$field}] as it is collection in object - {$meta->name}");
}
// fields cannot be overrided and throws mapping exception
$config['versioned'][] = $field;
}
}
}
}
if (isset($mapping['manyToOne'])) {
foreach ($mapping['manyToOne'] as $field => $fieldMapping) {
if (isset($fieldMapping['gedmo'])) {
if (in_array('versioned', $fieldMapping['gedmo'])) {
if ($meta->isCollectionValuedAssociation($field)) {
throw new InvalidMappingException("Cannot apply versioning to field [{$field}] as it is collection in object - {$meta->name}");
}
// fields cannot be overrided and throws mapping exception
$config['versioned'][] = $field;
}
}
}
}
if (isset($mapping['oneToOne'])) {
foreach ($mapping['oneToOne'] as $field => $fieldMapping) {
if (isset($fieldMapping['gedmo'])) {
if (in_array('versioned', $fieldMapping['gedmo'])) {
if ($meta->isCollectionValuedAssociation($field)) {
throw new InvalidMappingException("Cannot apply versioning to field [{$field}] as it is collection in object - {$meta->name}");
}
// fields cannot be overrided and throws mapping exception
$config['versioned'][] = $field;
}
}
}
}
if (isset($mapping['embedded'])) {
foreach ($mapping['embedded'] as $field => $fieldMapping) {
if (isset($fieldMapping['gedmo'])) {
if (in_array('versioned', $fieldMapping['gedmo'])) {
if ($meta->isCollectionValuedAssociation($field)) {
throw new InvalidMappingException("Cannot apply versioning to field [{$field}] as it is collection in object - {$meta->name}");
}
// fields cannot be overrided and throws mapping exception
$mapping = $this->_getMapping($fieldMapping['class']);
$this->inspectEmbeddedForVersioned($field, $mapping, $config);
}
}
}
}
if (!$meta->isMappedSuperclass && $config) {
if (is_array($meta->identifier) && count($meta->identifier) > 1) {
throw new InvalidMappingException("Loggable does not support composite identifiers in class - {$meta->name}");
}
if (isset($config['versioned']) && !isset($config['loggable'])) {
throw new InvalidMappingException("Class must be annoted with Loggable annotation in order to track versioned fields in class - {$meta->name}");
}
}
}
/**
* {@inheritDoc}
*/
protected function _loadMappingFile($file)
{
return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file));
}
/**
* @param string $field
* @param array $mapping
* @param array $config
*/
private function inspectEmbeddedForVersioned($field, array $mapping, array &$config)
{
if (isset($mapping['fields'])) {
foreach ($mapping['fields'] as $property => $fieldMapping) {
$config['versioned'][] = $field . '.' . $property;
}
}
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Gedmo\Loggable\Mapping\Event\Adapter;
use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM;
use Gedmo\Loggable\Mapping\Event\LoggableAdapter;
/**
* Doctrine event adapter for ODM adapted
* for Loggable behavior
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
final class ODM extends BaseAdapterODM implements LoggableAdapter
{
/**
* {@inheritDoc}
*/
public function getDefaultLogEntryClass()
{
return 'Gedmo\\Loggable\\Document\\LogEntry';
}
/**
* {@inheritDoc}
*/
public function isPostInsertGenerator($meta)
{
return false;
}
/**
* {@inheritDoc}
*/
public function getNewVersion($meta, $object)
{
$dm = $this->getObjectManager();
$objectMeta = $dm->getClassMetadata(get_class($object));
$identifierField = $this->getSingleIdentifierFieldName($objectMeta);
$objectId = $objectMeta->getReflectionProperty($identifierField)->getValue($object);
$qb = $dm->createQueryBuilder($meta->name);
$qb->select('version');
$qb->field('objectId')->equals($objectId);
$qb->field('objectClass')->equals($objectMeta->name);
$qb->sort('version', 'DESC');
$qb->limit(1);
$q = $qb->getQuery();
$q->setHydrate(false);
$result = $q->getSingleResult();
if ($result) {
$result = $result['version'] + 1;
}
return $result;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Gedmo\Loggable\Mapping\Event\Adapter;
use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM;
use Gedmo\Loggable\Mapping\Event\LoggableAdapter;
/**
* Doctrine event adapter for ORM adapted
* for Loggable behavior
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
final class ORM extends BaseAdapterORM implements LoggableAdapter
{
/**
* {@inheritDoc}
*/
public function getDefaultLogEntryClass()
{
return 'Gedmo\\Loggable\\Entity\\LogEntry';
}
/**
* {@inheritDoc}
*/
public function isPostInsertGenerator($meta)
{
return $meta->idGenerator->isPostInsertGenerator();
}
/**
* {@inheritDoc}
*/
public function getNewVersion($meta, $object)
{
$em = $this->getObjectManager();
$objectMeta = $em->getClassMetadata(get_class($object));
$identifierField = $this->getSingleIdentifierFieldName($objectMeta);
$objectId = (string) $objectMeta->getReflectionProperty($identifierField)->getValue($object);
$dql = "SELECT MAX(log.version) FROM {$meta->name} log";
$dql .= " WHERE log.objectId = :objectId";
$dql .= " AND log.objectClass = :objectClass";
$q = $em->createQuery($dql);
$q->setParameters(array(
'objectId' => $objectId,
'objectClass' => $objectMeta->name,
));
return $q->getSingleScalarResult() + 1;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Gedmo\Loggable\Mapping\Event;
use Gedmo\Mapping\Event\AdapterInterface;
/**
* Doctrine event adapter interface
* for Loggable behavior
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
interface LoggableAdapter extends AdapterInterface
{
/**
* Get default LogEntry class used to store the logs
*
* @return string
*/
public function getDefaultLogEntryClass();
/**
* Checks whether an id should be generated post insert
*
* @return boolean
*/
public function isPostInsertGenerator($meta);
/**
* Get new version number
*
* @param object $meta
* @param object $object
*
* @return integer
*/
public function getNewVersion($meta, $object);
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* Contains all annotations for extensions
* NOTE: should be included with require_once
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
foreach (glob(__DIR__ . "/*.php") as $filename) {
if (basename($filename, '.php') === 'All') {
continue;
}
include_once $filename;
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Gedmo\Mapping\Annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* Blameable annotation for Blameable behavioral extension
*
* @Annotation
* @Target("PROPERTY")
*
* @author David Buchmann <mail@davidbu.ch>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
final class Blameable extends Annotation
{
/** @var string */
public $on = 'update';
/** @var string|array */
public $field;
/** @var mixed */
public $value;
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Gedmo\Mapping\Annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* IpTraceable annotation for IpTraceable behavioral extension
*
* @Annotation
* @Target("PROPERTY")
*
* @author Pierre-Charles Bertineau <pc.bertineau@alterphp.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
final class IpTraceable extends Annotation
{
/** @var string */
public $on = 'update';
/** @var string|array */
public $field;
/** @var mixed */
public $value;
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Mapping\Annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* Language annotation for Translatable behavioral extension
*
* @Annotation
* @Target("PROPERTY")
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
final class Language extends Annotation
{
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Gedmo\Mapping\Annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* Locale annotation for Translatable behavioral extension
*
* @Annotation
* @Target("PROPERTY")
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
final class Locale extends Annotation
{
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Gedmo\Mapping\Annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* Loggable annotation for Loggable behavioral extension
*
* @Annotation
* @Target("CLASS")
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
final class Loggable extends Annotation
{
/** @var string */
public $logEntryClass;
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Gedmo\Mapping\Annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* Reference annotation for ORM -> ODM references extension
* to be user like "@ReferenceMany(type="entity", class="MyEntity", identifier="entity_id")"
*
* @author Bulat Shakirzyanov <mallluhuct@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
* @Annotation
*/
abstract class Reference extends Annotation
{
public $type;
public $class;
public $identifier;
public $mappedBy;
public $inversedBy;
}

Some files were not shown because too many files have changed in this diff Show More