init without trunk
This commit is contained in:
parent
ed24ac4994
commit
bb809e7233
14652 changed files with 177862 additions and 94817 deletions
566
vendor/gedmo/doctrine-extensions/doc/annotations.md
vendored
Normal file
566
vendor/gedmo/doctrine-extensions/doc/annotations.md
vendored
Normal 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;
|
||||
```
|
||||
644
vendor/gedmo/doctrine-extensions/doc/blameable.md
vendored
Normal file
644
vendor/gedmo/doctrine-extensions/doc/blameable.md
vendored
Normal 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", fields={"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.
|
||||
660
vendor/gedmo/doctrine-extensions/doc/ip_traceable.md
vendored
Normal file
660
vendor/gedmo/doctrine-extensions/doc/ip_traceable.md
vendored
Normal 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>
|
||||
|
||||
```
|
||||
266
vendor/gedmo/doctrine-extensions/doc/loggable.md
vendored
Normal file
266
vendor/gedmo/doctrine-extensions/doc/loggable.md
vendored
Normal 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
|
||||
491
vendor/gedmo/doctrine-extensions/doc/mapping.md
vendored
Normal file
491
vendor/gedmo/doctrine-extensions/doc/mapping.md
vendored
Normal 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
|
||||
171
vendor/gedmo/doctrine-extensions/doc/reference_integrity.md
vendored
Normal file
171
vendor/gedmo/doctrine-extensions/doc/reference_integrity.md
vendored
Normal 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
|
||||
219
vendor/gedmo/doctrine-extensions/doc/references.md
vendored
Normal file
219
vendor/gedmo/doctrine-extensions/doc/references.md
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
```
|
||||
810
vendor/gedmo/doctrine-extensions/doc/sluggable.md
vendored
Normal file
810
vendor/gedmo/doctrine-extensions/doc/sluggable.md
vendored
Normal 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
|
||||
286
vendor/gedmo/doctrine-extensions/doc/softdeleteable.md
vendored
Normal file
286
vendor/gedmo/doctrine-extensions/doc/softdeleteable.md
vendored
Normal 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.
|
||||
328
vendor/gedmo/doctrine-extensions/doc/sortable.md
vendored
Normal file
328
vendor/gedmo/doctrine-extensions/doc/sortable.md
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
```
|
||||
500
vendor/gedmo/doctrine-extensions/doc/symfony2.md
vendored
Normal file
500
vendor/gedmo/doctrine-extensions/doc/symfony2.md
vendored
Normal 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.
|
||||
434
vendor/gedmo/doctrine-extensions/doc/symfony4.md
vendored
Normal file
434
vendor/gedmo/doctrine-extensions/doc/symfony4.md
vendored
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
# 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)
|
||||
- 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 **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 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-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/packages/doctrine_extensions.yml**
|
||||
|
||||
```yaml
|
||||
# services to handle doctrine extensions
|
||||
# import it in config.yml
|
||||
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\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 **Acme** 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
|
||||
*/
|
||||
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 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/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 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.
|
||||
674
vendor/gedmo/doctrine-extensions/doc/timestampable.md
vendored
Normal file
674
vendor/gedmo/doctrine-extensions/doc/timestampable.md
vendored
Normal 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.
|
||||
234
vendor/gedmo/doctrine-extensions/doc/transaction-safety.md
vendored
Normal file
234
vendor/gedmo/doctrine-extensions/doc/transaction-safety.md
vendored
Normal 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.
|
||||
|
||||
894
vendor/gedmo/doctrine-extensions/doc/translatable.md
vendored
Normal file
894
vendor/gedmo/doctrine-extensions/doc/translatable.md
vendored
Normal 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.
|
||||
1339
vendor/gedmo/doctrine-extensions/doc/tree.md
vendored
Normal file
1339
vendor/gedmo/doctrine-extensions/doc/tree.md
vendored
Normal file
File diff suppressed because it is too large
Load diff
467
vendor/gedmo/doctrine-extensions/doc/uploadable.md
vendored
Normal file
467
vendor/gedmo/doctrine-extensions/doc/uploadable.md
vendored
Normal 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/l3pp4rd/DoctrineExtensions/blob/master/doc/annotations.md#em-setup)
|
||||
or check the [example code](http://github.com/l3pp4rd/DoctrineExtensions/tree/master/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());
|
||||
|
||||
```
|
||||
97
vendor/gedmo/doctrine-extensions/doc/zendframework2.md
vendored
Normal file
97
vendor/gedmo/doctrine-extensions/doc/zendframework2.md
vendored
Normal 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',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue