This commit is contained in:
Xes
2025-08-14 22:41:49 +02:00
parent 2de81ccc46
commit 8ce45119b6
39774 changed files with 4309466 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
composer.phar
vendor
composer.lock
bin

View File

@@ -0,0 +1,58 @@
language: php
sudo: false
php:
- 7.0
- 7.1
matrix:
fast_finish: true
exclude:
- php: 7.0
env: DOCTRINE_COMMON_VERSION="dev-master" DB=mysql DB_USER=root PREFER=latest
- php: 7.0
env: DOCTRINE_COMMON_VERSION="dev-master" DB=pgsql PREFER=latest
- php: 7.0
env: DOCTRINE_COMMON_VERSION="dev-master" DB=mysql DB_USER=root PREFER=lowest
- php: 7.0
env: DOCTRINE_COMMON_VERSION="dev-master" DB=pgsql PREFER=lowest
env:
matrix:
- DOCTRINE_COMMON_VERSION="~2.4.0" DB=mysql DB_USER=root PREFER=latest
- DOCTRINE_COMMON_VERSION="~2.5.0" DB=mysql DB_USER=root PREFER=latest
- DOCTRINE_COMMON_VERSION="~2.6.0" DB=mysql DB_USER=root PREFER=latest
- DOCTRINE_COMMON_VERSION="dev-master" DB=mysql DB_USER=root PREFER=latest
- DOCTRINE_COMMON_VERSION="~2.4.0" DB=pgsql PREFER=latest
- DOCTRINE_COMMON_VERSION="~2.5.0" DB=pgsql PREFER=latest
- DOCTRINE_COMMON_VERSION="~2.6.0" DB=pgsql PREFER=latest
- DOCTRINE_COMMON_VERSION="dev-master" DB=pgsql PREFER=latest
- DOCTRINE_COMMON_VERSION="~2.4.0" DB=mysql DB_USER=root PREFER=lowest
- DOCTRINE_COMMON_VERSION="~2.5.0" DB=mysql DB_USER=root PREFER=lowest
- DOCTRINE_COMMON_VERSION="~2.6.0" DB=mysql DB_USER=root PREFER=lowest
- DOCTRINE_COMMON_VERSION="dev-master" DB=mysql DB_USER=root PREFER=lowest
- DOCTRINE_COMMON_VERSION="~2.4.0" DB=pgsql PREFER=lowest
- DOCTRINE_COMMON_VERSION="~2.5.0" DB=pgsql PREFER=lowest
- DOCTRINE_COMMON_VERSION="~2.6.0" DB=pgsql PREFER=lowest
- DOCTRINE_COMMON_VERSION="dev-master" DB=pgsql PREFER=lowest
global:
- secure: "VKbBEUu1CftMOWU+dKRmV3KWMB/2rtTYbQh3jR2cgtIZuELPc/QvCgUO2YsiG686vaOOJt9pwVW+CAmQBManRxJEGvJMD3LI3/gif8HH478DYobRO8QLgpICGZrpMx85i8y9ln69gigERdEkHo8ILtwYTRzV4nK9LqVXVTxcnjQ="
cache:
directories:
- $HOME/.composer/cache
before_script:
- composer self-update
- sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'create database orm_behaviors_test' -U postgres; fi"
- sh -c "if [ '$DB' = 'pgsql' ]; then psql orm_behaviors_test -c 'create extension cube' -U postgres; fi"
- sh -c "if [ '$DB' = 'pgsql' ]; then psql orm_behaviors_test -c 'create extension earthdistance' -U postgres; fi"
- sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'create database IF NOT EXISTS orm_behaviors_test' -u$DB_USER; fi"
- composer require doctrine/common:${DOCTRINE_COMMON_VERSION}
- sh -c "if [ '$PREFER' = 'lowest' ]; then composer update --prefer-lowest; fi"
script:
- bin/parallel-lint -e php,phpt --exclude vendor .
- bin/yaml-lint config/*
- bin/phpunit

View File

@@ -0,0 +1,10 @@
# Contributing
Please make sure the test suite runs fine before and after contributing.
## Running the tests
Everything you need to do before running the test appears in `.travis.yml`. Make
sure you run the phpunit version bundled with the library, like this :
bin/phpunit

View File

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

View File

@@ -0,0 +1,749 @@
# Doctrine2 Behaviors
[![Build Status](https://travis-ci.org/KnpLabs/DoctrineBehaviors.svg?branch=master)](http://travis-ci.org/KnpLabs/DoctrineBehaviors)
* Branch `master` contains the next v2.0
* Branch `v1` contains the current v1.x
This PHP `>=7.0` library is a collection of traits and interfaces
that add behaviors to Doctrine2 entities and repositories.
It currently handles:
* [blameable](#blameable)
* [filterable](#filterable)
* [geocodable](#geocodable)
* joinable
* [loggable](#loggable)
* [sluggable](#sluggable)
* [softDeletable](#softDeletable)
* sortable
* [timestampable](#timestampable)
* [translatable](#translatable)
* [tree](#tree)
## This project is looking for maintainers
We realize we don't have so much time anymore to maintain this project as it should be maintained.
Therefore we are looking for maintainers. Open an issue if you want to keep working on this.
## Notice:
Some behaviors (translatable, timestampable, softDeletable, blameable, geocodable) need Doctrine subscribers in order to work.
Make sure to activate them by reading the [Subscribers](#subscribers) section.
## Installation
```
composer require knplabs/doctrine-behaviors:~1.1
```
## Configuration
By default, when integrated with Symfony, all subscribers are enabled (if you don't specify any configuration for the bundle).
But you can enable behaviors you need in a whitelist manner:
```yaml
knp_doctrine_behaviors:
blameable: false
geocodable: ~ # Here null is converted to false
loggable: ~
sluggable: true
soft_deletable: true
# All others behaviors are disabled
```
<a name="subscribers" id="subscribers"></a>
## Subscribers
If you use symfony2, you can easily register them in:
- *Recommended way:*
Add to AppKernel
```php
class AppKernel
{
function registerBundles()
{
$bundles = array(
//...
new Knp\DoctrineBehaviors\Bundle\DoctrineBehaviorsBundle(),
//...
);
//...
return $bundles;
}
}
```
- *Deprecated way:*
Importing a service definition file:
``` yaml
# app/config/config.yml
imports:
- { resource: ../../vendor/knplabs/doctrine-behaviors/config/orm-services.yml }
```
You can also register them using doctrine2 api:
``` php
<?php
$em->getEventManager()->addEventSubscriber(new \Knp\DoctrineBehaviors\ORM\Translatable\TranslatableSubscriber);
// register more if needed
```
## Usage
All you have to do is to define a Doctrine2 entity and use traits:
``` php
<?php
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model as ORMBehaviors;
/**
* @ORM\Entity(repositoryClass="CategoryRepository")
*/
class Category implements ORMBehaviors\Tree\NodeInterface, \ArrayAccess
{
use ORMBehaviors\Blameable\Blameable,
ORMBehaviors\Geocodable\Geocodable,
ORMBehaviors\Loggable\Loggable,
ORMBehaviors\Sluggable\Sluggable,
ORMBehaviors\SoftDeletable\SoftDeletable,
ORMBehaviors\Sortable\Sortable,
ORMBehaviors\Timestampable\Timestampable,
ORMBehaviors\Translatable\Translatable,
ORMBehaviors\Tree\Node
;
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="NONE")
*/
protected $id;
}
```
For some behaviors like tree, you can use repository traits:
``` php
<?php
use Doctrine\ORM\EntityRepository;
use Knp\DoctrineBehaviors\ORM as ORMBehaviors;
class CategoryRepository extends EntityRepository
{
use ORMBehaviors\Tree\Tree,
}
```
Voila!
You now have a working `Category` that behaves like:
<a name="tree" id="tree"></a>
### tree:
``` php
<?php
$category = new Category;
$category->setId(1); // tree nodes need an id to construct path.
$child = new Category;
$child->setId(2);
$child->setChildNodeOf($category);
$em->persist($child);
$em->persist($category);
$em->flush();
$root = $em->getRepository('Category')->getTree();
$root->getParentNode(); // null
$root->getChildNodes(); // ArrayCollection
$root[0][1]; // node or null
$root->isLeafNode(); // boolean
$root->isRootNode(); // boolean
```
> it is possible to use another identifier than `id`, simply override `getNodeId` and return your custom identifier (works great in combination with `Sluggable`)
<a name="translatable" id="translatable"></a>
### translatable:
If you're working on a `Category` entity, the `Translatable` behavior expects a **CategoryTranslation** entity in the
same folder of Category entity by default.
The default naming convention (or its customization via trait methods) avoids you to manually handle entity associations.
It is handled automatically by the TranslationSubscriber.
In order to use the Translatable trait, you will have to create this `CategoryTranslation` entity.
``` php
<?php
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model as ORMBehaviors;
/**
* @ORM\Entity
*/
class CategoryTranslation
{
use ORMBehaviors\Translatable\Translation;
/**
* @ORM\Column(type="string", length=255)
*/
protected $name;
/**
* @ORM\Column(type="string", length=255)
*/
protected $description;
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @param string
* @return null
*/
public function setName($name)
{
$this->name = $name;
}
/**
* @return string
*/
public function getDescription()
{
return $this->description;
}
/**
* @param string
* @return null
*/
public function setDescription($description)
{
$this->description = $description;
}
}
```
The corresponding Category entity needs to `use ORMBehaviors\Translatable\Translatable;`
and should only contain fields that you do not need to translate.
``` php
<?php
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model as ORMBehaviors;
/**
* @ORM\Entity
*/
class Category
{
use ORMBehaviors\Translatable\Translatable;
/**
* @ORM\Column(type="string", length=255)
*/
protected $someFieldYouDoNotNeedToTranslate;
}
```
After updating the database, ie. with `./console doctrine:schema:update --force`,
you can now work on translations using `translate` or `getTranslations` methods.
``` php
<?php
$category = new Category;
$category->translate('fr')->setName('Chaussures');
$category->translate('en')->setName('Shoes');
$em->persist($category);
// In order to persist new translations, call mergeNewTranslations method, before flush
$category->mergeNewTranslations();
$category->translate('en')->getName();
```
#### Override
In case you prefer to use a different class name for the translation entity,
or want to use a separate namespace, you have 2 ways :
If you want to define a custom translation entity class name globally :
Override the trait `Translatable` and his method `getTranslationEntityClass`
and the trait `Translation` and his method `getTranslatableEntityClass` in the translation entity.
If you override one, you also need to override the other to return the inverse class.
Example: Let's say you want to create a sub namespace AppBundle\Entity\Translation to stock translations classes
then put overrided traits in that folder.
``` php
<?php
namespace AppBundle\Entity\Translation;
use Knp\DoctrineBehaviors\Model\Translatable\Translatable;
use Symfony\Component\PropertyAccess\PropertyAccess;
trait TranslatableTrait
{
use Translatable;
/**
* @inheritdoc
*/
public static function getTranslationEntityClass()
{
$explodedNamespace = explode('\\', __CLASS__);
$entityClass = array_pop($explodedNamespace);
return '\\'.implode('\\', $explodedNamespace).'\\Translation\\'.$entityClass.'Translation';
}
}
```
``` php
<?php
namespace AppBundle\Entity\Translation;
use Knp\DoctrineBehaviors\Model\Translatable\Translation;
trait TranslationTrait
{
use Translation;
/**
* @inheritdoc
*/
public static function getTranslatableEntityClass()
{
$explodedNamespace = explode('\\', __CLASS__);
$entityClass = array_pop($explodedNamespace);
// Remove Translation namespace
array_pop($explodedNamespace);
return '\\'.implode('\\', $explodedNamespace).'\\'.substr($entityClass, 0, -11);
}
}
```
If you use that way make sure you override trait parameters of DoctrineBehaviors :
``` yaml
parameters:
knp.doctrine_behaviors.translatable_subscriber.translatable_trait: AppBundle\Entity\Translation\TranslatableTrait
knp.doctrine_behaviors.translatable_subscriber.translation_trait: AppBundle\Entity\Translation\TranslationTrait
```
If you want to define a custom translation entity class name just for a single translatable class :
Override the trait method `getTranslationEntityClass` in the translatable entity and `getTranslatableEntityClass`
in the translation entity. If you override one, you also need to override the other to return the inverse class.
#### guess the current locale
You can configure the way the subscriber guesses the current locale, by giving a callable as its first argument.
This library provides a callable object (`Knp\DoctrineBehaviors\ORM\Translatable\CurrentLocaleCallable`) that returns the current locale using Symfony2.
#### proxy translations
An extra feature allows you to proxy translated fields of a translatable entity.
You can use it in the magic `__call` method of you translatable entity
so that when you try to call `getName` (for example) it will return you the translated value of the name for current locale:
``` php
<?php
public function __call($method, $arguments)
{
return $this->proxyCurrentLocaleTranslation($method, $arguments);
}
// or do it with PropertyAccessor that ships with Symfony SE
// if your methods don't take any required arguments
public function __call($method, $arguments)
{
return \Symfony\Component\PropertyAccess\PropertyAccess::createPropertyAccessor()->getValue($this->translate(), $method);
}
```
<a name="softDeletable" id="softDeletable"></a>
### soft-deletable
``` php
<?php
$category = new Category;
$em->persist($category);
$em->flush();
// get id
$id = $category->getId();
// now remove it
$em->remove($category);
$em->flush();
// hey, I'm still here:
$category = $em->getRepository('Category')->findOneById($id);
// but I'm "deleted"
$category->isDeleted(); // === true
// restore me
$category->restore();
//look ma, I am back
$category->isDeleted(); // === false
//do not forget to call flush method to apply the change
$em->flush();
```
``` php
<?php
$category = new Category;
$em->persist($category);
$em->flush();
// I'll delete you tomorrow
$category->setDeletedAt((new \DateTime())->modify('+1 day'));
// OK, I'm here
$category->isDeleted(); // === false
/*
* 24 hours later...
*/
// OK, I'm deleted
$category->isDeleted(); // === true
```
<a name="timestampable" id="timestampable"></a>
### timestampable
``` php
<?php
$category = new Category;
$em->persist($category);
$em->flush();
$id = $category->getId();
$category = $em->getRepository('Category')->findOneById($id);
$category->getCreatedAt();
$category->getUpdatedAt();
```
If you wish to change the doctrine type of the database fields that will be created for timestampable models you can
set the following parameter like so:
``` yaml
parameters:
knp.doctrine_behaviors.timestampable_subscriber.db_field_type: datetimetz
```
`datetimetz` here is a useful one to use if you are working with a Postgres database, otherwise you may encounter some
timezone issues. For more information on this see:
<a href="http://doctrine-dbal.readthedocs.org/en/latest/reference/known-vendor-issues.html#datetime-datetimetz-and-time-types">http://doctrine-dbal.readthedocs.org/en/latest/reference/known-vendor-issues.html#datetime-datetimetz-and-time-types</a>
The default type is `datetime`.
<a name="blameable" id="blameable"></a>
### blameable
Blameable is able to track creators and updators of a given entity.
A blameable [callable](#callables) is used to get the current user from your application.
In the case you are using a Doctrine Entity to represent your users, you can configure the subscriber
to manage automatically the association between this user entity and your entites.
Using symfony2, all you have to do is to configure the DI parameter named `%knp.doctrine_behaviors.blameable_subscriber.user_entity%` with a fully qualified namespace,
for example:
# app/config/config.yml
parameters:
knp.doctrine_behaviors.blameable_subscriber.user_entity: AppBundle\Entity\User
Then, you can use it like that:
``` php
<?php
$category = new Category;
$em->persist($category);
// instances of %knp.doctrine_behaviors.blameable_subscriber.user_entity%
$creator = $category->getCreatedBy();
$updater = $category->getUpdatedBy();
```
<a name="loggable" id="loggable"></a>
### loggable
Loggable is able to track lifecycle modifications and log them using any third party log system.
A loggable [callable](#callables) is used to get the logger from anywhere you want.
``` php
<?php
/**
* @ORM\Entity
*/
class Category
{
use ORMBehaviors\Loggable\Loggable;
// you can override the default log messages defined in trait:
public function getUpdateLogMessage(array $changeSets = [])
{
return 'Changed: '.print_r($changeSets, true);
}
public function getRemoveLogMessage()
{
return 'removed!';
}
}
```
These messages are then passed to the configured callable.
You can define your own, by passing another callable to the LoggableSubscriber:
``` php
<?php
$em->getEventManager()->addEventSubscriber(
new \Knp\DoctrineBehaviors\ORM\Loggable\LoggableSubscriber(
new ClassAnalyzer,
function($message) {
// do stuff with message
}
)
);
```
If you're using symfony, you can also configure which callable to use:
// app/config/config.yml
parameters:
knp.doctrine_behaviors.loggable_subscriber.logger_callable.class: Your\InvokableClass
<a name="geocodable" id="geocodable"></a>
### geocodable
Geocodable Provides extensions to PostgreSQL platform in order to work with cube and earthdistance extensions.
It allows you to query entities based on geographical coordinates.
It also provides an easy entry point to use 3rd party libraries like the excellent [geocoder](https://github.com/willdurand/Geocoder) to transform addresses into latitude and longitude.
``` php
<?php
$geocoder = new \Geocoder\Geocoder;
// register geocoder providers
// $subscriber instanceof GeocodableSubscriber (add "knp.doctrine_behaviors.geocodable_subscriber" into your services.yml)
$subscriber->setGeolocationCallable(function($entity) use($geocoder) {
$location = $geocoder->geocode($entity->getAddress());
return new Point(
$location->getLatitude(),
$location->getLongitude()
));
});
$category = new Category;
$em->persist($category);
$location = $category->getLocation(); // instanceof Point
// find cities in a circle of 500 km around point 47 lon., 7 lat.
$nearCities = $repository->findByDistance(new Point(47, 7), 500);
```
<a name="sluggable" id="sluggable"></a>
### sluggable
Sluggable generates slugs (uniqueness is not guaranteed) for an entity.
Will automatically generate on update/persist (you can disable the on update generation by overriding `getRegenerateSlugOnUpdate` to return false.
You can also override the slug delimiter from the default hyphen by overriding `getSlugDelimiter`.
Slug generation algo can be changed by overriding `generateSlugValue`.
Use cases include SEO (i.e. URLs like http://example.com/post/3/introduction-to-php)
```php
<?php
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model as ORMBehaviors;
/**
* @ORM\Entity
*/
class BlogPost
{
use ORMBehaviors\Sluggable\Sluggable;
/**
* @ORM\Column(type="string")
*/
protected $title;
public function getSluggableFields()
{
return [ 'title' ];
}
public function generateSlugValue($values)
{
return implode('-', $values);
}
}
```
<a name="filterable" id="filterable"></a>
### filterable:
Filterable can be used at the Repository level
It allows to simple filter our result
Joined filters example:
```php
<?php
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="ProductRepository")
*/
class ProductEntity
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(type="string", nullable=true)
*/
private $name;
/**
* @ORM\Column(type="integer")
*/
private $code;
/**
* @ORM\OneToMany(targetEntity="Order", mappedBy="product")
*/
protected $orders;
}
```
and repository:
```php
<?php
use Knp\DoctrineBehaviors\ORM\Filterable;
use Doctrine\ORM\EntityRepository;
class ProductRepository extends EntityRepository
{
use Filterable\FilterableRepository;
public function getLikeFilterColumns()
{
return ['e:name', 'o:code'];
}
public function getEqualFilterColumns()
{
return [];
}
protected function createFilterQueryBuilder()
{
return $this
->createQueryBuilder('e')
->leftJoin('e.orders', 'o');
}
}
```
Now we can filtering using:
```php
$products = $em->getRepository('Product')->filterBy(['o:code' => '21']);
```
<a name="callables" id="callables"></a>
## callables
Callables are used by some subscribers like blameable and geocodable to fill information based on 3rd party system.
For example, the blameable callable can be any symfony2 service that implements `__invoke` method or any anonymous function, as soon as they return currently logged in user representation (which means everything, a User entity, a string, a username, ...).
For an example of DI service that is invoked, look at the `Knp\DoctrineBehaviors\ORM\Blameable\UserCallable` class.
In the case of geocodable, you can set it as any service that implements `__invoke` or anonymous function that returns a `Knp\DoctrineBehaviors\ORM\Geocodable\Type\Point` object.
## Testing
[Read the documentation for testing ](doc/test.md)
## Maintainers
KNPLabs is looking for maintainers ([see why](https://knplabs.com/en/blog/news-for-our-foss-projects-maintenance)).
If you are interested, feel free to open a PR to ask to be added as a maintainer.
Well be glad to hear from you :)

View File

@@ -0,0 +1,10 @@
# 1.0.2 to 1.0.x-dev
Most occurences of "listener" have been replaced with "subscriber" to honor a
subtle difference between the two that is explained
[in the documentation](http://doctrine-orm.readthedocs.org/en/latest/reference/events.html#listening-and-subscribing-to-lifecycle-events).
If you use translatable, you should map your translation classes `id` fields,
the `id` mapping is deprecated and will be removed in version 2.0
If you decided to create your own subscriber by extending a class from this library,
you might have to refactor your code because of this.
Likewise, DI parameters (for class names), and service ids have changed.

View File

@@ -0,0 +1,53 @@
{
"name": "knplabs/doctrine-behaviors",
"type": "library",
"description": "Doctrine2 behavior traits",
"keywords": [
"behaviors",
"doctrine2",
"timestampable",
"translatable",
"filterable",
"blameable",
"softdeletable",
"tree"
],
"homepage": "http://knplabs.com",
"license": "MIT",
"authors": [
{
"name": "Knplabs",
"homepage": "http://knplabs.com"
}
],
"require": {
"php": "~7.0",
"doctrine/common": ">=2.2",
"behat/transliterator": "~1.0"
},
"require-dev": {
"ext-pdo_sqlite": "*",
"ext-pdo_mysql": "*",
"ext-pdo_pgsql": "*",
"doctrine/orm": ">=2.4.5",
"phpunit/phpunit": "~4.8",
"jakub-onderka/php-parallel-lint": "~0.8",
"hexmedia/yaml-linter": "~0.1"
},
"suggest": {
"symfony/framework-bundle": "To be able to use it as a bundle"
},
"autoload": {
"psr-4": { "Knp\\DoctrineBehaviors\\": "src/" }
},
"minimum-stability": "dev",
"prefer-stable": true,
"config": {
"bin-dir": "bin"
},
"extra": {
"branch-alias": {
"dev-master": "1.5.x-dev"
}
}
}

View File

@@ -0,0 +1,157 @@
parameters:
knp.doctrine_behaviors.reflection.class_analyzer.class: Knp\DoctrineBehaviors\Reflection\ClassAnalyzer
knp.doctrine_behaviors.reflection.is_recursive: true
knp.doctrine_behaviors.translatable_subscriber.class: Knp\DoctrineBehaviors\ORM\Translatable\TranslatableSubscriber
knp.doctrine_behaviors.translatable_subscriber.current_locale_callable.class: Knp\DoctrineBehaviors\ORM\Translatable\CurrentLocaleCallable
knp.doctrine_behaviors.translatable_subscriber.default_locale_callable.class: Knp\DoctrineBehaviors\ORM\Translatable\DefaultLocaleCallable
knp.doctrine_behaviors.translatable_subscriber.translatable_trait: Knp\DoctrineBehaviors\Model\Translatable\Translatable
knp.doctrine_behaviors.translatable_subscriber.translation_trait: Knp\DoctrineBehaviors\Model\Translatable\Translation
knp.doctrine_behaviors.translatable_subscriber.translatable_fetch_method: LAZY
knp.doctrine_behaviors.translatable_subscriber.translation_fetch_method: LAZY
knp.doctrine_behaviors.softdeletable_subscriber.class: Knp\DoctrineBehaviors\ORM\SoftDeletable\SoftDeletableSubscriber
knp.doctrine_behaviors.softdeletable_subscriber.softdeletable_trait: Knp\DoctrineBehaviors\Model\SoftDeletable\SoftDeletable
knp.doctrine_behaviors.timestampable_subscriber.class: Knp\DoctrineBehaviors\ORM\Timestampable\TimestampableSubscriber
knp.doctrine_behaviors.timestampable_subscriber.timestampable_trait: Knp\DoctrineBehaviors\Model\Timestampable\Timestampable
knp.doctrine_behaviors.timestampable_subscriber.db_field_type: datetime
knp.doctrine_behaviors.blameable_subscriber.class: Knp\DoctrineBehaviors\ORM\Blameable\BlameableSubscriber
knp.doctrine_behaviors.blameable_subscriber.blameable_trait: Knp\DoctrineBehaviors\Model\Blameable\Blameable
knp.doctrine_behaviors.blameable_subscriber.user_callable.class: Knp\DoctrineBehaviors\ORM\Blameable\UserCallable
knp.doctrine_behaviors.blameable_subscriber.user_entity: ~
knp.doctrine_behaviors.loggable_subscriber.class: Knp\DoctrineBehaviors\ORM\Loggable\LoggableSubscriber
knp.doctrine_behaviors.loggable_subscriber.logger_callable.class: Knp\DoctrineBehaviors\ORM\Loggable\LoggerCallable
knp.doctrine_behaviors.geocodable_subscriber.class: Knp\DoctrineBehaviors\ORM\Geocodable\GeocodableSubscriber
knp.doctrine_behaviors.geocodable_subscriber.geocodable_trait: Knp\DoctrineBehaviors\Model\Geocodable\Geocodable
knp.doctrine_behaviors.sluggable_subscriber.class: Knp\DoctrineBehaviors\ORM\Sluggable\SluggableSubscriber
knp.doctrine_behaviors.sluggable_subscriber.sluggable_trait: Knp\DoctrineBehaviors\Model\Sluggable\Sluggable
knp.doctrine_behaviors.tree_subscriber.class: Knp\DoctrineBehaviors\ORM\Tree\TreeSubscriber
knp.doctrine_behaviors.tree_subscriber.tree_trait: Knp\DoctrineBehaviors\Model\Tree\Node
knp.doctrine_behaviors.sortable_subscriber.class: Knp\DoctrineBehaviors\ORM\Sortable\SortableSubscriber
knp.doctrine_behaviors.sortable_subscriber.sortable_trait: Knp\DoctrineBehaviors\Model\Sortable\Sortable
services:
knp.doctrine_behaviors.reflection.class_analyzer:
class: "%knp.doctrine_behaviors.reflection.class_analyzer.class%"
public: false
knp.doctrine_behaviors.translatable_subscriber:
class: "%knp.doctrine_behaviors.translatable_subscriber.class%"
public: false
arguments:
- "@knp.doctrine_behaviors.reflection.class_analyzer"
- "@knp.doctrine_behaviors.translatable_subscriber.current_locale_callable"
- "@knp.doctrine_behaviors.translatable_subscriber.default_locale_callable"
- "%knp.doctrine_behaviors.translatable_subscriber.translatable_trait%"
- "%knp.doctrine_behaviors.translatable_subscriber.translation_trait%"
- "%knp.doctrine_behaviors.translatable_subscriber.translatable_fetch_method%"
- "%knp.doctrine_behaviors.translatable_subscriber.translation_fetch_method%"
tags:
- { name: doctrine.event_subscriber }
knp.doctrine_behaviors.translatable_subscriber.current_locale_callable:
class: "%knp.doctrine_behaviors.translatable_subscriber.current_locale_callable.class%"
arguments:
- "@service_container" # lazy request resolution
public: false
knp.doctrine_behaviors.translatable_subscriber.default_locale_callable:
class: "%knp.doctrine_behaviors.translatable_subscriber.default_locale_callable.class%"
arguments:
- "%locale%"
public: false
knp.doctrine_behaviors.softdeletable_subscriber:
class: "%knp.doctrine_behaviors.softdeletable_subscriber.class%"
public: false
arguments:
- "@knp.doctrine_behaviors.reflection.class_analyzer"
- "%knp.doctrine_behaviors.reflection.is_recursive%"
- "%knp.doctrine_behaviors.softdeletable_subscriber.softdeletable_trait%"
tags:
- { name: doctrine.event_subscriber }
knp.doctrine_behaviors.timestampable_subscriber:
class: "%knp.doctrine_behaviors.timestampable_subscriber.class%"
public: false
arguments:
- "@knp.doctrine_behaviors.reflection.class_analyzer"
- "%knp.doctrine_behaviors.reflection.is_recursive%"
- "%knp.doctrine_behaviors.timestampable_subscriber.timestampable_trait%"
- "%knp.doctrine_behaviors.timestampable_subscriber.db_field_type%"
tags:
- { name: doctrine.event_subscriber }
knp.doctrine_behaviors.tree_subscriber:
class: "%knp.doctrine_behaviors.tree_subscriber.class%"
public: false
arguments:
- "@knp.doctrine_behaviors.reflection.class_analyzer"
- "%knp.doctrine_behaviors.reflection.is_recursive%"
- "%knp.doctrine_behaviors.tree_subscriber.tree_trait%"
tags:
- { name: doctrine.event_subscriber }
knp.doctrine_behaviors.sortable_subscriber:
class: "%knp.doctrine_behaviors.sortable_subscriber.class%"
public: false
arguments:
- "@knp.doctrine_behaviors.reflection.class_analyzer"
- "%knp.doctrine_behaviors.reflection.is_recursive%"
- "%knp.doctrine_behaviors.sortable_subscriber.sortable_trait%"
tags:
- { name: doctrine.event_subscriber }
knp.doctrine_behaviors.blameable_subscriber:
class: "%knp.doctrine_behaviors.blameable_subscriber.class%"
arguments:
- "@knp.doctrine_behaviors.reflection.class_analyzer"
- "%knp.doctrine_behaviors.reflection.is_recursive%"
- "%knp.doctrine_behaviors.blameable_subscriber.blameable_trait%"
- "@knp.doctrine_behaviors.blameable_subscriber.user_callable"
- "%knp.doctrine_behaviors.blameable_subscriber.user_entity%"
public: false
tags:
- { name: doctrine.event_subscriber }
knp.doctrine_behaviors.blameable_subscriber.user_callable:
class: "%knp.doctrine_behaviors.blameable_subscriber.user_callable.class%"
arguments:
- "@service_container" # because of circular dep
public: false
knp.doctrine_behaviors.loggable_subscriber:
class: "%knp.doctrine_behaviors.loggable_subscriber.class%"
arguments:
- "@knp.doctrine_behaviors.reflection.class_analyzer"
- "%knp.doctrine_behaviors.reflection.is_recursive%"
- "@knp.doctrine_behaviors.loggable_subscriber.logger_callable"
public: false
tags:
- { name: doctrine.event_subscriber }
knp.doctrine_behaviors.loggable_subscriber.logger_callable:
class: "%knp.doctrine_behaviors.loggable_subscriber.logger_callable.class%"
arguments:
- "@logger"
public: false
knp.doctrine_behaviors.geocodable_subscriber:
class: "%knp.doctrine_behaviors.geocodable_subscriber.class%"
public: false
arguments:
- "@knp.doctrine_behaviors.reflection.class_analyzer"
- "%knp.doctrine_behaviors.reflection.is_recursive%"
- "%knp.doctrine_behaviors.geocodable_subscriber.geocodable_trait%"
- "@?knp.doctrine_behaviors.geocodable_callable"
tags:
- { name: doctrine.event_subscriber }
knp.doctrine_behaviors.sluggable_subscriber:
class: "%knp.doctrine_behaviors.sluggable_subscriber.class%"
public: false
arguments:
- "@knp.doctrine_behaviors.reflection.class_analyzer"
- "%knp.doctrine_behaviors.reflection.is_recursive%"
- "%knp.doctrine_behaviors.sluggable_subscriber.sluggable_trait%"
tags:
- { name: doctrine.event_subscriber }

View File

@@ -0,0 +1,19 @@
# Testing
run test from console
``` bash
$ bin/phpunit
```
or you can setup vars for pdo driver like this
``` bash
export DB_NAME=acme && export DB_USER=acme && export DB_PASSWD=acme && export DB=mysql && export DB_HOST=acme && bin/phpunit
```
according to default credentials for travis CI you must run
``` bash
export DB_NAME=orm_behaviors_test && export DB_USER=root && unset DB_PASSWD && unset DB && unset DB_HOST && bin/phpunit
```

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="tests/bootstrap.php"
>
<testsuites>
<testsuite name="Knp Doctrine2 Behaviors">
<directory suffix=".php">tests</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@@ -0,0 +1,56 @@
<?php
namespace Knp\DoctrineBehaviors\Bundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$builder = new TreeBuilder('knp_doctrine_behaviors');
if (method_exists($builder, 'getRootNode')) {
$rootNode = $builder->getRootNode();
} else {
// for symfony/config 4.1 and older
$rootNode = $builder->root('knp_doctrine_behaviors');
}
$rootNode
->beforeNormalization()
->always(function (array $config) {
if (empty($config)) {
return [
'blameable' => true,
'geocodable' => true,
'loggable' => true,
'sluggable' => true,
'soft_deletable' => true,
'sortable' => true,
'timestampable' => true,
'translatable' => true,
'tree' => true,
];
}
return $config;
})
->end()
->children()
->booleanNode('blameable')->defaultFalse()->treatNullLike(false)->end()
->booleanNode('geocodable')->defaultFalse()->treatNullLike(false)->end()
->booleanNode('loggable')->defaultFalse()->treatNullLike(false)->end()
->booleanNode('sluggable')->defaultFalse()->treatNullLike(false)->end()
->booleanNode('soft_deletable')->defaultFalse()->treatNullLike(false)->end()
->booleanNode('sortable')->defaultFalse()->treatNullLike(false)->end()
->booleanNode('timestampable')->defaultFalse()->treatNullLike(false)->end()
->booleanNode('translatable')->defaultFalse()->treatNullLike(false)->end()
->booleanNode('tree')->defaultFalse()->treatNullLike(false)->end()
->end()
;
return $builder;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Knp\DoctrineBehaviors\Bundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
class DoctrineBehaviorsExtension extends Extension
{
/**
* {@inheritDoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../../../config'));
$loader->load('orm-services.yml');
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
// Don't rename in Configuration for BC reasons
$config['softdeletable'] = $config['soft_deletable'];
unset($config['soft_deletable']);
foreach ($config as $behavior => $enabled) {
if (!$enabled) {
$container->removeDefinition(sprintf('knp.doctrine_behaviors.%s_subscriber', $behavior));
}
}
}
/**
* {@inheritDoc}
*/
public function getAlias()
{
return 'knp_doctrine_behaviors';
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Knp\DoctrineBehaviors\Bundle;
use Knp\DoctrineBehaviors\Bundle\DependencyInjection\DoctrineBehaviorsExtension;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class DoctrineBehaviorsBundle extends Bundle
{
public function getContainerExtension()
{
return new DoctrineBehaviorsExtension();
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace Knp\DoctrineBehaviors\DBAL\Types;
use Doctrine\DBAL\Platforms\MySqlPlatform;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Knp\DoctrineBehaviors\ORM\Geocodable\Type\Point;
/**
* Mapping type for spatial POINT objects
*/
class PointType extends Type
{
/**
* Gets the name of this type.
*
* @return string
*/
public function getName()
{
return 'point';
}
/**
* Returns the SQL declaration snippet for a field of this type.
*
* @param array $fieldDeclaration The field declaration.
* @param AbstractPlatform $platform The currently used database platform.
*
* @return string
*/
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return 'POINT';
}
/**
* Converts SQL value to the PHP representation.
*
* @param string $value value in DB format
* @param AbstractPlatform $platform DB platform
*
* @return Point
*/
public function convertToPHPValue($value, AbstractPlatform $platform)
{
if (!$value) {
return null;
}
if ($platform instanceof MySqlPlatform) {
$data = sscanf($value, 'POINT(%f %f)');
} else {
$data = sscanf($value, "(%f,%f)");
}
return new Point($data[0], $data[1]);
}
/**
* Converts PHP representation to the SQL value.
*
* @param Point $value specific point
* @param AbstractPlatform $platform DB platform
*
* @return string
*/
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if (!$value) {
return null;
}
if ($platform instanceof MySqlPlatform) {
$format = "POINT(%F %F)";
} else {
$format = "(%F, %F)";
}
return sprintf($format, $value->getLatitude(), $value->getLongitude());
}
/**
* Does working with this column require SQL conversion functions?
*
* This is a metadata function that is required for example in the ORM.
* Usage of {@link convertToDatabaseValueSQL} and
* {@link convertToPHPValueSQL} works for any type and mostly
* does nothing. This method can additionally be used for optimization purposes.
*
* @return boolean
*/
public function canRequireSQLConversion()
{
return true;
}
/**
* Modifies the SQL expression (identifier, parameter) to convert to a database value.
*
* @param string $sqlExpr
* @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform
*
* @return string
*/
public function convertToDatabaseValueSQL($sqlExpr, AbstractPlatform $platform)
{
if ($platform instanceof MySqlPlatform) {
return sprintf('PointFromText(%s)', $sqlExpr);
} else {
return parent::convertToDatabaseValueSQL($sqlExpr, $platform);
}
}
/**
* @param string $sqlExpr
* @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform
*
* @return string
*/
public function convertToPHPValueSQL($sqlExpr, $platform)
{
if ($platform instanceof MySqlPlatform) {
return sprintf('AsText(%s)', $sqlExpr);
} else {
return parent::convertToPHPValueSQL($sqlExpr, $platform);
}
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\Model\Blameable;
/**
* Blameable trait.
*
* Should be used inside entity where you need to track which user created or updated it
*/
trait Blameable
{
use BlameableProperties,
BlameableMethods
;
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\Model\Blameable;
/**
* Blameable trait.
*
* Should be used inside entity where you need to track which user created or updated it
*/
trait BlameableMethods
{
/**
* @param mixed the user representation
* @return $this
*/
public function setCreatedBy($user)
{
$this->createdBy = $user;
return $this;
}
/**
* @param mixed the user representation
* @return $this
*/
public function setUpdatedBy($user)
{
$this->updatedBy = $user;
return $this;
}
/**
* @param mixed the user representation
* @return $this
*/
public function setDeletedBy($user)
{
$this->deletedBy = $user;
return $this;
}
/**
* @return mixed the user who created entity
*/
public function getCreatedBy()
{
return $this->createdBy;
}
/**
* @return mixed the user who last updated entity
*/
public function getUpdatedBy()
{
return $this->updatedBy;
}
/**
* @return mixed the user who removed entity
*/
public function getDeletedBy()
{
return $this->deletedBy;
}
public function isBlameable()
{
return true;
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\Model\Blameable;
/**
* Blameable trait.
*
* Should be used inside entity where you need to track which user created or updated it
*/
trait BlameableProperties
{
/**
* Will be mapped to either string or user entity
* by BlameableSubscriber
*/
protected $createdBy;
/**
* Will be mapped to either string or user entity
* by BlameableSubscriber
*/
protected $updatedBy;
/**
* Will be mapped to either string or user entity
* by BlameableSubscriber
*/
protected $deletedBy;
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\Model\Geocodable;
use Knp\DoctrineBehaviors\ORM\Geocodable\Type\Point;
/**
* Geocodable trait.
*
* Should be used inside entity where you need to manipulate geographical information
*/
trait Geocodable
{
use GeocodableProperties,
GeocodableMethods
;
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\Model\Geocodable;
use Knp\DoctrineBehaviors\ORM\Geocodable\Type\Point;
/**
* Geocodable trait.
*
* Should be used inside entity where you need to manipulate geographical information
*/
trait GeocodableMethods
{
/**
* Get location.
*
* @return Point.
*/
public function getLocation()
{
return $this->location;
}
/**
* Set location.
*
* @param Point|null $location the value to set.
*
* @return $this
*/
public function setLocation(Point $location = null)
{
$this->location = $location;
return $this;
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\Model\Geocodable;
use Knp\DoctrineBehaviors\ORM\Geocodable\Type\Point;
/**
* Geocodable trait.
*
* Should be used inside entity where you need to manipulate geographical information
*/
trait GeocodableProperties
{
protected $location;
}

View File

@@ -0,0 +1,58 @@
<?php
/**
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\Model\Loggable;
/**
* Loggable trait.
*
* Should be used inside entity where you need to track modifications log
*/
trait Loggable
{
/**
* @return string some log informations
*/
public function getUpdateLogMessage(array $changeSets = [])
{
$message = [];
foreach ($changeSets as $property => $changeSet) {
for ($i = 0, $s = sizeof($changeSet); $i < $s; $i++) {
if ($changeSet[$i] instanceof \DateTime) {
$changeSet[$i] = $changeSet[$i]->format("Y-m-d H:i:s.u");
}
}
if ($changeSet[0] != $changeSet[1]) {
$message[] = sprintf(
'%s #%d : property "%s" changed from "%s" to "%s"',
__CLASS__,
$this->getId(),
$property,
!is_array($changeSet[0]) ? $changeSet[0] : "an array",
!is_array($changeSet[1]) ? $changeSet[1] : "an array"
);
}
}
return implode("\n", $message);
}
public function getCreateLogMessage()
{
return sprintf('%s #%d created', __CLASS__, $this->getId());
}
public function getRemoveLogMessage()
{
return sprintf('%s #%d removed', __CLASS__, $this->getId());
}
}

View File

@@ -0,0 +1,19 @@
<?php
/**
* @author Lusitanian
* Freely released with no restrictions, re-license however you'd like!
*/
namespace Knp\DoctrineBehaviors\Model\Sluggable;
/**
* Sluggable trait.
*
* Should be used inside entities for which slugs should automatically be generated on creation for SEO/permalinks.
*/
trait Sluggable
{
use SluggableProperties,
SluggableMethods
;
}

View File

@@ -0,0 +1,125 @@
<?php
/**
* @author Lusitanian
* Freely released with no restrictions, re-license however you'd like!
*/
namespace Knp\DoctrineBehaviors\Model\Sluggable;
/**
* Sluggable trait.
*
* Should be used inside entities for which slugs should automatically be generated on creation for SEO/permalinks.
*/
trait SluggableMethods
{
/**
* Returns an array of the fields used to generate the slug.
*
* @abstract
* @return array
*/
abstract public function getSluggableFields();
/**
* Returns the slug's delimiter
*
* @return string
*/
private function getSlugDelimiter()
{
return '-';
}
/**
* Returns whether or not the slug gets regenerated on update.
*
* @return bool
*/
private function getRegenerateSlugOnUpdate()
{
return true;
}
/**
* Sets the entity's slug.
*
* @param $slug
* @return $this
*/
public function setSlug($slug)
{
$this->slug = $slug;
return $this;
}
/**
* Returns the entity's slug.
*
* @return string
*/
public function getSlug()
{
return $this->slug;
}
/**
* @param $values
* @return mixed|string
*/
private function generateSlugValue($values)
{
$usableValues = [];
foreach ($values as $fieldName => $fieldValue) {
if (!empty($fieldValue)) {
$usableValues[] = $fieldValue;
}
}
if (count($usableValues) < 1) {
throw new \UnexpectedValueException(
'Sluggable expects to have at least one usable (non-empty) field from the following: [ ' . implode(array_keys($values), ',') .' ]'
);
}
// generate the slug itself
$sluggableText = implode(' ', $usableValues);
$transliterator = new Transliterator;
$sluggableText = $transliterator->transliterate($sluggableText, $this->getSlugDelimiter());
$urlized = strtolower( trim( preg_replace("/[^a-zA-Z0-9\/_|+ -]/", '', $sluggableText ), $this->getSlugDelimiter() ) );
$urlized = preg_replace("/[\/_|+ -]+/", $this->getSlugDelimiter(), $urlized);
return $urlized;
}
/**
* Generates and sets the entity's slug. Called prePersist and preUpdate
*/
public function generateSlug()
{
if ( $this->getRegenerateSlugOnUpdate() || empty( $this->slug ) ) {
$fields = $this->getSluggableFields();
$values = [];
foreach ($fields as $field) {
if (property_exists($this, $field)) {
$val = $this->{$field};
} else {
$methodName = 'get' . ucfirst($field);
if (method_exists($this, $methodName)) {
$val = $this->{$methodName}();
} else {
$val = null;
}
}
$values[] = $val;
}
$this->slug = $this->generateSlugValue($values);
}
}
}

View File

@@ -0,0 +1,17 @@
<?php
/**
* @author Lusitanian
* Freely released with no restrictions, re-license however you'd like!
*/
namespace Knp\DoctrineBehaviors\Model\Sluggable;
/**
* Sluggable trait.
*
* Should be used inside entities for which slugs should automatically be generated on creation for SEO/permalinks.
*/
trait SluggableProperties
{
protected $slug;
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* @author Lusitanian
* Freely released with no restrictions, re-license however you'd like!
*/
namespace Knp\DoctrineBehaviors\Model\Sluggable;
/**
* Transliteration utility
*/
class Transliterator extends \Behat\Transliterator\Transliterator
{
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\Model\SoftDeletable;
/**
* SoftDeletable trait.
*
* Should be used inside entity, that needs to be self-deleted.
*/
trait SoftDeletable
{
use SoftDeletableProperties,
SoftDeletableMethods
;
}

View File

@@ -0,0 +1,107 @@
<?php
/*
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\Model\SoftDeletable;
/**
* SoftDeletable trait.
*
* Should be used inside entity, that needs to be self-deleted.
*/
trait SoftDeletableMethods
{
/**
* Marks entity as deleted.
*/
public function delete()
{
$this->deletedAt = $this->currentDateTime();
}
/**
* Restore entity by undeleting it
*/
public function restore()
{
$this->deletedAt = null;
}
/**
* Checks whether the entity has been deleted.
*
* @return Boolean
*/
public function isDeleted()
{
if (null !== $this->deletedAt) {
return $this->deletedAt <= $this->currentDateTime();
}
return false;
}
/**
* Checks whether the entity will be deleted.
*
* @return Boolean
*/
public function willBeDeleted(\DateTime $at = null)
{
if ($this->deletedAt === null) {
return false;
}
if ($at === null) {
return true;
}
return $this->deletedAt <= $at;
}
/**
* Returns date on which entity was been deleted.
*
* @return DateTime|null
*/
public function getDeletedAt()
{
return $this->deletedAt;
}
/**
* Set the delete date to given date.
*
* @param DateTime $date
* @param Object
*
* @return $this
*/
public function setDeletedAt(\DateTime $date)
{
$this->deletedAt = $date;
return $this;
}
/**
* Get a instance of \DateTime with the current data time including milliseconds.
*
* @return \DateTime
*/
private function currentDateTime()
{
$dateTime = \DateTime::createFromFormat('U.u', sprintf('%.6F', microtime(true)));
$dateTime->setTimezone(new \DateTimeZone(date_default_timezone_get()));
return $dateTime;
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\Model\SoftDeletable;
/**
* SoftDeletable trait.
*
* Should be used inside entity, that needs to be self-deleted.
*/
trait SoftDeletableProperties
{
protected $deletedAt;
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Knp\DoctrineBehaviors\Model\Sortable;
trait Sortable
{
/**
* @var int
*/
protected $sort = 1;
/**
* @var bool
*/
private $reordered = false;
/**
* Get sort.
*
* @return int
*/
public function getSort()
{
return $this->sort;
}
/**
* Set sort.
*
* @param int $sort Sort the value to set
*
* @return $this
*/
public function setSort($sort)
{
$this->reordered = $this->sort !== $sort;
$this->sort = $sort;
return $this;
}
/**
* @return bool
*/
public function isReordered()
{
return $this->reordered;
}
/**
* @return $this
*/
public function setReordered()
{
$this->reordered = true;
return $this;
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\Model\Timestampable;
/**
* Timestampable trait.
*
* Should be used inside entity, that needs to be timestamped.
*/
trait Timestampable
{
use TimestampableProperties,
TimestampableMethods
;
}

View File

@@ -0,0 +1,78 @@
<?php
/*
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\Model\Timestampable;
/**
* Timestampable trait.
*
* Should be used inside entity, that needs to be timestamped.
*/
trait TimestampableMethods
{
/**
* Returns createdAt value.
*
* @return \DateTime
*/
public function getCreatedAt()
{
return $this->createdAt;
}
/**
* Returns updatedAt value.
*
* @return \DateTime
*/
public function getUpdatedAt()
{
return $this->updatedAt;
}
/**
* @param \DateTime $createdAt
* @return $this
*/
public function setCreatedAt(\DateTime $createdAt)
{
$this->createdAt = $createdAt;
return $this;
}
/**
* @param \DateTime $updatedAt
* @return $this
*/
public function setUpdatedAt(\DateTime $updatedAt)
{
$this->updatedAt = $updatedAt;
return $this;
}
/**
* Updates createdAt and updatedAt timestamps.
*/
public function updateTimestamps()
{
// Create a datetime with microseconds
$dateTime = \DateTime::createFromFormat('U.u', sprintf('%.6F', microtime(true)));
$dateTime->setTimezone(new \DateTimeZone(date_default_timezone_get()));
if (null === $this->createdAt) {
$this->createdAt = $dateTime;
}
$this->updatedAt = $dateTime;
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\Model\Timestampable;
/**
* Timestampable trait.
*
* Should be used inside entity, that needs to be timestamped.
*/
trait TimestampableProperties
{
protected $createdAt;
protected $updatedAt;
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\Model\Translatable;
/**
* Translatable trait.
*
* Should be used inside entity, that needs to be translated.
*/
trait Translatable
{
use TranslatableProperties,
TranslatableMethods
;
}

View File

@@ -0,0 +1,227 @@
<?php
/*
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\Model\Translatable;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Translatable trait.
*
* Should be used inside entity, that needs to be translated.
*/
trait TranslatableMethods
{
/**
* Returns collection of translations.
*
* @return ArrayCollection
*/
public function getTranslations()
{
return $this->translations = $this->translations ?: new ArrayCollection();
}
/**
* Returns collection of new translations.
*
* @return ArrayCollection
*/
public function getNewTranslations()
{
return $this->newTranslations = $this->newTranslations ?: new ArrayCollection();
}
/**
* Adds new translation.
*
* @param Translation $translation The translation
*
* @return $this
*/
public function addTranslation($translation)
{
$this->getTranslations()->set((string)$translation->getLocale(), $translation);
$translation->setTranslatable($this);
return $this;
}
/**
* Removes specific translation.
*
* @param Translation $translation The translation
*/
public function removeTranslation($translation)
{
$this->getTranslations()->removeElement($translation);
}
/**
* Returns translation for specific locale (creates new one if doesn't exists).
* If requested translation doesn't exist, it will first try to fallback default locale
* If any translation doesn't exist, it will be added to newTranslations collection.
* In order to persist new translations, call mergeNewTranslations method, before flush
*
* @param string $locale The locale (en, ru, fr) | null If null, will try with current locale
* @param bool $fallbackToDefault Whether fallback to default locale
*
* @return Translation
*/
public function translate($locale = null, $fallbackToDefault = true)
{
return $this->doTranslate($locale, $fallbackToDefault);
}
/**
* Returns translation for specific locale (creates new one if doesn't exists).
* If requested translation doesn't exist, it will first try to fallback default locale
* If any translation doesn't exist, it will be added to newTranslations collection.
* In order to persist new translations, call mergeNewTranslations method, before flush
*
* @param string $locale The locale (en, ru, fr) | null If null, will try with current locale
* @param bool $fallbackToDefault Whether fallback to default locale
*
* @return Translation
*/
protected function doTranslate($locale = null, $fallbackToDefault = true)
{
if (null === $locale) {
$locale = $this->getCurrentLocale();
}
$translation = $this->findTranslationByLocale($locale);
if ($translation and !$translation->isEmpty()) {
return $translation;
}
if ($fallbackToDefault) {
if (($fallbackLocale = $this->computeFallbackLocale($locale))
&& ($translation = $this->findTranslationByLocale($fallbackLocale))) {
return $translation;
}
if ($defaultTranslation = $this->findTranslationByLocale($this->getDefaultLocale(), false)) {
return $defaultTranslation;
}
}
$class = static::getTranslationEntityClass();
$translation = new $class();
$translation->setLocale($locale);
$this->getNewTranslations()->set((string)$translation->getLocale(), $translation);
$translation->setTranslatable($this);
return $translation;
}
/**
* Merges newly created translations into persisted translations.
*/
public function mergeNewTranslations()
{
foreach ($this->getNewTranslations() as $newTranslation) {
if (!$this->getTranslations()->contains($newTranslation) && !$newTranslation->isEmpty()) {
$this->addTranslation($newTranslation);
$this->getNewTranslations()->removeElement($newTranslation);
}
}
}
/**
* @param mixed $locale the current locale
*/
public function setCurrentLocale($locale)
{
$this->currentLocale = $locale;
}
/**
* @return Returns the current locale
*/
public function getCurrentLocale()
{
return $this->currentLocale ?: $this->getDefaultLocale();
}
/**
* @param mixed $locale the default locale
*/
public function setDefaultLocale($locale)
{
$this->defaultLocale = $locale;
}
/**
* @return Returns the default locale
*/
public function getDefaultLocale()
{
return $this->defaultLocale;
}
/**
* An extra feature allows you to proxy translated fields of a translatable entity.
*
* @param string $method
* @param array $arguments
*
* @return mixed The translated value of the field for current locale
*/
protected function proxyCurrentLocaleTranslation($method, array $arguments = [])
{
return call_user_func_array(
[$this->translate($this->getCurrentLocale()), $method],
$arguments
);
}
/**
* Returns translation entity class name.
*
* @return string
*/
public static function getTranslationEntityClass()
{
return __CLASS__.'Translation';
}
/**
* Finds specific translation in collection by its locale.
*
* @param string $locale The locale (en, ru, fr)
* @param bool $withNewTranslations searched in new translations too
*
* @return Translation|null
*/
protected function findTranslationByLocale($locale, $withNewTranslations = true)
{
$translation = $this->getTranslations()->get($locale);
if ($translation) {
return $translation;
}
if ($withNewTranslations) {
return $this->getNewTranslations()->get($locale);
}
}
protected function computeFallbackLocale($locale)
{
if (strrchr($locale, '_') !== false) {
return substr($locale, 0, -strlen(strrchr($locale, '_')));
}
return false;
}
}

View File

@@ -0,0 +1,40 @@
<?php
/*
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\Model\Translatable;
/**
* Translatable trait.
*
* Should be used inside entity, that needs to be translated.
*/
trait TranslatableProperties
{
/**
* Will be mapped to translatable entity
* by TranslatableSubscriber
*/
protected $translations;
/**
* Will be merged with persisted translations on mergeNewTranslations call
*
* @see mergeNewTranslations
*/
protected $newTranslations;
/**
* currentLocale is a non persisted field configured during postLoad event
*/
protected $currentLocale;
protected $defaultLocale = 'en';
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\Model\Translatable;
/**
* Translation trait.
*
* Should be used inside translation entity.
*/
trait Translation
{
use TranslationProperties,
TranslationMethods
;
}

View File

@@ -0,0 +1,108 @@
<?php
/*
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\Model\Translatable;
/**
* Translation trait.
*
* Should be used inside translation entity.
*/
trait TranslationMethods
{
/**
* Returns the translatable entity class name.
*
* @return string
*/
public static function getTranslatableEntityClass()
{
// By default, the translatable class has the same name but without the "Translation" suffix
return substr(__CLASS__, 0, -11);
}
/**
* Returns object id.
*
* @return mixed
*/
public function getId() {
return $this->id;
}
/**
* Sets entity, that this translation should be mapped to.
*
* @param Translatable $translatable The translatable
*
* @return $this
*/
public function setTranslatable($translatable)
{
$this->translatable = $translatable;
return $this;
}
/**
* Returns entity, that this translation is mapped to.
*
* @return Translatable
*/
public function getTranslatable()
{
return $this->translatable;
}
/**
* Sets locale name for this translation.
*
* @param string $locale The locale
*
* @return $this
*/
public function setLocale($locale)
{
$this->locale = $locale;
return $this;
}
/**
* Returns this translation locale.
*
* @return string
*/
public function getLocale()
{
return $this->locale;
}
/**
* Tells if translation is empty
*
* @return bool true if translation is not filled
*/
public function isEmpty()
{
foreach (get_object_vars($this) as $var => $value) {
if (in_array($var, ['id', 'translatable', 'locale'])) {
continue;
}
if (!empty($value)) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,36 @@
<?php
/*
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\Model\Translatable;
/**
* Translation trait.
*
* Should be used inside translation entity.
*/
trait TranslationProperties
{
/**
* @var int
*/
protected $id;
/**
* @var string
*/
protected $locale;
/**
* Will be mapped to translatable entity
* by TranslatableSubscriber
*/
protected $translatable;
}

View File

@@ -0,0 +1,333 @@
<?php
/**
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\Model\Tree;
use Knp\DoctrineBehaviors\Model\Tree\NodeInterface;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
/*
* @author Florian Klein <florian.klein@free.fr>
*/
trait Node
{
/**
* @var ArrayCollection $childNodes the children in the tree
*/
private $childNodes;
/**
* @var NodeInterface $parentNode the parent in the tree
*/
private $parentNode;
protected $materializedPath = '';
public function getNodeId()
{
return $this->getId();
}
/**
* Returns path separator for entity's materialized path.
*
* @return string "/" by default
*/
public static function getMaterializedPathSeparator()
{
return '/';
}
/**
* {@inheritdoc}
**/
public function getRealMaterializedPath()
{
return $this->getMaterializedPath() . self::getMaterializedPathSeparator() . $this->getNodeId();
}
public function getMaterializedPath()
{
return $this->materializedPath;
}
/**
* {@inheritdoc}
**/
public function setMaterializedPath($path)
{
$this->materializedPath = $path;
$this->setParentMaterializedPath($this->getParentMaterializedPath());
return $this;
}
/**
* {@inheritdoc}
**/
public function getParentMaterializedPath()
{
$path = $this->getExplodedPath();
array_pop($path);
$parentPath = static::getMaterializedPathSeparator().implode(static::getMaterializedPathSeparator(), $path);
return $parentPath;
}
/**
* {@inheritdoc}
**/
public function setParentMaterializedPath($path)
{
$this->parentNodePath = $path;
}
/**
* {@inheritdoc}
**/
public function getRootMaterializedPath()
{
$explodedPath = $this->getExplodedPath();
return static::getMaterializedPathSeparator() . array_shift($explodedPath);
}
/**
* {@inheritdoc}
**/
public function getNodeLevel()
{
return count($this->getExplodedPath());
}
public function isRootNode()
{
return self::getMaterializedPathSeparator() === $this->getParentMaterializedPath();
}
public function isLeafNode()
{
return 0 === $this->getChildNodes()->count();
}
/**
* {@inheritdoc}
**/
public function getChildNodes()
{
return $this->childNodes = $this->childNodes ?: new ArrayCollection;
}
/**
* {@inheritdoc}
**/
public function addChildNode(NodeInterface $node)
{
$this->getChildNodes()->add($node);
}
/**
* {@inheritdoc}
**/
public function isIndirectChildNodeOf(NodeInterface $node)
{
return $this->getRealMaterializedPath() !== $node->getRealMaterializedPath()
&& 0 === strpos($this->getRealMaterializedPath(), $node->getRealMaterializedPath());
}
/**
* {@inheritdoc}
**/
public function isChildNodeOf(NodeInterface $node)
{
return $this->getParentMaterializedPath() === $node->getRealMaterializedPath();
}
/**
* {@inheritdoc}
**/
public function setChildNodeOf(NodeInterface $node = null)
{
$id = $this->getNodeId();
if (empty($id)) {
throw new \LogicException('You must provide an id for this node if you want it to be part of a tree.');
}
$path = null !== $node
? rtrim($node->getRealMaterializedPath(), static::getMaterializedPathSeparator())
: static::getMaterializedPathSeparator()
;
$this->setMaterializedPath($path);
if (null !== $this->parentNode) {
$this->parentNode->getChildNodes()->removeElement($this);
}
$this->parentNode = $node;
if (null !== $node) {
$this->parentNode->addChildNode($this);
}
foreach ($this->getChildNodes() as $child) {
$child->setChildNodeOf($this);
}
return $this;
}
/**
* {@inheritdoc}
**/
public function getParentNode()
{
return $this->parentNode;
}
/**
* {@inheritdoc}
**/
public function setParentNode(NodeInterface $node)
{
$this->parentNode = $node;
$this->setChildNodeOf($this->parentNode);
return $this;
}
/**
* {@inheritdoc}
**/
public function getRootNode()
{
$parent = $this;
while (null !== $parent->getParentNode()) {
$parent = $parent->getParentNode();
}
return $parent;
}
/**
* {@inheritdoc}
**/
public function buildTree(array $results)
{
$this->getChildNodes()->clear();
foreach ($results as $i => $node) {
if ($node->getMaterializedPath() === $this->getRealMaterializedPath()) {
$node->setParentNode($this);
$node->buildTree($results);
}
}
}
/**
* @param \Closure $prepare a function to prepare the node before putting into the result
*
* @return string the json representation of the hierarchical result
**/
public function toJson(\Closure $prepare = null)
{
$tree = $this->toArray($prepare);
return json_encode($tree);
}
/**
* @param \Closure $prepare a function to prepare the node before putting into the result
* @param array $tree a reference to an array, used internally for recursion
*
* @return array the hierarchical result
**/
public function toArray(\Closure $prepare = null, array &$tree = null)
{
if (null === $prepare) {
$prepare = function(NodeInterface $node) {
return (string) $node;
};
}
if (null === $tree) {
$tree = array($this->getNodeId() => array('node' => $prepare($this), 'children' => array()));
}
foreach ($this->getChildNodes() as $node) {
$tree[$this->getNodeId()]['children'][$node->getNodeId()] = array('node' => $prepare($node), 'children' => array());
$node->toArray($prepare, $tree[$this->getNodeId()]['children']);
}
return $tree;
}
/**
* @param \Closure $prepare a function to prepare the node before putting into the result
* @param array $tree a reference to an array, used internally for recursion
*
* @return array the flatten result
**/
public function toFlatArray(\Closure $prepare = null, array &$tree = null)
{
if (null === $prepare) {
$prepare = function(NodeInterface $node) {
$pre = $node->getNodeLevel() > 1 ? implode('', array_fill(0, $node->getNodeLevel(), '--')) : '';
return $pre.(string) $node;
};
}
if (null === $tree) {
$tree = array($this->getNodeId() => $prepare($this));
}
foreach ($this->getChildNodes() as $node) {
$tree[$node->getNodeId()] = $prepare($node);
$node->toFlatArray($prepare, $tree);
}
return $tree;
}
public function offsetSet($offset, $node)
{
$node->setChildNodeOf($this);
return $this;
}
public function offsetExists($offset)
{
return isset($this->getChildNodes()[$offset]);
}
public function offsetUnset($offset)
{
unset($this->getChildNodes()[$offset]);
}
public function offsetGet($offset)
{
return $this->getChildNodes()[$offset];
}
/**
* {@inheritdoc}
**/
protected function getExplodedPath()
{
$path = explode(static::getMaterializedPathSeparator(), $this->getRealMaterializedPath());
return array_filter($path, function($item) {
return '' !== $item;
});
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Knp\DoctrineBehaviors\Model\Tree;
use Doctrine\Common\Collections\Collection;
/**
* Tree\Node defines a set of needed methods
* to work with materialized path tree nodes
*
* @author Florian Klein <florian.klein@free.fr>
*/
interface NodeInterface
{
/**
* @return string the field that will represent the node in the path
**/
public function getNodeId();
/**
* @return string the materialized path,
* eg the representation of path from all ancestors
**/
public function getMaterializedPath();
/**
* @return string the real materialized path,
* eg the representation of path from all ancestors + current node
**/
public function getRealMaterializedPath();
/**
* @return string the materialized path from the parent, eg: the representation of path from all parent ancestors
**/
public function getParentMaterializedPath();
/**
* Set parent path.
*
* @param string $path the value to set.
*/
public function setParentMaterializedPath($path);
/**
* @return NodeInterface the parent node
**/
public function getParentNode();
/**
* @param string $path the materialized path, eg: the the materialized path to its parent
*
* @return NodeInterface $this Fluent interface
**/
public function setMaterializedPath($path);
/**
* Used to build the hierarchical tree.
* This method will do:
* - modify the parent of this node
* - Add the this node to the children of the new parent
* - Remove the this node from the children of the old parent
* - Modify the materialized path of this node and all its children, recursively
*
* @param NodeInterface | null $node The node to use as a parent
*
* @return NodeInterface $this Fluent interface
**/
public function setChildNodeOf(NodeInterface $node = null);
/**
* @param NodeInterface $node the node to append to the children collection
*
* @return NodeInterface $this Fluent interface
**/
public function addChildNode(NodeInterface $node);
/**
* @return Collection the children collection
**/
public function getChildNodes();
/**
* @return bool if the node is a leaf (i.e has no children)
**/
public function isLeafNode();
/**
* @return bool if the node is a root (i.e has no parent)
**/
public function isRootNode();
/**
* @return NodeInterface
**/
public function getRootNode();
/**
* Tells if this node is a child of another node
* @param NodeInterface $node the node to compare with
*
* @return boolean true if this node is a direct child of $node
**/
public function isChildNodeOf(NodeInterface $node);
/**
*
* @return integer the level of this node, eg: the depth compared to root node
**/
public function getNodeLevel();
/**
* Builds a hierarchical tree from a flat collection of NodeInterface elements
*
* @return void
**/
public function buildTree(array $nodes);
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\ORM;
use Doctrine\Common\EventSubscriber;
use Knp\DoctrineBehaviors\Reflection\ClassAnalyzer;
abstract class AbstractSubscriber implements EventSubscriber
{
private $classAnalyser;
protected $isRecursive;
public function __construct(ClassAnalyzer $classAnalyser, $isRecursive)
{
$this->classAnalyser = $classAnalyser;
$this->isRecursive = (bool) $isRecursive;
}
protected function getClassAnalyzer()
{
return $this->classAnalyser;
}
abstract public function getSubscribedEvents();
}

View File

@@ -0,0 +1,317 @@
<?php
/**
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\ORM\Blameable;
use Knp\DoctrineBehaviors\Reflection\ClassAnalyzer;
use Knp\DoctrineBehaviors\ORM\AbstractSubscriber;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\Common\EventSubscriber,
Doctrine\ORM\Event\OnFlushEventArgs,
Doctrine\ORM\Events;
/**
* BlameableSubscriber handle Blameable entites
* Adds class metadata depending of user type (entity or string)
* Listens to prePersist and PreUpdate lifecycle events
*/
class BlameableSubscriber extends AbstractSubscriber
{
/**
* @var callable
*/
private $userCallable;
/**
* @var mixed
*/
private $user;
/**
* userEntity name
*/
private $userEntity;
private $blameableTrait;
/**
* @param callable
* @param string $userEntity
*/
public function __construct(ClassAnalyzer $classAnalyzer, $isRecursive, $blameableTrait, callable $userCallable = null, $userEntity = null)
{
parent::__construct($classAnalyzer, $isRecursive);
$this->blameableTrait = $blameableTrait;
$this->userCallable = $userCallable;
$this->userEntity = $userEntity;
}
/**
* Adds metadata about how to store user, either a string or an ManyToOne association on user entity
*
* @param LoadClassMetadataEventArgs $eventArgs
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
$classMetadata = $eventArgs->getClassMetadata();
if (null === $classMetadata->reflClass) {
return;
}
if ($this->isBlameable($classMetadata)) {
$this->mapEntity($classMetadata);
}
}
private function mapEntity(ClassMetadata $classMetadata)
{
if ($this->userEntity) {
$this->mapManyToOneUser($classMetadata);
} else {
$this->mapStringUser($classMetadata);
}
}
private function mapStringUser(ClassMetadata $classMetadata)
{
if (!$classMetadata->hasField('createdBy')) {
$classMetadata->mapField([
'fieldName' => 'createdBy',
'type' => 'string',
'nullable' => true,
]);
}
if (!$classMetadata->hasField('updatedBy')) {
$classMetadata->mapField([
'fieldName' => 'updatedBy',
'type' => 'string',
'nullable' => true,
]);
}
if (!$classMetadata->hasField('deletedBy')) {
$classMetadata->mapField([
'fieldName' => 'deletedBy',
'type' => 'string',
'nullable' => true,
]);
}
}
private function mapManyToOneUser(classMetadata $classMetadata)
{
if (!$classMetadata->hasAssociation('createdBy')) {
$classMetadata->mapManyToOne([
'fieldName' => 'createdBy',
'targetEntity' => $this->userEntity,
'joinColumns' => array(array(
'onDelete' => 'SET NULL'
))
]);
}
if (!$classMetadata->hasAssociation('updatedBy')) {
$classMetadata->mapManyToOne([
'fieldName' => 'updatedBy',
'targetEntity' => $this->userEntity,
'joinColumns' => array(array(
'onDelete' => 'SET NULL'
))
]);
}
if (!$classMetadata->hasAssociation('deletedBy')) {
$classMetadata->mapManyToOne([
'fieldName' => 'deletedBy',
'targetEntity' => $this->userEntity,
'joinColumns' => array(array(
'onDelete' => 'SET NULL'
))
]);
}
}
/**
* Stores the current user into createdBy and updatedBy properties
*
* @param LifecycleEventArgs $eventArgs
*/
public function prePersist(LifecycleEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
$entity = $eventArgs->getEntity();
$classMetadata = $em->getClassMetadata(get_class($entity));
if ($this->isBlameable($classMetadata)) {
if (!$entity->getCreatedBy()) {
$user = $this->getUser();
if ($this->isValidUser($user)) {
$entity->setCreatedBy($user);
$uow->propertyChanged($entity, 'createdBy', null, $user);
$uow->scheduleExtraUpdate($entity, [
'createdBy' => [null, $user],
]);
}
}
if (!$entity->getUpdatedBy()) {
$user = $this->getUser();
if ($this->isValidUser($user)) {
$entity->setUpdatedBy($user);
$uow->propertyChanged($entity, 'updatedBy', null, $user);
$uow->scheduleExtraUpdate($entity, [
'updatedBy' => [null, $user],
]);
}
}
}
}
/**
*
*/
private function isValidUser($user)
{
if ($this->userEntity) {
return $user instanceof $this->userEntity;
}
if (is_object($user)) {
return method_exists($user, '__toString');
}
return is_string($user);
}
/**
* Stores the current user into updatedBy property
*
* @param LifecycleEventArgs $eventArgs
*/
public function preUpdate(LifecycleEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
$entity = $eventArgs->getEntity();
$classMetadata = $em->getClassMetadata(get_class($entity));
if ($this->isBlameable($classMetadata)) {
if (!$entity->isBlameable()) {
return;
}
$user = $this->getUser();
if ($this->isValidUser($user)) {
$oldValue = $entity->getUpdatedBy();
$entity->setUpdatedBy($user);
$uow->propertyChanged($entity, 'updatedBy', $oldValue, $user);
$uow->scheduleExtraUpdate($entity, [
'updatedBy' => [$oldValue, $user],
]);
}
}
}
/**
* Stores the current user into deletedBy property
*
* @param LifecycleEventArgs $eventArgs
*/
public function preRemove(LifecycleEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
$entity = $eventArgs->getEntity();
$classMetadata = $em->getClassMetadata(get_class($entity));
if ($this->isBlameable($classMetadata)) {
if (!$entity->isBlameable()) {
return;
}
$user = $this->getUser();
if ($this->isValidUser($user)) {
$oldValue = $entity->getDeletedBy();
$entity->setDeletedBy($user);
$uow->propertyChanged($entity, 'deletedBy', $oldValue, $user);
$uow->scheduleExtraUpdate($entity, [
'deletedBy' => [$oldValue, $user],
]);
}
}
}
/**
* set a custome representation of current user
*
* @param mixed $user
*/
public function setUser($user)
{
$this->user = $user;
}
/**
* get current user, either if $this->user is present or from userCallable
*
* @return mixed The user reprensentation
*/
public function getUser()
{
if (null !== $this->user) {
return $this->user;
}
if (null === $this->userCallable) {
return;
}
$callable = $this->userCallable;
return $callable();
}
public function getSubscribedEvents()
{
$events = [
Events::prePersist,
Events::preUpdate,
Events::preRemove,
Events::loadClassMetadata,
];
return $events;
}
public function setUserCallable(callable $callable)
{
$this->userCallable = $callable;
}
/**
* Checks if entity is blameable
*
* @param ClassMetadata $classMetadata The metadata
*
* @return Boolean
*/
private function isBlameable(ClassMetadata $classMetadata)
{
return $this->getClassAnalyzer()->hasTrait($classMetadata->reflClass, $this->blameableTrait, $this->isRecursive);
}
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\ORM\Blameable;
use Symfony\Component\DependencyInjection\Container;
/**
* UserCallable can be invoked to return a blameable user
*/
class UserCallable
{
/**
* @var Container
*/
private $container;
/**
* @param callable
* @param string $userEntity
*/
public function __construct(Container $container)
{
$this->container = $container;
}
public function __invoke()
{
$token = $this->container->get('security.token_storage')->getToken();
if (null !== $token) {
return $token->getUser();
}
}
}

View File

@@ -0,0 +1,129 @@
<?php
/*
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\ORM\Filterable;
use Doctrine\ORM\QueryBuilder;
/**
* Filterable trait.
*
* Should be used inside entity repository, that needs to be filterable
*/
trait FilterableRepository
{
/**
* Retrieve field which will be sorted using LIKE
*
* Example format: ['e:name', 'e:description']
*
* @return array
*/
abstract public function getLikeFilterColumns();
/**
* Retrieve field which will be sorted using LOWER() LIKE
*
* Example format: ['e:name', 'e:description']
*
* @return array
*/
abstract public function getILikeFilterColumns();
/**
* Retrieve field which will be sorted using EQUAL
*
* Example format: ['e:name', 'e:description']
*
* @return array
*/
abstract public function getEqualFilterColumns();
/**
* Retrieve field which will be sorted using IN()
*
* Example format: ['e:group_id']
*
* @return array
*/
abstract public function getInFilterColumns();
/**
* Filter values
*
* @param array $filters - array like ['e:name' => 'nameValue'] where "e" is entity alias query, so we can filter using joins.
* @param \Doctrine\ORM\QueryBuilder
* @return \Doctrine\ORM\QueryBuilder
*/
public function filterBy(array $filters, QueryBuilder $qb = null)
{
$filters = array_filter($filters, function ($filter) {
return !empty($filter);
});
if (null === $qb) {
$qb = $this->createFilterQueryBuilder();
}
foreach ($filters as $col => $value) {
foreach ($this->getColumnParameters($col) as $colName => $colParam) {
$compare = $this->getWhereOperator($col).'Where';
if (in_array($col, $this->getLikeFilterColumns())) {
$qb
->$compare(sprintf('%s LIKE :%s', $colName, $colParam))
->setParameter($colParam, '%'.$value.'%')
;
}
if (in_array($col, $this->getILikeFilterColumns())) {
$qb
->$compare(sprintf('LOWER(%s) LIKE :%s', $colName, $colParam))
->setParameter($colParam, '%'.strtolower($value).'%')
;
}
if (in_array($col, $this->getEqualFilterColumns())) {
$qb
->$compare(sprintf('%s = :%s', $colName, $colParam))
->setParameter($colParam, $value)
;
}
if (in_array($col, $this->getInFilterColumns())) {
$qb
->$compare($qb->expr()->in(sprintf('%s', $colName), (array) $value))
;
}
}
}
return $qb;
}
protected function getColumnParameters($col)
{
$colName = str_replace(':', '.', $col);
$colParam = str_replace(':', '_', $col);
return [$colName => $colParam];
}
protected function getWhereOperator($col)
{
return 'and';
}
protected function createFilterQueryBuilder()
{
return $this->createQueryBuilder('e');
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Knp\DoctrineBehaviors\ORM\Geocodable;
use Knp\DoctrineBehaviors\ORM\Geocodable\Type\Point;
trait GeocodableRepository
{
public function findByDistanceQB(Point $point, $distanceMax)
{
return $this->createQueryBuilder('e')
->andWhere('DISTANCE(e.location, :latitude, :longitude) <= :distanceMax')
->setParameter('latitude', $point->getLatitude())
->setParameter('longitude', $point->getLongitude())
->setParameter('distanceMax', $distanceMax)
;
}
public function findByDistance(Point $point, $distanceMax)
{
return $this->findByDistanceQB($point, $distanceMax)
->getQuery()
->execute()
;
}
}

View File

@@ -0,0 +1,197 @@
<?php
/**
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\ORM\Geocodable;
use Knp\DoctrineBehaviors\Reflection\ClassAnalyzer;
use Knp\DoctrineBehaviors\ORM\AbstractSubscriber;
use Knp\DoctrineBehaviors\ORM\Geocodable\Type\Point;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
use Doctrine\DBAL\Platforms\MySqlPlatform;
use Doctrine\DBAL\Types\Type;
use Doctrine\Common\EventSubscriber,
Doctrine\ORM\Event\OnFlushEventArgs,
Doctrine\ORM\Events;
/**
* GeocodableSubscriber handle Geocodable entites
* Adds doctrine point type
*/
class GeocodableSubscriber extends AbstractSubscriber
{
/**
* @var callable
*/
private $geolocationCallable;
private $geocodableTrait;
/**
* @param \Knp\DoctrineBehaviors\Reflection\ClassAnalyzer $classAnalyzer
* @param $isRecursive
* @param $geocodableTrait
* @param callable $geolocationCallable
*/
public function __construct(
ClassAnalyzer $classAnalyzer,
$isRecursive,
$geocodableTrait,
callable $geolocationCallable = null
) {
parent::__construct($classAnalyzer, $isRecursive);
$this->geocodableTrait = $geocodableTrait;
$this->geolocationCallable = $geolocationCallable;
}
/**
* Adds doctrine point type
*
* @param LoadClassMetadataEventArgs $eventArgs
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
$classMetadata = $eventArgs->getClassMetadata();
if (null === $classMetadata->reflClass) {
return;
}
if ($this->isGeocodable($classMetadata)) {
if (!Type::hasType('point')) {
Type::addType('point', 'Knp\DoctrineBehaviors\DBAL\Types\PointType');
}
$em = $eventArgs->getEntityManager();
$con = $em->getConnection();
// skip non-postgres platforms
if (!$con->getDatabasePlatform() instanceof PostgreSqlPlatform &&
!$con->getDatabasePlatform() instanceof MySqlPlatform
) {
return;
}
// skip platforms with registerd stuff
if (!$con->getDatabasePlatform()->hasDoctrineTypeMappingFor('point')) {
$con->getDatabasePlatform()->registerDoctrineTypeMapping('point', 'point');
if ($con->getDatabasePlatform() instanceof PostgreSqlPlatform) {
$em->getConfiguration()->addCustomNumericFunction(
'DISTANCE',
'Knp\DoctrineBehaviors\ORM\Geocodable\Query\AST\Functions\DistanceFunction'
);
}
}
$classMetadata->mapField(
[
'fieldName' => 'location',
'type' => 'point',
'nullable' => true
]
);
}
}
/**
* @param LifecycleEventArgs $eventArgs
*/
private function updateLocation(LifecycleEventArgs $eventArgs, $override = false)
{
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
$entity = $eventArgs->getEntity();
$classMetadata = $em->getClassMetadata(get_class($entity));
if ($this->isGeocodable($classMetadata)) {
$oldValue = $entity->getLocation();
if (!$oldValue instanceof Point || $override) {
$newLocation = $this->getLocation($entity);
if ($newLocation !== false) {
$entity->setLocation($newLocation);
}
$uow->propertyChanged($entity, 'location', $oldValue, $entity->getLocation());
$uow->scheduleExtraUpdate(
$entity,
[
'location' => [$oldValue, $entity->getLocation()],
]
);
}
}
}
public function prePersist(LifecycleEventArgs $eventArgs)
{
$this->updateLocation($eventArgs, false);
}
public function preUpdate(LifecycleEventArgs $eventArgs)
{
$this->updateLocation($eventArgs, true);
}
/**
* @return Point the location
*/
public function getLocation($entity)
{
if (null === $this->geolocationCallable) {
return false;
}
$callable = $this->geolocationCallable;
return $callable($entity);
}
/**
* Checks if entity is geocodable
*
* @param ClassMetadata $classMetadata The metadata
*
* @return boolean
*/
private function isGeocodable(ClassMetadata $classMetadata)
{
return $this->getClassAnalyzer()->hasTrait(
$classMetadata->reflClass,
$this->geocodableTrait,
$this->isRecursive
);
}
public function getSubscribedEvents()
{
return [
Events::prePersist,
Events::preUpdate,
Events::loadClassMetadata,
];
}
public function setGeolocationCallable(callable $callable)
{
$this->geolocationCallable = $callable;
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Knp\DoctrineBehaviors\ORM\Geocodable\Query\AST\Functions;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\AST\PathExpression;
/**
* DQL function for calculating distances in meters between two points
*
* DISTANCE(entity.point, :latitude, :longitude)
*/
class DistanceFunction extends FunctionNode
{
private $entityLocation;
private $latitude;
private $longitude;
/**
* Returns SQL representation of this function.
*
* @param SqlWalker $sqlWalker
*
* @return string
*/
public function getSql(SqlWalker $sqlWalker)
{
$entityLocation = $this->entityLocation->dispatch($sqlWalker);
return sprintf('earth_distance(ll_to_earth(%s[0], %s[1]),ll_to_earth(%s, %s))',
$entityLocation,
$entityLocation,
$this->latitude->dispatch($sqlWalker),
$this->longitude->dispatch($sqlWalker)
);
}
/**
* Parses DQL function.
*
* @param Parser $parser
*/
public function parse(Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->entityLocation = $parser->ArithmeticPrimary();
$parser->match(Lexer::T_COMMA);
$this->latitude = $parser->ArithmeticPrimary();
$parser->match(Lexer::T_COMMA);
$this->longitude = $parser->ArithmeticPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Knp\DoctrineBehaviors\ORM\Geocodable\Type;
/**
* Point object for spatial mapping
*/
class Point
{
private $latitude;
private $longitude;
/**
* Initializes point.
*
* @param float|integer $latitude
* @param float|integer $longitude
*/
public function __construct($latitude, $longitude)
{
$this->latitude = $latitude;
$this->longitude = $longitude;
}
/**
* Creates new Point from array.
*
* @param array $array either hash or array of lat, long
*
* @return Point
*/
public static function fromArray(array $array)
{
if (isset($array['latitude'])) {
return new self($array['latitude'], $array['longitude']);
} else {
return new self($array[0], $array[1]);
}
}
/**
* Creates new Point from string.
*
* @param string $string string in (%f,%f) format
*
* @return Point
*/
public static function fromString($string)
{
return self::fromArray(sscanf($string, '(%f,%f)'));
}
/**
* Returns Point latitude.
*
* @return float|integer
*/
public function getLatitude()
{
return $this->latitude;
}
/**
* Returns Point longitude.
*
* @return float|integer
*/
public function getLongitude()
{
return $this->longitude;
}
/**
* Returns string representation for Point in (%f,%f) format.
*
* @return string
*/
public function __toString()
{
return sprintf('(%F,%F)', $this->latitude, $this->longitude);
}
public function isEmpty()
{
return empty($this->latitude) && empty($this->longitude);
}
}

View File

@@ -0,0 +1,94 @@
<?php
/*
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\ORM\Joinable;
use Doctrine\ORM\QueryBuilder;
/**
* Joinable trait.
*
* Should be used inside entity repository, that needs to easily make joined queries
*/
trait JoinableRepository
{
public function getJoinAllQueryBuilder($alias = null, QueryBuilder $qb = null)
{
if (null === $alias) {
$alias = $this->getAlias($this->getClassName());
}
if (null === $qb) {
$qb = $this->createQueryBuilder($alias);
}
$className = $this->getClassName();
$this->addJoinsToQueryBuilder($alias, $qb, $className);
return $qb;
}
private function addJoinsToQueryBuilder($alias, QueryBuilder $qb, $className, $recursive = true)
{
foreach ($this->getEntityManager()->getClassMetadata($className)->getAssociationMappings() as $assoc) {
if (in_array($assoc['targetEntity'], $qb->getRootEntities()) || $className === $assoc['targetEntity']) {
continue;
}
$uniqueJoinAlias = $this->getUniqueAlias($assoc['targetEntity'], $qb);
$qb
->addSelect($uniqueJoinAlias)
->leftJoin(sprintf('%s.%s', $alias, $assoc['fieldName']), $uniqueJoinAlias)
;
if ($recursive) {
$this->addJoinsToQueryBuilder($uniqueJoinAlias, $qb, $assoc['targetEntity']);
}
}
}
private function getAlias($className)
{
$shortName = $this->getEntityManager()->getClassMetadata($className)->reflClass->getShortName();
$alias = strtolower(substr($shortName, 0, 1));
return $alias;
}
private function getUniqueAlias($className, QueryBuilder $qb)
{
$alias = $this->getAlias($className);
$i = 1;
$firstAlias = $alias;
while ($this->aliasExists($alias, $qb)) {
$alias = $firstAlias.$i;
$i++;
}
return $alias;
}
private function aliasExists($alias, QueryBuilder $qb)
{
$aliases = [];
foreach ($qb->getDqlPart('join') as $joins) {
foreach ($joins as $join) {
$aliases[] = $join->getAlias();
}
}
$aliases[] = $qb->getRootAlias();
return in_array($alias, $aliases);
}
}

View File

@@ -0,0 +1,125 @@
<?php
/**
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\ORM\Loggable;
use Knp\DoctrineBehaviors\Reflection\ClassAnalyzer;
use Knp\DoctrineBehaviors\ORM\AbstractSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\Common\EventSubscriber,
Doctrine\ORM\Event\OnFlushEventArgs,
Doctrine\ORM\Events;
/**
* LoggableSubscriber handle Loggable entites
* Listens to lifecycle events
*/
class LoggableSubscriber extends AbstractSubscriber
{
/**
* @var callable
*/
private $loggerCallable;
/**
* @param callable
*/
public function __construct(ClassAnalyzer $classAnalyzer, $isRecursive, callable $loggerCallable)
{
parent::__construct($classAnalyzer, $isRecursive);
$this->loggerCallable = $loggerCallable;
}
public function postPersist(LifecycleEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$entity = $eventArgs->getEntity();
$classMetadata = $em->getClassMetadata(get_class($entity));
if ($this->isEntitySupported($classMetadata->reflClass)) {
$message = $entity->getCreateLogMessage();
$loggerCallable = $this->loggerCallable;
$loggerCallable($message);
}
return $this->logChangeSet($eventArgs);
}
public function postUpdate(LifecycleEventArgs $eventArgs)
{
return $this->logChangeSet($eventArgs);
}
/**
* Logs entity changeset
*
* @param LifecycleEventArgs $eventArgs
*/
public function logChangeSet(LifecycleEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
$entity = $eventArgs->getEntity();
$classMetadata = $em->getClassMetadata(get_class($entity));
if ($this->isEntitySupported($classMetadata->reflClass)) {
$uow->computeChangeSet($classMetadata, $entity);
$changeSet = $uow->getEntityChangeSet($entity);
$message = $entity->getUpdateLogMessage($changeSet);
$loggerCallable = $this->loggerCallable;
$loggerCallable($message);
}
}
public function preRemove(LifecycleEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$entity = $eventArgs->getEntity();
$classMetadata = $em->getClassMetadata(get_class($entity));
if ($this->isEntitySupported($classMetadata->reflClass)) {
$message = $entity->getRemoveLogMessage();
$loggerCallable = $this->loggerCallable;
$loggerCallable($message);
}
}
public function setLoggerCallable(callable $callable)
{
$this->loggerCallable = $callable;
}
/**
* Checks if entity supports Loggable
*
* @param ReflectionClass $reflClass
* @return boolean
*/
protected function isEntitySupported(\ReflectionClass $reflClass)
{
return $this->getClassAnalyzer()->hasTrait($reflClass, 'Knp\DoctrineBehaviors\Model\Loggable\Loggable', $this->isRecursive);
}
public function getSubscribedEvents()
{
$events = [
Events::postPersist,
Events::postUpdate,
Events::preRemove,
];
return $events;
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\ORM\Loggable;
use Psr\Log\LoggerInterface;
/**
* LoggerCallable can be invoked to log messages using symfony2 logger
*/
class LoggerCallable
{
/**
* @var LoggerInterface
*/
private $logger;
/**
* @param LoggerInterface $logger
*/
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function __invoke($message)
{
$this->logger->debug($message);
}
}

View File

@@ -0,0 +1,96 @@
<?php
/**
* @author Lusitanian
* Freely released with no restrictions, re-license however you'd like!
*/
namespace Knp\DoctrineBehaviors\ORM\Sluggable;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Knp\DoctrineBehaviors\Reflection\ClassAnalyzer;
use Knp\DoctrineBehaviors\ORM\AbstractSubscriber;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs,
Doctrine\Common\EventSubscriber,
Doctrine\ORM\Events,
Doctrine\ORM\Mapping\ClassMetadata;
/**
* Sluggable subscriber.
*
* Adds mapping to sluggable entities.
*/
class SluggableSubscriber extends AbstractSubscriber
{
private $sluggableTrait;
public function __construct(ClassAnalyzer $classAnalyzer, $isRecursive, $sluggableTrait)
{
parent::__construct($classAnalyzer, $isRecursive);
$this->sluggableTrait = $sluggableTrait;
}
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
$classMetadata = $eventArgs->getClassMetadata();
if (null === $classMetadata->reflClass) {
return;
}
if ($this->isSluggable($classMetadata)) {
if (!$classMetadata->hasField('slug')) {
$classMetadata->mapField(array(
'fieldName' => 'slug',
'type' => 'string',
'nullable' => true
));
}
}
}
public function prePersist(LifecycleEventArgs $eventArgs)
{
$entity = $eventArgs->getEntity();
$em = $eventArgs->getEntityManager();
$classMetadata = $em->getClassMetadata(get_class($entity));
if ($this->isSluggable($classMetadata)) {
$entity->generateSlug();
}
}
public function preUpdate(LifecycleEventArgs $eventArgs)
{
$entity = $eventArgs->getEntity();
$em = $eventArgs->getEntityManager();
$classMetadata = $em->getClassMetadata(get_class($entity));
if ($this->isSluggable($classMetadata)) {
$entity->generateSlug();
}
}
public function getSubscribedEvents()
{
return [ Events::loadClassMetadata, Events::prePersist, Events::preUpdate ];
}
/**
* Checks if entity is sluggable
*
* @param ClassMetadata $classMetadata The metadata
*
* @return boolean
*/
private function isSluggable(ClassMetadata $classMetadata)
{
return $this->getClassAnalyzer()->hasTrait(
$classMetadata->reflClass,
$this->sluggableTrait,
$this->isRecursive
);
}
}

View File

@@ -0,0 +1,108 @@
<?php
/*
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\ORM\SoftDeletable;
use Knp\DoctrineBehaviors\Reflection\ClassAnalyzer;
use Knp\DoctrineBehaviors\ORM\AbstractSubscriber;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs,
Doctrine\Common\Persistence\Mapping\ClassMetadata,
Doctrine\Common\EventSubscriber,
Doctrine\ORM\Event\OnFlushEventArgs,
Doctrine\ORM\Events;
/**
* SoftDeletable Doctrine2 subscriber.
*
* Listens to onFlush event and marks SoftDeletable entities
* as deleted instead of really removing them.
*/
class SoftDeletableSubscriber extends AbstractSubscriber
{
private $softDeletableTrait;
public function __construct(ClassAnalyzer $classAnalyzer, $isRecursive, $softDeletableTrait)
{
parent::__construct($classAnalyzer, $isRecursive);
$this->softDeletableTrait = $softDeletableTrait;
}
/**
* Listens to onFlush event.
*
* @param OnFlushEventArgs $args The event arguments
*/
public function onFlush(OnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityDeletions() as $entity) {
$classMetadata = $em->getClassMetadata(get_class($entity));
if ($this->isSoftDeletable($classMetadata)) {
$oldValue = $entity->getDeletedAt();
$entity->delete();
$em->persist($entity);
$uow->propertyChanged($entity, 'deletedAt', $oldValue, $entity->getDeletedAt());
$uow->scheduleExtraUpdate($entity, [
'deletedAt' => [$oldValue, $entity->getDeletedAt()]
]);
}
}
}
/**
* Checks if entity is softDeletable
*
* @param ClassMetadata $classMetadata The metadata
*
* @return Boolean
*/
private function isSoftDeletable(ClassMetadata $classMetadata)
{
return $this->getClassAnalyzer()->hasTrait($classMetadata->reflClass, $this->softDeletableTrait, $this->isRecursive);
}
/**
* Returns list of events, that this subscriber is listening to.
*
* @return array
*/
public function getSubscribedEvents()
{
return [Events::onFlush, Events::loadClassMetadata];
}
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
$classMetadata = $eventArgs->getClassMetadata();
if (null === $classMetadata->reflClass) {
return;
}
if ($this->isSoftDeletable($classMetadata)) {
if (!$classMetadata->hasField('deletedAt')) {
$classMetadata->mapField(array(
'fieldName' => 'deletedAt',
'type' => 'datetime',
'nullable' => true
));
}
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Knp\DoctrineBehaviors\ORM\Sortable;
use Doctrine\ORM\QueryBuilder;
trait SortableRepository
{
public function reorderEntity($entity)
{
if (!$entity->isReordered()) {
return;
}
$qb = $this->createQueryBuilder('e')
->update($this->getEntityName(), 'e')
->set('e.sort', 'e.sort + 1')
->andWhere('e.sort >= :sort')
->setParameter('sort', $entity->getSort())
;
$entity->setReordered();
$this->addSortingScope($qb, $entity);
$qb
->getQuery()
->execute()
;
}
protected function addSortingScope(QueryBuilder $qb, $entity)
{
}
}

View File

@@ -0,0 +1,77 @@
<?php
/*
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\ORM\Sortable;
use Knp\DoctrineBehaviors\Reflection\ClassAnalyzer;
use Knp\DoctrineBehaviors\ORM\AbstractSubscriber;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs,
Doctrine\ORM\Events,
Doctrine\ORM\Mapping\ClassMetadata;
/**
* Sortable subscriber.
*
* Adds mapping to the sortable entities.
*/
class SortableSubscriber extends AbstractSubscriber
{
private $sortableTrait;
public function __construct(ClassAnalyzer $classAnalyzer, $isRecursive, $sortableTrait)
{
parent::__construct($classAnalyzer, $isRecursive);
$this->sortableTrait = $sortableTrait;
}
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
$classMetadata = $eventArgs->getClassMetadata();
if (null === $classMetadata->reflClass) {
return;
}
if ($this->isSortable($classMetadata)) {
if (!$classMetadata->hasField('sort')) {
$classMetadata->mapField(array(
'fieldName' => 'sort',
'type' => 'integer'
));
}
}
}
public function getSubscribedEvents()
{
return [Events::loadClassMetadata];
}
/**
* Checks if entity is a sortable
*
* @param ClassMetadata $classMetadata The metadata
*
* @return Boolean
*/
private function isSortable(ClassMetadata $classMetadata)
{
return $this->getClassAnalyzer()->hasTrait(
$classMetadata->reflClass,
$this->sortableTrait,
$this->isRecursive
);
}
}

View File

@@ -0,0 +1,86 @@
<?php
/*
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\ORM\Timestampable;
use Knp\DoctrineBehaviors\Reflection\ClassAnalyzer;
use Knp\DoctrineBehaviors\ORM\AbstractSubscriber;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs,
Doctrine\ORM\Events,
Doctrine\ORM\Mapping\ClassMetadata;
/**
* Timestampable subscriber.
*
* Adds mapping to the timestampable entites.
*/
class TimestampableSubscriber extends AbstractSubscriber
{
private $timestampableTrait;
private $dbFieldType;
public function __construct(ClassAnalyzer $classAnalyzer, $isRecursive, $timestampableTrait, $dbFieldType)
{
parent::__construct($classAnalyzer, $isRecursive);
$this->timestampableTrait = $timestampableTrait;
$this->dbFieldType = $dbFieldType;
}
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
$classMetadata = $eventArgs->getClassMetadata();
if (null === $classMetadata->reflClass) {
return;
}
if ($this->isTimestampable($classMetadata)) {
if ($this->getClassAnalyzer()->hasMethod($classMetadata->reflClass, 'updateTimestamps')) {
$classMetadata->addLifecycleCallback('updateTimestamps', Events::prePersist);
$classMetadata->addLifecycleCallback('updateTimestamps', Events::preUpdate);
}
foreach (array('createdAt', 'updatedAt') as $field) {
if (!$classMetadata->hasField($field)) {
$classMetadata->mapField(array(
'fieldName' => $field,
'type' => $this->dbFieldType,
'nullable' => true
));
}
}
}
}
public function getSubscribedEvents()
{
return [Events::loadClassMetadata];
}
/**
* Checks if entity is timestampable
*
* @param ClassMetadata $classMetadata The metadata
*
* @return Boolean
*/
private function isTimestampable(ClassMetadata $classMetadata)
{
return $this->getClassAnalyzer()->hasTrait(
$classMetadata->reflClass,
$this->timestampableTrait,
$this->isRecursive
);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Knp\DoctrineBehaviors\ORM\Translatable;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\DependencyInjection\Container;
/**
* @author Florian Klein <florian.klein@free.fr>
*/
class CurrentLocaleCallable
{
private $container;
public function __construct(Container $container)
{
$this->container = $container;
}
public function __invoke()
{
if (!$this->container->has('request_stack')) {
if(!$this->container->isScopeActive('request')) {
return NULL;
}
$request = $this->container->get('request');
return $request->getLocale();
} else if ($request = $this->container->get('request_stack')->getCurrentRequest()) {
return $request->getLocale();
}
return NULL;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Knp\DoctrineBehaviors\ORM\Translatable;
use Symfony\Component\DependencyInjection\Container;
/**
* @author Jérôme Fix <jerome.fix@zapoyok.info>
*/
class DefaultLocaleCallable
{
private $locale;
public function __construct($locale)
{
$this->locale = $locale;
}
public function __invoke()
{
return $this->locale;
}
}

View File

@@ -0,0 +1,359 @@
<?php
/*
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\ORM\Translatable;
use Knp\DoctrineBehaviors\Reflection\ClassAnalyzer;
use Knp\DoctrineBehaviors\ORM\AbstractSubscriber;
use Doctrine\ORM\ORMException;
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
use Doctrine\ORM\Id\BigIntegerIdentityGenerator;
use Doctrine\ORM\Id\IdentityGenerator;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Events;
use Doctrine\DBAL\Platforms;
/**
* Translatable Doctrine2 subscriber.
*
* Provides mapping for translatable entities and their translations.
*/
class TranslatableSubscriber extends AbstractSubscriber
{
private $currentLocaleCallable;
private $defaultLocaleCallable;
private $translatableTrait;
private $translationTrait;
private $translatableFetchMode;
private $translationFetchMode;
public function __construct(ClassAnalyzer $classAnalyzer, callable $currentLocaleCallable = null,
callable $defaultLocaleCallable = null,$translatableTrait, $translationTrait,
$translatableFetchMode, $translationFetchMode)
{
parent::__construct($classAnalyzer, false);
$this->currentLocaleCallable = $currentLocaleCallable;
$this->defaultLocaleCallable = $defaultLocaleCallable;
$this->translatableTrait = $translatableTrait;
$this->translationTrait = $translationTrait;
$this->translatableFetchMode = $this->convertFetchString($translatableFetchMode);
$this->translationFetchMode = $this->convertFetchString($translationFetchMode);
}
/**
* Adds mapping to the translatable and translations.
*
* @param LoadClassMetadataEventArgs $eventArgs The event arguments
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
$classMetadata = $eventArgs->getClassMetadata();
if (null === $classMetadata->reflClass) {
return;
}
if ($this->isTranslatable($classMetadata)) {
$this->mapTranslatable($classMetadata);
}
if ($this->isTranslation($classMetadata)) {
$this->mapTranslation($classMetadata);
$this->mapId(
$classMetadata,
$eventArgs->getEntityManager()
);
}
}
/**
* Kept for BC-compatibility purposes : people expect this lib to map ids for
* translations.
*
* @deprecated It should be removed because it probably does not work with
* every doctrine version.
*
* @see https://github.com/doctrine/doctrine2/blob/0bff6aadbc9f3fd8167a320d9f4f6cf269382da0/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php#L508
*/
private function mapId(ClassMetadata $class, EntityManager $em)
{
$platform = $em->getConnection()->getDatabasePlatform();
if (!$class->hasField('id')) {
$builder = new ClassMetadataBuilder($class);
$builder->createField('id', 'integer')->isPrimaryKey()->generatedValue()->build();
/// START DOCTRINE CODE
$idGenType = $class->generatorType;
if ($idGenType == ClassMetadata::GENERATOR_TYPE_AUTO) {
if ($platform->prefersSequences()) {
$class->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_SEQUENCE);
} else if ($platform->prefersIdentityColumns()) {
$class->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
} else {
$class->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_TABLE);
}
}
// Create & assign an appropriate ID generator instance
switch ($class->generatorType) {
case ClassMetadata::GENERATOR_TYPE_IDENTITY:
// For PostgreSQL IDENTITY (SERIAL) we need a sequence name. It defaults to
// <table>_<column>_seq in PostgreSQL for SERIAL columns.
// Not pretty but necessary and the simplest solution that currently works.
$sequenceName = null;
$fieldName = $class->identifier ? $class->getSingleIdentifierFieldName() : null;
if ($platform instanceof Platforms\PostgreSQLPlatform) {
$columnName = $class->getSingleIdentifierColumnName();
$quoted = isset($class->fieldMappings[$fieldName]['quoted']) || isset($class->table['quoted']);
$sequenceName = $class->getTableName() . '_' . $columnName . '_seq';
$definition = array(
'sequenceName' => $platform->fixSchemaElementName($sequenceName)
);
if ($quoted) {
$definition['quoted'] = true;
}
$sequenceName = $em->getConfiguration()->getQuoteStrategy()->getSequenceName($definition, $class, $platform);
}
$generator = ($fieldName && $class->fieldMappings[$fieldName]['type'] === 'bigint')
? new BigIntegerIdentityGenerator($sequenceName)
: new IdentityGenerator($sequenceName);
$class->setIdGenerator($generator);
break;
case ClassMetadata::GENERATOR_TYPE_SEQUENCE:
// If there is no sequence definition yet, create a default definition
$definition = $class->sequenceGeneratorDefinition;
if ( ! $definition) {
$fieldName = $class->getSingleIdentifierFieldName();
$columnName = $class->getSingleIdentifierColumnName();
$quoted = isset($class->fieldMappings[$fieldName]['quoted']) || isset($class->table['quoted']);
$sequenceName = $class->getTableName() . '_' . $columnName . '_seq';
$definition = array(
'sequenceName' => $platform->fixSchemaElementName($sequenceName),
'allocationSize' => 1,
'initialValue' => 1,
);
if ($quoted) {
$definition['quoted'] = true;
}
$class->setSequenceGeneratorDefinition($definition);
}
$sequenceGenerator = new \Doctrine\ORM\Id\SequenceGenerator(
$em->getConfiguration()->getQuoteStrategy()->getSequenceName($definition, $class, $platform),
$definition['allocationSize']
);
$class->setIdGenerator($sequenceGenerator);
break;
case ClassMetadata::GENERATOR_TYPE_NONE:
$class->setIdGenerator(new \Doctrine\ORM\Id\AssignedGenerator());
break;
case ClassMetadata::GENERATOR_TYPE_UUID:
$class->setIdGenerator(new \Doctrine\ORM\Id\UuidGenerator());
break;
case ClassMetadata::GENERATOR_TYPE_TABLE:
throw new ORMException("TableGenerator not yet implemented.");
break;
case ClassMetadata::GENERATOR_TYPE_CUSTOM:
$definition = $class->customGeneratorDefinition;
if ( ! class_exists($definition['class'])) {
throw new ORMException("Can't instantiate custom generator : " .
$definition['class']);
}
$class->setIdGenerator(new $definition['class']);
break;
default:
throw new ORMException("Unknown generator type: " . $class->generatorType);
}
/// END DOCTRINE COPY / PASTED code
}
}
private function mapTranslatable(ClassMetadata $classMetadata)
{
if (!$classMetadata->hasAssociation('translations')) {
$classMetadata->mapOneToMany([
'fieldName' => 'translations',
'mappedBy' => 'translatable',
'indexBy' => 'locale',
'cascade' => ['persist', 'merge', 'remove'],
'fetch' => $this->translatableFetchMode,
'targetEntity' => $classMetadata->getReflectionClass()->getMethod('getTranslationEntityClass')->invoke(null),
'orphanRemoval' => true
]);
}
}
private function mapTranslation(ClassMetadata $classMetadata)
{
if (!$classMetadata->hasAssociation('translatable')) {
$classMetadata->mapManyToOne([
'fieldName' => 'translatable',
'inversedBy' => 'translations',
'cascade' => ['persist', 'merge'],
'fetch' => $this->translationFetchMode,
'joinColumns' => [[
'name' => 'translatable_id',
'referencedColumnName' => 'id',
'onDelete' => 'CASCADE'
]],
'targetEntity' => $classMetadata->getReflectionClass()->getMethod('getTranslatableEntityClass')->invoke(null),
]);
}
$name = $classMetadata->getTableName().'_unique_translation';
if (!$this->hasUniqueTranslationConstraint($classMetadata, $name)) {
$classMetadata->table['uniqueConstraints'][$name] = [
'columns' => ['translatable_id', 'locale' ]
];
}
if (!($classMetadata->hasField('locale') || $classMetadata->hasAssociation('locale'))) {
$classMetadata->mapField(array(
'fieldName' => 'locale',
'type' => 'string'
));
}
}
/**
* Convert string FETCH mode to required string
*
* @param $fetchMode
*
* @return int
*/
private function convertFetchString($fetchMode){
if (is_int($fetchMode)) return $fetchMode;
switch($fetchMode){
case "LAZY":
return ClassMetadataInfo::FETCH_LAZY;
case "EAGER":
return ClassMetadataInfo::FETCH_EAGER;
case "EXTRA_LAZY":
return ClassMetadataInfo::FETCH_EXTRA_LAZY;
default:
return ClassMetadataInfo::FETCH_LAZY;
}
}
private function hasUniqueTranslationConstraint(ClassMetadata $classMetadata, $name)
{
if (!isset($classMetadata->table['uniqueConstraints'])) {
return;
}
return isset($classMetadata->table['uniqueConstraints'][$name]);
}
/**
* Checks if entity is translatable
*
* @param ClassMetadata $classMetadata
*
* @return boolean
*/
private function isTranslatable(ClassMetadata $classMetadata)
{
return $this->getClassAnalyzer()->hasTrait($classMetadata->reflClass, $this->translatableTrait);
}
/**
* Checks if entity is a translation
*
* @param ClassMetadata $classMetadata
*
* @return boolean
*/
private function isTranslation(ClassMetadata $classMetadata)
{
return $this->getClassAnalyzer()->hasTrait($classMetadata->reflClass, $this->translationTrait);
}
public function postLoad(LifecycleEventArgs $eventArgs)
{
$this->setLocales($eventArgs);
}
public function prePersist(LifecycleEventArgs $eventArgs)
{
$this->setLocales($eventArgs);
}
private function setLocales(LifecycleEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$entity = $eventArgs->getEntity();
$classMetadata = $em->getClassMetadata(get_class($entity));
if (!$this->isTranslatable($classMetadata)) {
return;
}
if ($locale = $this->getCurrentLocale()) {
$entity->setCurrentLocale($locale);
}
if ($locale = $this->getDefaultLocale()) {
$entity->setDefaultLocale($locale);
}
}
private function getCurrentLocale()
{
if ($currentLocaleCallable = $this->currentLocaleCallable) {
return $currentLocaleCallable();
}
}
private function getDefaultLocale()
{
if ($defaultLocaleCallable = $this->defaultLocaleCallable) {
return $defaultLocaleCallable();
}
}
/**
* Returns hash of events, that this subscriber is bound to.
*
* @return array
*/
public function getSubscribedEvents()
{
return [
Events::loadClassMetadata,
Events::postLoad,
Events::prePersist,
];
}
}

View File

@@ -0,0 +1,148 @@
<?php
namespace Knp\DoctrineBehaviors\ORM\Tree;
use Knp\DoctrineBehaviors\Model\Tree\NodeInterface;
use Doctrine\ORM\QueryBuilder;
trait Tree
{
/**
* Constructs a query builder to get all root nodes
*
* @param string $rootAlias
*
* @return QueryBuilder
*/
public function getRootNodesQB($rootAlias = 't')
{
return $this->createQueryBuilder($rootAlias)
->andWhere($rootAlias.'.materializedPath = :empty')
->setParameter('empty', '')
;
}
/**
* Returns all root nodes
*
* @api
*
* @param string $rootAlias
*
* @return array
*/
public function getRootNodes($rootAlias = 't')
{
return $this
->getRootNodesQB($rootAlias)
->getQuery()
->execute()
;
}
/**
* Returns a node hydrated with its children and parents
*
* @api
*
* @param string $path
* @param string $rootAlias
* @param array $extraParams To be used in addFlatTreeConditions
*
* @return NodeInterface a node
*/
public function getTree($path = '', $rootAlias = 't', $extraParams = array())
{
$results = $this->getFlatTree($path, $rootAlias, $extraParams);
return $this->buildTree($results);
}
public function getTreeExceptNodeAndItsChildrenQB(NodeInterface $entity, $rootAlias = 't')
{
return $this->getFlatTreeQB('', $rootAlias)
->andWhere($rootAlias.'.materializedPath NOT LIKE :except_path')
->andWhere($rootAlias.'.id != :id')
->setParameter('except_path', $entity->getRealMaterializedPath().'%')
->setParameter('id', $entity->getId())
;
}
/**
* Extracts the root node and constructs a tree using flat resultset
*
* @param Iterable|array $results a flat resultset
*
* @return NodeInterface
*/
public function buildTree($results)
{
if (!count($results)) {
return;
}
$root = $results[0];
$root->buildTree($results);
return $root;
}
/**
* Constructs a query builder to get a flat tree, starting from a given path
*
* @param string $path
* @param string $rootAlias
* @param array $extraParams To be used in addFlatTreeConditions
*
* @return QueryBuilder
*/
public function getFlatTreeQB($path = '', $rootAlias = 't', $extraParams = array())
{
$qb = $this->createQueryBuilder($rootAlias)
->andWhere($rootAlias.'.materializedPath LIKE :path')
->addOrderBy($rootAlias.'.materializedPath', 'ASC')
->setParameter('path', $path.'%')
;
$parentId = basename($path);
if ($parentId) {
$qb
->orWhere($rootAlias.'.id = :parent')
->setParameter('parent', $parentId)
;
}
$this->addFlatTreeConditions($qb, $extraParams);
return $qb;
}
/**
* manipulates the flat tree query builder before executing it.
* Override this method to customize the tree query
*
* @param QueryBuilder $qb
* @param array $extraParams
*/
protected function addFlatTreeConditions(QueryBuilder $qb, $extraParams)
{
}
/**
* Executes the flat tree query builder
*
* @param string $path
* @param string $rootAlias
* @param array $extraParams To be used in addFlatTreeConditions
*
* @return array the flat resultset
*/
public function getFlatTree($path, $rootAlias = 't', $extraParams = array())
{
return $this
->getFlatTreeQB($path, $rootAlias, $extraParams)
->getQuery()
->execute()
;
}
}

View File

@@ -0,0 +1,78 @@
<?php
/*
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\ORM\Tree;
use Knp\DoctrineBehaviors\Reflection\ClassAnalyzer;
use Knp\DoctrineBehaviors\ORM\AbstractSubscriber;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs,
Doctrine\ORM\Events,
Doctrine\ORM\Mapping\ClassMetadata;
/**
* Tree subscriber.
*
* Adds mapping to the tree entities.
*/
class TreeSubscriber extends AbstractSubscriber
{
private $nodeTrait;
public function __construct(ClassAnalyzer $classAnalyzer, $isRecursive, $nodeTrait)
{
parent::__construct($classAnalyzer, $isRecursive);
$this->nodeTrait = $nodeTrait;
}
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
$classMetadata = $eventArgs->getClassMetadata();
if (null === $classMetadata->reflClass) {
return;
}
if ($this->isTreeNode($classMetadata)) {
if (!$classMetadata->hasField('materializedPath')) {
$classMetadata->mapField(array(
'fieldName' => 'materializedPath',
'type' => 'string',
'length' => 255
));
}
}
}
public function getSubscribedEvents()
{
return [Events::loadClassMetadata];
}
/**
* Checks if entity is a tree
*
* @param ClassMetadata $classMetadata The metadata
*
* @return Boolean
*/
private function isTreeNode(ClassMetadata $classMetadata)
{
return $this->getClassAnalyzer()->hasTrait(
$classMetadata->reflClass,
$this->nodeTrait,
$this->isRecursive
);
}
}

View File

@@ -0,0 +1,68 @@
<?php
/**
* This file is part of the KnpDoctrineBehaviors package.
*
* (c) KnpLabs <http://knplabs.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Knp\DoctrineBehaviors\Reflection;
use Doctrine\Common\EventSubscriber;
class ClassAnalyzer
{
/**
* Return TRUE if the given object use the given trait, FALSE if not
* @param ReflectionClass $class
* @param string $traitName
* @param boolean $isRecursive
*/
public function hasTrait(\ReflectionClass $class, $traitName, $isRecursive = false)
{
if (in_array($traitName, $class->getTraitNames())) {
return true;
}
$parentClass = $class->getParentClass();
if ((false === $isRecursive) || (false === $parentClass) || (null === $parentClass)) {
return false;
}
return $this->hasTrait($parentClass, $traitName, $isRecursive);
}
/**
* Return TRUE if the given object has the given method, FALSE if not
* @param ReflectionClass $class
* @param string $methodName
*/
public function hasMethod(\ReflectionClass $class, $methodName)
{
return $class->hasMethod($methodName);
}
/**
* Return TRUE if the given object has the given property, FALSE if not
* @param ReflectionClass $class
* @param string $propertyName
*/
public function hasProperty(\ReflectionClass $class, $propertyName)
{
if ($class->hasProperty($propertyName)) {
return true;
}
$parentClass = $class->getParentClass();
if (false === $parentClass) {
return false;
}
return $this->hasProperty($parentClass, $propertyName);
}
}

View File

@@ -0,0 +1,193 @@
<?php
namespace Tests\Knp\DoctrineBehaviors\ORM;
use Knp\DoctrineBehaviors\Reflection\ClassAnalyzer;
use Doctrine\Common\EventManager;
require_once 'EntityManagerProvider.php';
class BlameableTest extends \PHPUnit_Framework_TestCase
{
private $subscriber;
use EntityManagerProvider;
protected function getUsedEntityFixtures()
{
return [
'BehaviorFixtures\\ORM\\BlameableEntity',
'BehaviorFixtures\\ORM\\UserEntity'
];
}
protected function getEventManager($user = null, $userCallback = null, $userEntity = null)
{
$em = new EventManager;
$this->subscriber = new \Knp\DoctrineBehaviors\ORM\Blameable\BlameableSubscriber(
new ClassAnalyzer(),
false,
'Knp\DoctrineBehaviors\Model\Blameable\Blameable',
$userCallback,
$userEntity
);
$this->subscriber->setUser($user);
$em->addEventSubscriber($this->subscriber);
return $em;
}
public function testCreate()
{
$em = $this->getEntityManager($this->getEventManager('user'));
$entity = new \BehaviorFixtures\ORM\BlameableEntity();
$em->persist($entity);
$em->flush();
$this->assertEquals('user', $entity->getCreatedBy());
$this->assertEquals('user', $entity->getUpdatedBy());
$this->assertNull($entity->getDeletedBy());
}
public function testUpdate()
{
$em = $this->getEntityManager($this->getEventManager('user'));
$entity = new \BehaviorFixtures\ORM\BlameableEntity();
$em->persist($entity);
$em->flush();
$id = $entity->getId();
$createdBy = $entity->getCreatedBy();
$em->clear();
$subscribers = $em->getEventManager()->getListeners()['preUpdate'];
$subscriber = array_pop($subscribers);
$subscriber->setUser('user2');
$entity = $em->getRepository('BehaviorFixtures\ORM\BlameableEntity')->find($id);
$entity->setTitle('test'); // need to modify at least one column to trigger onUpdate
$em->flush();
$em->clear();
//$entity = $em->getRepository('BehaviorFixtures\ORM\BlameableEntity')->find($id);
$this->assertEquals($createdBy, $entity->getCreatedBy(), 'createdBy is constant');
$this->assertEquals('user2', $entity->getUpdatedBy());
$this->assertNotEquals(
$entity->getCreatedBy(),
$entity->getUpdatedBy(),
'createBy and updatedBy have diverged since new update'
);
}
public function testRemove()
{
$em = $this->getEntityManager($this->getEventManager('user'));
$entity = new \BehaviorFixtures\ORM\BlameableEntity();
$em->persist($entity);
$em->flush();
$id = $entity->getId();
$em->clear();
$subscribers = $em->getEventManager()->getListeners()['preRemove'];
$subscriber = array_pop($subscribers);
$subscriber->setUser('user3');
$entity = $em->getRepository('BehaviorFixtures\ORM\BlameableEntity')->find($id);
$em->remove($entity);
$em->flush();
$em->clear();
$this->assertEquals('user3', $entity->getDeletedBy());
}
public function testSubscriberWithUserCallback()
{
$user = new \BehaviorFixtures\ORM\UserEntity();
$user->setUsername('user');
$user2 = new \BehaviorFixtures\ORM\UserEntity();
$user2->setUsername('user2');
$userCallback = function() use($user) {
return $user;
};
$em = $this->getEntityManager($this->getEventManager(null, $userCallback, get_class($user)));
$em->persist($user);
$em->persist($user2);
$em->flush();
$entity = new \BehaviorFixtures\ORM\BlameableEntity();
$em->persist($entity);
$em->flush();
$id = $entity->getId();
$createdBy = $entity->getCreatedBy();
$this->subscriber->setUser($user2); // switch user for update
$entity = $em->getRepository('BehaviorFixtures\ORM\BlameableEntity')->find($id);
$entity->setTitle('test'); // need to modify at least one column to trigger onUpdate
$em->flush();
$em->clear();
$this->assertInstanceOf('BehaviorFixtures\\ORM\\UserEntity', $entity->getCreatedBy(), 'createdBy is a user object');
$this->assertEquals($createdBy->getUsername(), $entity->getCreatedBy()->getUsername(), 'createdBy is constant');
$this->assertEquals($user2->getUsername(), $entity->getUpdatedBy()->getUsername());
$this->assertNotEquals(
$entity->getCreatedBy(),
$entity->getUpdatedBy(),
'createBy and updatedBy have diverged since new update'
);
}
/**
* @test
*/
public function should_only_persist_user_entity()
{
$user = new \BehaviorFixtures\ORM\UserEntity();
$user->setUsername('user');
$userCallback = function() use($user) {
return $user;
};
$em = $this->getEntityManager($this->getEventManager('anon.', $userCallback, get_class($user)));
$em->persist($user);
$em->flush();
$entity = new \BehaviorFixtures\ORM\BlameableEntity();
$em->persist($entity);
$em->flush();
$this->assertNull($entity->getCreatedBy(), 'createdBy is a not updated because not a user entity object');
$this->assertNull($entity->getUpdatedBy(), 'updatedBy is a not updated because not a user entity object');
}
/**
* @test
*/
public function should_only_persist_user_string()
{
$user = new \BehaviorFixtures\ORM\UserEntity();
$em = $this->getEntityManager($this->getEventManager($user));
$entity = new \BehaviorFixtures\ORM\BlameableEntity();
$em->persist($entity);
$em->flush();
$this->assertNull($entity->getCreatedBy(), 'createdBy is a not updated because not a user entity object');
$this->assertNull($entity->getUpdatedBy(), 'updatedBy is a not updated because not a user entity object');
}
}

View File

@@ -0,0 +1,194 @@
<?php
namespace tests\Knp\DoctrineBehaviors\ORM;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Repository\DefaultRepositoryFactory;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\Common\EventManager;
use Doctrine\ORM\Mapping\DefaultQuoteStrategy;
trait EntityManagerProvider
{
private $em;
abstract protected function getUsedEntityFixtures();
/**
* EntityManager mock object together with
* annotation mapping driver and pdo_sqlite
* database in memory
*
* @param EventManager $evm
* @return EntityManager
*/
protected function getEntityManager(EventManager $evm = null, Configuration $config = null, array $conn = [])
{
if (null !== $this->em) {
return $this->em;
}
$conn = array_merge(array(
'driver' => 'pdo_sqlite',
'memory' => true,
), $conn);
$config = is_null($config) ? $this->getAnnotatedConfig() : $config;
$em = EntityManager::create($conn, $config, $evm ?: $this->getEventManager());
$schema = array_map(function ($class) use ($em) {
return $em->getClassMetadata($class);
}, (array) $this->getUsedEntityFixtures());
$schemaTool = new SchemaTool($em);
$schemaTool->dropSchema($schema);
$schemaTool->createSchema($schema);
return $this->em = $em;
}
/**
* EntityManager mock object together with
* annotation mapping driver and engine given
* by DB_ENGINE (pdo_mysql or pdo_pgsql)
* database in memory
*
* @return \Doctrine\ORM\EntityManager
*/
protected function getDBEngineEntityManager()
{
if (DB_ENGINE == "pgsql") {
return $this->getEntityManager(
null,
null,
[
'driver' => 'pdo_pgsql',
'host' => DB_HOST,
'dbname' => DB_NAME,
'user' => DB_USER,
'password' => DB_PASSWD
]
);
} else {
return $this->getEntityManager(
null,
null,
[
'driver' => 'pdo_mysql',
'host' => DB_HOST,
'dbname' => DB_NAME,
'user' => DB_USER,
'password' => DB_PASSWD
]
);
}
}
/**
* Get annotation mapping configuration
*
* @return \Doctrine\ORM\Configuration
*/
protected function getAnnotatedConfig()
{
// We need to mock every method except the ones which
// handle the filters
$configurationClass = 'Doctrine\ORM\Configuration';
$refl = new \ReflectionClass($configurationClass);
$methods = $refl->getMethods();
$mockMethods = array();
foreach ($methods as $method) {
if (!in_array($method->name, ['addFilter', 'getFilterClassName', 'addCustomNumericFunction', 'getCustomNumericFunction'])) {
$mockMethods[] = $method->name;
}
}
$config = $this->getMock($configurationClass, $mockMethods);
$config
->expects($this->once())
->method('getProxyDir')
->will($this->returnValue(TESTS_TEMP_DIR))
;
$config
->expects($this->once())
->method('getProxyNamespace')
->will($this->returnValue('Proxy'))
;
$config
->expects($this->once())
->method('getAutoGenerateProxyClasses')
->will($this->returnValue(true))
;
$config
->expects($this->once())
->method('getClassMetadataFactoryName')
->will($this->returnValue('Doctrine\\ORM\\Mapping\\ClassMetadataFactory'))
;
$mappingDriver = $this->getMetadataDriverImplementation();
$config
->expects($this->any())
->method('getMetadataDriverImpl')
->will($this->returnValue($mappingDriver))
;
$config
->expects($this->any())
->method('getDefaultRepositoryClassName')
->will($this->returnValue('Doctrine\\ORM\\EntityRepository'))
;
if (class_exists('Doctrine\ORM\Mapping\DefaultQuoteStrategy')) {
$config
->expects($this->any())
->method('getQuoteStrategy')
->will($this->returnValue(new DefaultQuoteStrategy))
;
}
if (class_exists('Doctrine\ORM\Repository\DefaultRepositoryFactory')) {
$config
->expects($this->any())
->method('getRepositoryFactory')
->will($this->returnValue(new DefaultRepositoryFactory()))
;
}
$config
->expects($this->any())
->method('getDefaultQueryHints')
->will($this->returnValue([]))
;
return $config;
}
/**
* Creates default mapping driver
*
* @return \Doctrine\ORM\Mapping\Driver\Driver
*/
protected function getMetadataDriverImplementation()
{
return new AnnotationDriver($_ENV['annotation_reader']);
}
/**
* Build event manager
*
* @return EventManager
*/
protected function getEventManager()
{
return new EventManager;
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace tests\Knp\DoctrineBehaviors\ORM;
use BehaviorFixtures\ORM\FilterableEntity;
require_once 'EntityManagerProvider.php';
class FilterableRepositoryTest extends \PHPUnit_Framework_TestCase
{
use EntityManagerProvider;
protected function getUsedEntityFixtures()
{
return [
'BehaviorFixtures\\ORM\\FilterableEntity'
];
}
/**
* @test
*/
public function shouldFilterByNameUsingLike()
{
$this->createEntities();
/**@var \BehaviorFixtures\ORM\FilterableRepository $repository*/
$repository = $this->getEntityManager()->getRepository('BehaviorFixtures\ORM\FilterableEntity');
$collection = $repository->filterBy(['e:name' => 'name'])->getQuery()->execute();
$this->assertCount(2, $collection);
$this->assertEquals('name1', $collection[0]->getName());
$this->assertEquals('name2', $collection[1]->getName());
}
/**
* @test
*/
public function shouldFilterByCodeUsingEqual()
{
$this->createEntities();
$repository = $this->getEntityManager()->getRepository('BehaviorFixtures\ORM\FilterableEntity');
$collection = $repository->filterBy(['e:code' => '2'])->getQuery()->execute();
$this->assertCount(1, $collection);
$this->assertEquals('name1', $collection[0]->getName());
}
private function createEntities()
{
$em = $this->getEntityManager();
foreach ([2 => 'name1', 20 => 'name2', 40 => 'otherValue'] as $code => $name) {
$entity = new FilterableEntity();
$entity->setCode($code);
$entity->setName($name);
$em->persist($entity);
}
$em->flush();
}
}

View File

@@ -0,0 +1,266 @@
<?php
namespace Tests\Knp\DoctrineBehaviors\ORM;
use BehaviorFixtures\ORM\GeocodableEntity;
use Knp\DoctrineBehaviors\Reflection\ClassAnalyzer;
use Knp\DoctrineBehaviors\ORM\Geocodable\Type\Point;
use Doctrine\Common\EventManager;
require_once 'EntityManagerProvider.php';
class GeocodableTest extends \PHPUnit_Framework_TestCase
{
use EntityManagerProvider;
/**
* @var callable $callable
*/
private $callable;
protected function getUsedEntityFixtures()
{
return array(
'BehaviorFixtures\\ORM\\GeocodableEntity'
);
}
/**
* @return \Doctrine\Common\EventManager
*/
protected function getEventManager()
{
$em = new EventManager;
if ($this->callable === false) {
$callable = function ($entity) {
if ($location = $entity->getLocation()) {
return $location;
}
return Point::fromArray(
[
'longitude' => 47.7,
'latitude' => 7.9
]
);
};
} else {
$callable = $this->callable;
}
$em->addEventSubscriber(
new \Knp\DoctrineBehaviors\ORM\Geocodable\GeocodableSubscriber(
new ClassAnalyzer(),
false,
'Knp\DoctrineBehaviors\Model\Geocodable\Geocodable',
$callable
)
);
return $em;
}
public function setUp()
{
$em = $this->getDBEngineEntityManager();
$cities = $this->dataSetCities();
foreach ($cities as $city) {
$entity = new GeocodableEntity($city[1][0], $city[1][1]);
$entity->setTitle($city[0]);
$em->persist($entity);
};
$em->flush();
}
/**
* @dataProvider dataSetCities
*/
public function testInsertLocation($city, array $location)
{
$em = $this->getDBEngineEntityManager();
$repository = $em->getRepository('BehaviorFixtures\ORM\GeocodableEntity');
$entity = $repository->findOneByTitle($city);
$this->assertLocation($location, $entity->getLocation());
}
/**
* @dataProvider dataSetCities
*/
public function testUpdateWithEditLocation($city, array $location, array $newLocation)
{
$em = $this->getDBEngineEntityManager();
$repository = $em->getRepository('BehaviorFixtures\ORM\GeocodableEntity');
/** @var GeocodableEntity $entity */
$entity = $repository->findOneByTitle($city);
$entity->setLocation(new Point($newLocation[0], $newLocation[1]));
$newTitle = $city . " - edited";
$entity->setTitle($newTitle);
$em->flush();
/** @var GeocodableEntity $entity */
$entity = $repository->findOneByTitle($newTitle);
$this->assertEquals($newTitle, $entity->getTitle());
$this->assertLocation($newLocation, $entity->getLocation());
}
/**
* @dataProvider dataSetCities
*/
public function testUpdateWithoutEditLocation($city, array $location)
{
$em = $this->getDBEngineEntityManager();
$repository = $em->getRepository('BehaviorFixtures\ORM\GeocodableEntity');
/** @var GeocodableEntity $entity */
$entity = $repository->findOneByTitle($city);
$em->flush();
$this->assertLocation($location, $entity->getLocation());
}
/**
* @dataProvider dataSetCities
*/
public function testUpdateWithoutEditWithGeocodableWatcher($city, array $location, array $newLocation)
{
$this->callable = null;
$this->testUpdateWithEditLocation($city, $location, $newLocation);
}
public function testGetLocation()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\GeocodableEntity();
$em->persist($entity);
$em->flush();
$this->assertInstanceOf('Knp\DoctrineBehaviors\ORM\Geocodable\Type\Point', $entity->getLocation());
}
/**
* Geographical info
* Taken from http://andrew.hedges.name/experiments/haversine, I don't know if it's the same
* formula Postgresql uses, but it should do the trick.
*
* Requisheim <-> Paris ~384 km, ~238 miles
* Requisheim <-> Nantes ~671 km, ~417 miles
* Requisheim <-> New-York ~6217 km, ~3864 miles
*
* @dataProvider dataSetCitiesDistances
*/
public function testFindByDistance(array $location, $distance, $result, $text = null)
{
if (getenv("DB") == "mysql") {
$this->markTestSkipped("findByDistance does not work with MYSQL");
return null;
}
$em = $this->getDBEngineEntityManager();
$repo = $em->getRepository('BehaviorFixtures\ORM\GeocodableEntity');
$cities = $repo->findByDistance(new Point($location[0], $location[1]), $distance);
$this->assertCount($result, $cities, $text);
}
/**
* @return array
*/
public function dataSetCities()
{
return array(
array(
'New-York',
array(40.742786, -73.989272),
array(40.742787, -73.989273)
),
array(
'Paris',
array(48.858842, 2.355194),
array(48.858843, 2.355195)
),
array(
'Nantes',
array(47.218635, -1.544266),
array(47.218636, -1.544267)
)
);
}
/**
* From 'Reguisheim' (47.896319, 7.352943)
*
* @return array
*/
public function dataSetCitiesDistances()
{
return array(
array(
array(47.896319, 7.352943),
384000,
0,
'Paris is more than 384 km far from Reguisheim'
),
array(
array(47.896319, 7.352943),
385000,
1,
'Paris is less than 385 km far from Reguisheim'
),
array(
array(47.896319, 7.352943),
672000,
1,
'Nantes is more than 672 km far from Reguisheim'
),
array(
array(47.896319, 7.352943),
673000,
2,
'Paris and Nantes are less than 673 km far from Reguisheim'
),
array(
array(47.896319, 7.352943),
6222000,
2,
'New-York is more than 6222 km far from Reguisheim'
),
array(
array(47.896319, 7.352943),
6223000,
3,
'Paris, Nantes and New-York are less than 6223 km far from Reguisheim'
)
);
}
private function assertLocation(array $expected, Point $given = null, $message = null)
{
$this->assertInstanceOf('Knp\DoctrineBehaviors\ORM\Geocodable\Type\Point', $given, $message);
$this->assertEquals($expected[0], $given->getLatitude(), $message);
$this->assertEquals($expected[1], $given->getLongitude(), $message);
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace Tests\Knp\DoctrineBehaviors\ORM;
use Knp\DoctrineBehaviors\Reflection\ClassAnalyzer;
use Doctrine\Common\EventManager;
require_once 'EntityManagerProvider.php';
class LoggableTest extends \PHPUnit_Framework_TestCase
{
private $subscriber;
private $logs = [];
use EntityManagerProvider;
protected function getUsedEntityFixtures()
{
return [
'BehaviorFixtures\\ORM\\LoggableEntity',
];
}
protected function getEventManager()
{
$em = new EventManager;
$loggerCallback = function($message) {
$this->logs[] = $message;
};
$this->subscriber = new \Knp\DoctrineBehaviors\ORM\Loggable\LoggableSubscriber(
new ClassAnalyzer(),
false,
$loggerCallback
);
$em->addEventSubscriber($this->subscriber);
return $em;
}
/**
* @test
*
* @dataProvider dataProviderValues
*/
public function should_log_changeset_message_when_created($field, $value, $expected)
{
$em = $this->getEntityManager($this->getEventManager());
$entity = new \BehaviorFixtures\ORM\LoggableEntity();
$set = "set" . ucfirst($field);
$entity->$set($value);
$em->persist($entity);
$em->flush();
$this->assertCount(2, $this->logs);
$this->assertEquals(
$this->logs[0],
'BehaviorFixtures\ORM\LoggableEntity #1 created'
);
$this->assertEquals(
$this->logs[1],
'BehaviorFixtures\ORM\LoggableEntity #1 : property "' . $field . '" changed from "" to "' . $expected . '"'
);
}
/**
* @test
*
* @dataProvider dataProviderValues
*/
public function should_log_changeset_message_when_updated($field, $value, $expected)
{
$em = $this->getEntityManager($this->getEventManager());
$entity = new \BehaviorFixtures\ORM\LoggableEntity();
$em->persist($entity);
$em->flush();
$set = "set" . ucfirst($field);
$entity->$set($value);
$em->flush();
$this->assertCount(3, $this->logs);
$this->assertEquals(
$this->logs[2],
'BehaviorFixtures\ORM\LoggableEntity #1 : property "' . $field . '" changed from "" to "' . $expected . '"'
);
}
/**
* @test
*/
public function should_not_log_changeset_message_when_no_change()
{
$em = $this->getEntityManager($this->getEventManager());
$entity = new \BehaviorFixtures\ORM\LoggableEntity();
$em->persist($entity);
$em->flush();
$entity->setTitle('test2');
$entity->setTitle(null);
$em->flush();
$this->assertCount(2, $this->logs);
}
/**
* @test
*/
public function should_log_removal_message_when_deleted()
{
$em = $this->getEntityManager($this->getEventManager());
$entity = new \BehaviorFixtures\ORM\LoggableEntity();
$em->persist($entity);
$em->flush();
$em->remove($entity);
$em->flush();
$this->assertCount(3, $this->logs);
$this->assertEquals(
$this->logs[2],
'BehaviorFixtures\ORM\LoggableEntity #1 removed'
);
}
public function dataProviderValues() {
return array(
array(
"title", "test", "test"
),
array(
"roles", array("x" => "y"), "an array"
),
array(
"date", new \DateTime("2014-02-02 12:20:30.000010"), "2014-02-02 12:20:30.000010"
)
);
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace Tests\Knp\DoctrineBehaviors\ORM;
use Knp\DoctrineBehaviors\Reflection\ClassAnalyzer;
use Doctrine\Common\EventManager;
require_once 'EntityManagerProvider.php';
class SluggableMultiTest extends \PHPUnit_Framework_TestCase
{
use EntityManagerProvider;
protected function getUsedEntityFixtures()
{
return array(
'BehaviorFixtures\\ORM\\SluggableMultiEntity'
);
}
protected function getEventManager()
{
$em = new EventManager;
$em->addEventSubscriber(
new \Knp\DoctrineBehaviors\ORM\Sluggable\SluggableSubscriber(
new ClassAnalyzer(),
false,
'Knp\DoctrineBehaviors\Model\Sluggable\Sluggable'
));
return $em;
}
public function testSlugLoading()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\SluggableMultiEntity();
$expected = 'the+name+title';
$entity->setName('The name');
$em->persist($entity);
$em->flush();
$this->assertNotNull($id = $entity->getId());
$em->clear();
$entity = $em->getRepository('BehaviorFixtures\ORM\SluggableMultiEntity')->find($id);
$this->assertNotNull($entity);
$this->assertEquals($entity->getSlug(), $expected);
}
public function testNotUpdatedSlug()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\SluggableMultiEntity();
$expected = 'the+name+title';
$entity->setName('The name');
$em->persist($entity);
$em->flush();
$entity->setDate(new \DateTime);
$em->persist($entity);
$em->flush();
$this->assertEquals($entity->getSlug(), $expected);
}
public function testUpdatedSlug()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\SluggableMultiEntity();
$expected = 'the+name+title';
$entity->setName('The name');
$em->persist($entity);
$em->flush();
$this->assertEquals($entity->getSlug(), $expected);
$expected = 'the+name+2+title';
$entity->setName('The name 2');
$em->persist($entity);
$em->flush();
$this->assertEquals($entity->getSlug(), $expected);
}
}

View File

@@ -0,0 +1,126 @@
<?php
namespace Tests\Knp\DoctrineBehaviors\ORM;
use Knp\DoctrineBehaviors\Reflection\ClassAnalyzer;
use Doctrine\Common\EventManager;
require_once 'EntityManagerProvider.php';
class SluggableTest extends \PHPUnit_Framework_TestCase
{
use EntityManagerProvider;
protected function getUsedEntityFixtures()
{
return [
'BehaviorFixtures\\ORM\\SluggableEntity'
];
}
protected function getEventManager()
{
$em = new EventManager;
$em->addEventSubscriber(
new \Knp\DoctrineBehaviors\ORM\Sluggable\SluggableSubscriber(
new ClassAnalyzer(),
false,
'Knp\DoctrineBehaviors\Model\Sluggable\Sluggable'
));
return $em;
}
public function testSlugLoading()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\SluggableEntity();
$expected = 'the-name';
$entity->setName('The name');
$em->persist($entity);
$em->flush();
$this->assertNotNull($id = $entity->getId());
$em->clear();
$entity = $em->getRepository('BehaviorFixtures\ORM\SluggableEntity')->find($id);
$this->assertNotNull($entity);
$this->assertEquals($expected, $entity->getSlug());
}
public function testNotUpdatedSlug()
{
$em = $this->getEntityManager();
$data = [
[
'slug' => 'the-name',
'name' => 'The name',
],
[
'slug' => 'loic-rene',
'name' => 'Löic & René',
],
[
'slug' => 'ivan-ivanovich',
'name' => 'Иван Иванович',
],
[
'slug' => 'chateauneuf-du-pape',
'name' => 'Châteauneuf du Pape'
],
[
'slug' => 'zlutoucky-kun',
'name' => 'Žluťoučký kůň'
]
];
foreach ($data as $row) {
$entity = new \BehaviorFixtures\ORM\SluggableEntity();
$entity->setName($row['name']);
$em->persist($entity);
$em->flush();
$entity->setDate(new \DateTime);
$em->persist($entity);
$em->flush();
$this->assertEquals($row['slug'], $entity->getSlug());
}
}
public function testUpdatedSlug()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\SluggableEntity();
$expected = 'the-name';
$entity->setName('The name');
$em->persist($entity);
$em->flush();
$this->assertEquals($entity->getSlug(), $expected);
$expected = 'the-name-2';
$entity->setName('The name 2');
$em->persist($entity);
$em->flush();
$this->assertEquals($expected, $entity->getSlug());
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace Tests\Knp\DoctrineBehaviors\ORM;
use Knp\DoctrineBehaviors\Reflection\ClassAnalyzer;
use Doctrine\Common\EventManager;
require_once 'EntityManagerProvider.php';
class SoftDeletableTest extends \PHPUnit_Framework_TestCase
{
use EntityManagerProvider;
protected function getUsedEntityFixtures()
{
return array(
'BehaviorFixtures\\ORM\\DeletableEntity'
);
}
protected function getEventManager()
{
$em = new EventManager;
$em->addEventSubscriber(
new \Knp\DoctrineBehaviors\ORM\SoftDeletable\SoftDeletableSubscriber(
new ClassAnalyzer(),
true,
'Knp\DoctrineBehaviors\Model\SoftDeletable\SoftDeletable'
));
return $em;
}
public function testDelete()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\DeletableEntity();
$em->persist($entity);
$em->flush();
$this->assertNotNull($id = $entity->getId());
$this->assertFalse($entity->isDeleted());
$em->remove($entity);
$em->flush();
$em->clear();
$entity = $em->getRepository('BehaviorFixtures\ORM\DeletableEntity')->find($id);
$this->assertNotNull($entity);
$this->assertTrue($entity->isDeleted());
}
public function testPostDelete()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\DeletableEntity();
$em->persist($entity);
$em->flush();
$this->assertNotNull($id = $entity->getId());
$entity->setDeletedAt((new \DateTime())->modify('+1 day'));
$em->flush();
$em->clear();
$entity = $em->getRepository('BehaviorFixtures\ORM\DeletableEntity')->find($id);
$this->assertNotNull($entity);
$this->assertFalse($entity->isDeleted());
$this->assertTrue($entity->willBeDeleted());
$this->assertTrue($entity->willBeDeleted((new \DateTime())->modify('+2 day')));
$this->assertFalse($entity->willBeDeleted((new \DateTime())->modify('+12 hour')));
$entity->setDeletedAt((new \DateTime())->modify('-1 day'));
$em->flush();
$em->clear();
$entity = $em->getRepository('BehaviorFixtures\ORM\DeletableEntity')->find($id);
$this->assertNotNull($entity);
$this->assertTrue($entity->isDeleted());
}
public function testDeleteInheritance()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\DeletableEntityInherit();
$em->persist($entity);
$em->flush();
$em->remove($entity);
$em->flush();
$this->assertTrue($entity->isDeleted());
}
public function testRestore()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\DeletableEntityInherit();
$em->persist($entity);
$em->flush();
$em->remove($entity);
$em->flush();
$this->assertTrue($entity->isDeleted());
$entity->restore();
$this->assertFalse($entity->isDeleted());
}
}

View File

@@ -0,0 +1,168 @@
<?php
namespace Tests\Knp\DoctrineBehaviors\ORM;
use Knp\DoctrineBehaviors\Reflection\ClassAnalyzer;
use Doctrine\Common\EventManager;
require_once 'EntityManagerProvider.php';
class TimestampableTest extends \PHPUnit_Framework_TestCase
{
use EntityManagerProvider;
protected function getUsedEntityFixtures()
{
return array(
'BehaviorFixtures\\ORM\\TimestampableEntity'
);
}
protected function getEventManager()
{
$em = new EventManager;
$em->addEventSubscriber(
new \Knp\DoctrineBehaviors\ORM\Timestampable\TimestampableSubscriber(
new ClassAnalyzer(),
false,
'Knp\DoctrineBehaviors\Model\Timestampable\Timestampable',
'datetime'
));
return $em;
}
/**
* @test
*/
public function it_should_initialize_create_and_update_datetime_when_created()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\TimestampableEntity();
$em->persist($entity);
$em->flush();
$this->assertInstanceOf('Datetime', $entity->getCreatedAt());
$this->assertInstanceOf('Datetime', $entity->getUpdatedAt());
$this->assertEquals(
$entity->getCreatedAt(),
$entity->getUpdatedAt(),
'On creation, createdAt and updatedAt are the same'
);
}
/**
* @test
*/
public function it_should_modify_update_datetime_when_updated_but_not_the_creation_datetime()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\TimestampableEntity();
$em->persist($entity);
$em->flush();
$em->refresh($entity);
$id = $entity->getId();
$createdAt = $entity->getCreatedAt();
$em->clear();
// wait for a second:
sleep(1);
$entity = $em->getRepository('BehaviorFixtures\ORM\TimestampableEntity')->find($id);
$entity->setTitle('test');
$em->flush();
$em->clear();
$entity = $em->getRepository('BehaviorFixtures\ORM\TimestampableEntity')->find($id);
$this->assertEquals($createdAt, $entity->getCreatedAt(), 'createdAt is constant');
$this->assertNotEquals(
$entity->getCreatedAt(),
$entity->getUpdatedAt(),
'createat and updatedAt have diverged since new update'
);
}
/**
* @test
*/
public function it_should_return_the_same_datetime_when_not_updated()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\TimestampableEntity();
$em->persist($entity);
$em->flush();
$em->refresh($entity);
$id = $entity->getId();
$createdAt = $entity->getCreatedAt();
$updatedAt = $entity->getUpdatedAt();
$em->clear();
sleep(1);
$entity = $em->getRepository('BehaviorFixtures\ORM\TimestampableEntity')->find($id);
$em->persist($entity);
$em->flush();
$em->clear();
$this->assertEquals(
$entity->getCreatedAt(),
$createdAt,
'Creation timestamp has changed'
);
$this->assertEquals(
$entity->getUpdatedAt(),
$updatedAt,
'Update timestamp has changed'
);
}
/**
* @test
*/
public function it_should_modify_update_datetime_only_once()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\TimestampableEntity();
$em->persist($entity);
$em->flush();
$em->refresh($entity);
$id = $entity->getId();
$createdAt = $entity->getCreatedAt();
$em->clear();
sleep(1);
$entity = $em->getRepository('BehaviorFixtures\ORM\TimestampableEntity')->find($id);
$entity->setTitle('test');
$em->flush();
$updatedAt = $entity->getUpdatedAt();
sleep(1);
$em->flush();
$this->assertEquals(
$entity->getCreatedAt(),
$createdAt,
'Creation timestamp has changed'
);
$this->assertEquals(
$entity->getUpdatedAt(),
$updatedAt,
'Update timestamp has changed'
);
}
}

View File

@@ -0,0 +1,329 @@
<?php
namespace Tests\Knp\DoctrineBehaviors\ORM;
use Knp\DoctrineBehaviors\Reflection\ClassAnalyzer;
use Doctrine\Common\EventManager;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
require_once 'EntityManagerProvider.php';
class TranslatableTest extends \PHPUnit_Framework_TestCase
{
use EntityManagerProvider;
protected function getUsedEntityFixtures()
{
return [
'BehaviorFixtures\\ORM\\TranslatableEntity',
'BehaviorFixtures\\ORM\\TranslatableEntityTranslation'
];
}
protected function getEventManager()
{
$em = new EventManager;
$em->addEventSubscriber(new \Knp\DoctrineBehaviors\ORM\Translatable\TranslatableSubscriber(
new ClassAnalyzer(),
function()
{
return 'en';
},
function()
{
return 'en';
},
'Knp\DoctrineBehaviors\Model\Translatable\Translatable',
'Knp\DoctrineBehaviors\Model\Translatable\Translation',
'LAZY',
'LAZY'
));
return $em;
}
/**
* @test
*/
public function should_persist_translations()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\TranslatableEntity();
$entity->translate('fr')->setTitle('fabuleux');
$entity->translate('en')->setTitle('awesome');
$entity->translate('ru')->setTitle('удивительный');
$entity->mergeNewTranslations();
$em->persist($entity);
$em->flush();
$id = $entity->getId();
$em->clear();
$entity = $em
->getRepository('BehaviorFixtures\ORM\TranslatableEntity')
->find($id)
;
$this->assertEquals(
'fabuleux',
$entity->translate('fr')->getTitle()
);
$this->assertEquals(
'awesome',
$entity->translate('en')->getTitle()
);
$this->assertEquals(
'удивительный',
$entity->translate('ru')->getTitle()
);
}
/**
* @test
*/
public function should_fallback_country_locale_to_language_only_translation()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\TranslatableEntity();
$entity->translate('en', false)->setTitle('plastic bag');
$entity->translate('fr', false)->setTitle('sac plastique');
$entity->translate('fr_CH', false)->setTitle('cornet');
$entity->mergeNewTranslations();
$em->persist($entity);
$em->flush();
$id = $entity->getId();
$em->clear();
$entity = $em
->getRepository('BehaviorFixtures\ORM\TranslatableEntity')
->find($id)
;
$this->assertEquals(
'plastic bag',
$entity->translate('de')->getTitle()
);
$this->assertEquals(
'sac plastique',
$entity->translate('fr_FR')->getTitle()
);
$this->assertEquals(
'cornet',
$entity->translate('fr_CH')->getTitle()
);
}
/**
* @test
*/
public function should_update_and_add_new_translations()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\TranslatableEntity();
$entity->translate('en')->setTitle('awesome');
$entity->translate('ru')->setTitle('удивительный');
$entity->mergeNewTranslations();
$em->persist($entity);
$em->flush();
$id = $entity->getId();
$em->clear();
$entity = $em
->getRepository('BehaviorFixtures\ORM\TranslatableEntity')
->find($id)
;
$this->assertEquals(
'awesome',
$entity->translate('en')->getTitle()
);
$this->assertEquals(
'удивительный',
$entity->translate('ru')->getTitle()
);
$entity->translate('en')->setTitle('great');
$entity->translate('fr', false)->setTitle('fabuleux');
$entity->mergeNewTranslations();
$em->persist($entity);
$em->flush();
$em->clear();
$entity = $em
->getRepository('BehaviorFixtures\ORM\TranslatableEntity')
->find($id)
;
$this->assertEquals(
'great',
$entity->translate('en')->getTitle()
);
$this->assertEquals(
'fabuleux',
$entity->translate('fr')->getTitle()
);
$this->assertEquals(
'удивительный',
$entity->translate('ru')->getTitle()
);
}
/**
* @test
*/
public function translate_method_should_always_return_translation_object()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\TranslatableEntity();
$this->assertInstanceOf(
'BehaviorFixtures\ORM\TranslatableEntityTranslation',
$entity->translate('fr')
);
}
/**
* @test
*/
public function subscriber_should_configure_entity_with_current_locale()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\TranslatableEntity();
$entity->setTitle('test'); // magic method
$entity->mergeNewTranslations();
$em->persist($entity);
$em->flush();
$id = $entity->getId();
$em->clear();
$entity = $em->getRepository('BehaviorFixtures\ORM\TranslatableEntity')->find($id);
$this->assertEquals('en', $entity->getCurrentLocale());
$this->assertEquals('test', $entity->getTitle());
$this->assertEquals('test', $entity->translate($entity->getCurrentLocale())->getTitle());
}
/**
* @test
*/
public function subscriber_should_configure_entity_with_default_locale()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\TranslatableEntity();
$entity->setTitle('test'); // magic method
$entity->mergeNewTranslations();
$em->persist($entity);
$em->flush();
$id = $entity->getId();
$em->clear();
$entity = $em->getRepository('BehaviorFixtures\ORM\TranslatableEntity')->find($id);
$this->assertEquals('en', $entity->getDefaultLocale());
$this->assertEquals('test', $entity->getTitle());
$this->assertEquals('test', $entity->translate($entity->getDefaultLocale())->getTitle());
$this->assertEquals('test', $entity->translate('fr')->getTitle());
}
/**
* @test
*/
public function should_have_oneToMany_relation()
{
$this->assertTranslationsOneToManyMapped(
'BehaviorFixtures\ORM\TranslatableEntity',
'BehaviorFixtures\ORM\TranslatableEntityTranslation'
);
}
/**
* @test
*/
public function should_have_oneToMany_relation_when_translation_class_name_is_custom()
{
$this->assertTranslationsOneToManyMapped(
'BehaviorFixtures\ORM\TranslatableCustomizedEntity',
'BehaviorFixtures\ORM\Translation\TranslatableCustomizedEntityTranslation'
);
}
/**
* @test
*/
public function should_create_only_one_time_the_same_translation()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\TranslatableEntity();
$translation = $entity->translate('fr');
$translation->setTitle('fabuleux');
$entity->translate('fr')->setTitle('fabuleux2');
$entity->translate('fr')->setTitle('fabuleux3');
$this->assertEquals('fabuleux3', $entity->translate('fr')->getTitle());
$this->assertEquals(spl_object_hash($entity->translate('fr')), spl_object_hash($translation));
}
/**
* @test
*/
public function should_remove_translation()
{
$em = $this->getEntityManager();
$entity = new \BehaviorFixtures\ORM\TranslatableEntity();
$entity->translate('en')->setTitle('Hello');
$entity->translate('nl')->setTitle('Hallo');
$entity->mergeNewTranslations();
$em->persist($entity);
$em->flush();
$nlTranslation = $entity->translate('nl');
$entity->removeTranslation($nlTranslation);
$em->flush();
$em->refresh($entity);
$this->assertNotEquals('Hallo', $entity->translate('nl')->getTitle());
}
/**
* Asserts that the one to many relationship between translatable and translations is mapped correctly.
*
* @param string $translatableClass The class name of the translatable entity
* @param string $translationClass The class name of the translation entity
*/
private function assertTranslationsOneToManyMapped($translatableClass, $translationClass)
{
$em = $this->getEntityManager();
$meta = $em->getClassMetadata($translationClass);
$this->assertEquals($translatableClass, $meta->getAssociationTargetClass('translatable'));
$meta = $em->getClassMetadata($translatableClass);
$this->assertEquals($translationClass, $meta->getAssociationTargetClass('translations'));
$this->assertTrue($meta->isAssociationInverseSide('translations'));
$this->assertEquals(
ClassMetadataInfo::ONE_TO_MANY,
$meta->getAssociationMapping('translations')['type']
);
}
}

View File

@@ -0,0 +1,317 @@
<?php
namespace Tests\Knp\DoctrineBehaviors\ORM\Tree;
use Knp\DoctrineBehaviors\Model\Tree\NodeInterface;
use Tests\Knp\DoctrineBehaviors\ORM\EntityManagerProvider;
use BehaviorFixtures\ORM\TreeNodeEntity;
use Knp\DoctrineBehaviors\ORM\Tree\TreeSubscriber;
use Knp\DoctrineBehaviors\Reflection\ClassAnalyzer;
use Doctrine\Common\EventManager;
require_once __DIR__.'/../EntityManagerProvider.php';
class NodeTest extends \PHPUnit_Framework_TestCase
{
use EntityManagerProvider;
protected function getUsedEntityFixtures()
{
return array(
'BehaviorFixtures\\ORM\\TreeNodeEntity'
);
}
protected function getEventManager()
{
$em = new EventManager;
$em->addEventSubscriber(
new TreeSubscriber(
new ClassAnalyzer(),
false,
'Knp\DoctrineBehaviors\Model\Tree\Node'
)
);
return $em;
}
protected function buildNode(array $values = array())
{
$node = new TreeNodeEntity;
foreach ($values as $method => $value) {
$node->$method($value);
}
return $node;
}
private function buildTree()
{
$item = $this->buildNode();
$item->setMaterializedPath('');
$item->setId(1);
$childItem = $this->buildNode();
$childItem->setMaterializedPath('/1');
$childItem->setId(2);
$childItem->setChildNodeOf($item);
$secondChildItem = $this->buildNode();
$secondChildItem->setMaterializedPath('/1');
$secondChildItem->setId(3);
$secondChildItem->setChildNodeOf($item);
$childChildItem = $this->buildNode();
$childChildItem->setId(4);
$childChildItem->setMaterializedPath('/1/2');
$childChildItem->setChildNodeOf($childItem);
$childChildChildItem = $this->buildNode();
$childChildChildItem->setId(5);
$childChildChildItem->setMaterializedPath('/1/2/4');
$childChildChildItem->setChildNodeOf($childChildItem);
return $item;
}
public function testBuildTree()
{
$root = $this->buildNode(array('setMaterializedPath' => '' , 'setName' => 'root' , 'setId' => 1));
$flatTree = array(
$this->buildNode(array('setMaterializedPath' => '/1' , 'setName' => 'Villes' , 'setId' => 2)),
$this->buildNode(array('setMaterializedPath' => '/1/2' , 'setName' => 'Nantes' , 'setId' => 3)),
$this->buildNode(array('setMaterializedPath' => '/1/2/3' , 'setName' => 'Nantes Est' , 'setId' => 4)),
$this->buildNode(array('setMaterializedPath' => '/1/2/3' , 'setName' => 'Nantes Nord' , 'setId' => 5)),
$this->buildNode(array('setMaterializedPath' => '/1/2/3/5' , 'setName' => 'St-Mihiel' , 'setId' => 6)),
);
$root->buildTree($flatTree);
$this->assertCount(1, $root->getChildNodes());
$this->assertCount(1, $root->getChildNodes()->first()->getChildNodes());
$this->assertCount(2, $root->getChildNodes()->first()->getChildNodes()->first()->getChildNodes());
$this->assertEquals(1, $root->getNodeLevel());
$this->assertEquals(4, $root->getChildNodes()->first()->getChildNodes()->first()->getChildNodes()->first()->getNodeLevel());
}
public function testIsRoot()
{
$tree = $this->buildTree();
$this->assertTrue($tree->getRootNode()->isRootNode());
$this->assertTrue($tree->isRootNode());
}
public function testIsLeaf()
{
$tree = $this->buildTree();
$this->assertTrue($tree[0][0][0]->isLeafNode());
$this->assertTrue($tree[1]->isLeafNode());
}
public function testGetRoot()
{
$tree = $this->buildTree();
$this->assertEquals($tree, $tree->getRootNode());
$this->assertNull($tree->getRootNode()->getParentNode());
$this->assertEquals($tree, $tree->getChildNodes()->get(0)->getChildNodes()->get(0)->getRootNode());
}
public function provideRootPaths()
{
return array(
array($this->buildNode(array('setMaterializedPath' => '/0/1')) , '/0'),
array($this->buildNode(array('setMaterializedPath' => '/')) , '/'),
array($this->buildNode(array('setMaterializedPath' => '')) , '/'),
array($this->buildNode(array('setMaterializedPath' => '/test')) , '/test'),
array($this->buildNode(array('setMaterializedPath' => '/0/1/2/3/4/5/6/')) , '/0'),
);
}
/**
* @dataProvider provideisChildNodeOf
**/
public function testisChildNodeOf(NodeInterface $child, NodeInterface $parent, $expected)
{
$this->assertEquals($expected, $child->isChildNodeOf($parent));
}
public function provideisChildNodeOf()
{
$tree = $this->buildTree();
return array(
array($tree[0][0] , $tree[0] , true),
array($tree[0][0][0] , $tree[0][0] , true),
array($tree[0][0][0] , $tree[0] , false),
array($tree[0][0][0] , $tree[0][0][0] , false),
);
}
public function provideToArray()
{
$expected = array (
1 =>
array (
'node' => '',
'children' =>
array (
2 =>
array (
'node' => '',
'children' =>
array (
4 =>
array (
'node' => '',
'children' =>
array (
5 =>
array (
'node' => '',
'children' =>
array (
),
),
),
),
),
),
3 =>
array (
'node' => '',
'children' =>
array (
),
),
),
),
);
return $expected;
}
public function testToArray()
{
$expected = $this->provideToArray();
$tree = $this->buildTree();
$this->assertEquals($expected, $tree->toArray());
}
public function testToJson()
{
$expected = $this->provideToArray();
$tree = $this->buildTree();
$this->assertEquals(json_encode($expected), $tree->toJson());
}
public function testToFlatArray()
{
$tree = $this->buildTree();
$expected = array(
1 => '',
2 => '----',
4 => '------',
5 => '--------',
3 => '----',
);
$this->assertEquals($expected, $tree->toFlatArray());
}
public function testArrayAccess()
{
$tree = $this->buildTree();
$tree[] = $this->buildNode(array('setId' => 45));
$tree[] = $this->buildNode(array('setId' => 46));
$this->assertEquals(4, $tree->getChildNodes()->count());
$tree[2][] = $this->buildNode(array('setId' => 47));
$tree[2][] = $this->buildNode(array('setId' => 48));
$this->assertEquals(2, $tree[2]->getChildNodes()->count());
$this->assertTrue(isset($tree[2][1]));
$this->assertFalse(isset($tree[2][1][2]));
unset($tree[2][1]);
$this->assertFalse(isset($tree[2][1]));
}
/**
* @expectedException \LogicException
* @expectedExceptionMessage You must provide an id for this node if you want it to be part of a tree.
**/
public function testsetChildNodeOfWithoutId()
{
$this->buildNode(array('setMaterializedPath' => '/0/1'))->setChildNodeOf($this->buildNode(array('setMaterializedPath' => '/0')));
}
public function testChildrenCount()
{
$tree = $this->buildTree();
$this->assertEquals(2, $tree->getChildNodes()->count());
$this->assertEquals(1, $tree->getChildNodes()->get(0)->getChildNodes()->count());
}
public function testGetPath()
{
$tree = $this->buildTree();
$this->assertEquals('/1', $tree->getRealMaterializedPath());
$this->assertEquals('/1/2', $tree->getChildNodes()->get(0)->getRealMaterializedPath());
$this->assertEquals('/1/2/4', $tree->getChildNodes()->get(0)->getChildNodes()->get(0)->getRealMaterializedPath());
$this->assertEquals('/1/2/4/5', $tree->getChildNodes()->get(0)->getChildNodes()->get(0)->getChildNodes()->get(0)->getRealMaterializedPath());
$childChildItem = $tree->getChildNodes()->get(0)->getChildNodes()->get(0);
$childChildChildItem = $tree->getChildNodes()->get(0)->getChildNodes()->get(0)->getChildNodes()->get(0);
$childChildItem->setChildNodeOf($tree);
$this->assertEquals('/1/4', $childChildItem->getRealMaterializedPath(), 'The path has been updated fo the node');
$this->assertEquals('/1/4/5', $childChildChildItem->getRealMaterializedPath(), 'The path has been updated fo the node and all its descendants');
$this->assertTrue($tree->getChildNodes()->contains($childChildItem), 'The children collection has been updated to reference the moved node');
}
public function testMoveChildren()
{
$tree = $this->buildTree();
$childChildItem = $tree->getChildNodes()->get(0)->getChildNodes()->get(0);
$childChildChildItem = $tree->getChildNodes()->get(0)->getChildNodes()->get(0)->getChildNodes()->get(0);
$this->assertEquals(4, $childChildChildItem->getNodeLevel(), 'The level is well calcuated');
$childChildItem->setChildNodeOf($tree);
$this->assertEquals('/1/4', $childChildItem->getRealMaterializedPath(), 'The path has been updated fo the node');
$this->assertEquals('/1/4/5', $childChildChildItem->getRealMaterializedPath(), 'The path has been updated fo the node and all its descendants');
$this->assertTrue($tree->getChildNodes()->contains($childChildItem), 'The children collection has been updated to reference the moved node');
$this->assertEquals(3, $childChildChildItem->getNodeLevel(), 'The level has been updated');
}
public function testGetTree()
{
$em = $this->getEntityManager();
$repo = $em->getRepository('BehaviorFixtures\ORM\TreeNodeEntity');
$entity = new TreeNodeEntity(1);
$entity[0] = new TreeNodeEntity(2);
$entity[0][0] = new TreeNodeEntity(3);
$em->persist($entity);
$em->persist($entity[0]);
$em->persist($entity[0][0]);
$em->flush();
$root = $repo->getTree();
$this->assertEquals($root[0][0], $entity[0][0]);
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace Tests\Knp\DoctrineBehaviors\Reflection;
use Knp\DoctrineBehaviors\Reflection\ClassAnalyzer;
use BehaviorFixtures\ORM\DeletableEntity;
use BehaviorFixtures\ORM\DeletableEntityInherit;
use BehaviorFixtures\ORM\GeocodableEntity;
use BehaviorFixtures\ORM\TranslatableEntity;
class ClassAnalyserTest extends \PHPUnit_Framework_TestCase
{
/**
* @test
*/
public function it_should_test_if_object_use_trait () {
$analyser = new ClassAnalyzer;
$object = new DeletableEntity;
$use = $analyser->hasTrait(
new \ReflectionClass($object),
'Knp\DoctrineBehaviors\Model\SoftDeletable\SoftDeletable',
false
);
$this->assertTrue($use);
}
/**
* @test
*/
public function it_should_test_if_object_dont_use_trait () {
$analyser = new ClassAnalyzer;
$object = new DeletableEntity;
$use = $analyser->hasTrait(
new \ReflectionClass($object),
'Knp\DoctrineBehaviors\Model\Blameable\Blameable',
false
);
$this->assertFalse($use);
}
/**
* @test
*/
public function it_should_test_if_object_or_his_parent_classes_use_trait () {
$analyser = new ClassAnalyzer;
$object = new DeletableEntityInherit;
$use = $analyser->hasTrait(
new \ReflectionClass($object),
'Knp\DoctrineBehaviors\Model\SoftDeletable\SoftDeletable',
false
);
$this->assertFalse($use);
$useInherit = $analyser->hasTrait(
new \ReflectionClass($object),
'Knp\DoctrineBehaviors\Model\SoftDeletable\SoftDeletable',
true
);
$this->assertTrue($useInherit);
}
/**
* @test
*/
public function it_should_test_if_object_has_a_method () {
$analyser = new ClassAnalyzer;
$object = new GeocodableEntity;
$use = $analyser->hasMethod(
new \ReflectionClass($object),
'getLocation'
);
$this->assertTrue($use);
}
/**
* @test
*/
public function it_should_test_if_object_dont_has_a_method () {
$analyser = new ClassAnalyzer;
$object = new DeletableEntity;
$use = $analyser->hasMethod(
new \ReflectionClass($object),
'getLocation'
);
$this->assertFalse($use);
}
/**
* @test
*/
public function it_should_test_if_object_has_a_property () {
$analyser = new ClassAnalyzer;
$object = new TranslatableEntity;
$use = $analyser->hasProperty(
new \ReflectionClass($object),
'translations'
);
$this->assertTrue($use);
}
/**
* @test
*/
public function it_should_test_if_object_dont_has_a_property () {
$analyser = new ClassAnalyzer;
$object = new DeletableEntity;
$use = $analyser->hasProperty(
new \ReflectionClass($object),
'translations'
);
$this->assertFalse($use);
}
/**
* @test
*/
public function it_should_test_if_object_or_his_parent_classes_has_a_property () {
$analyser = new ClassAnalyzer;
$object = new DeletableEntityInherit;
$use = $analyser->hasProperty(
new \ReflectionClass($object),
'deletedAt'
);
$this->assertTrue($use);
}
}

View File

@@ -0,0 +1,41 @@
<?php
/**
* This is bootstrap for phpUnit unit tests,
* use README.md for more details
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @package Gedmo.Tests
* @link http://www.gediminasm.org
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
if (!class_exists('PHPUnit_Framework_TestCase') ||
version_compare(PHPUnit_Runner_Version::id(), '3.6') < 0
) {
die('PHPUnit framework is required, at least 3.6 version');
}
if (!class_exists('PHPUnit_Framework_MockObject_MockBuilder')) {
die('PHPUnit MockObject plugin is required, at least 1.0.8 version');
}
define("DB_ENGINE", getenv("DB") ?: "pgsql");
define('DB_HOST', getenv("DB_HOST") ?: 'localhost');
define('DB_NAME', getenv("DB_NAME") ?: 'orm_behaviors_test');
define("DB_USER", getenv("DB_USER") ?: null);
define("DB_PASSWD", getenv("DB_PASSWD") ?: null);
define('TESTS_PATH', __DIR__);
define('TESTS_TEMP_DIR', __DIR__.'/temp');
define('VENDOR_PATH', realpath(__DIR__ . '/../vendor'));
$loader = require(VENDOR_PATH.'/autoload.php');
$loader->add('BehaviorFixtures', __DIR__.'/fixtures');
Doctrine\Common\Annotations\AnnotationRegistry::registerFile(
VENDOR_PATH.'/doctrine/orm/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php'
);
$reader = new \Doctrine\Common\Annotations\AnnotationReader();
$reader = new \Doctrine\Common\Annotations\CachedReader($reader, new \Doctrine\Common\Cache\ArrayCache());
$_ENV['annotation_reader'] = $reader;

View File

@@ -0,0 +1,56 @@
<?php
namespace BehaviorFixtures\ORM;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model;
/**
* @ORM\Entity
*/
class BlameableEntity
{
use Model\Blameable\Blameable;
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(type="string", nullable=true)
*/
private $title;
/**
* Returns object id.
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Get title.
*
* @return title.
*/
public function getTitle()
{
return $this->title;
}
/**
* Set title.
*
* @param title the value to set.
*/
public function setTitle($title)
{
$this->title = $title;
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace BehaviorFixtures\ORM;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model;
/**
* @ORM\Entity
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorMap({
* "mainclass" = "BehaviorFixtures\ORM\DeletableEntity",
* "subclass" = "BehaviorFixtures\ORM\DeletableEntityInherit"
* })
*/
class DeletableEntity
{
use Model\SoftDeletable\SoftDeletable;
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* Returns object id.
*
* @return integer
*/
public function getId()
{
return $this->id;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace BehaviorFixtures\ORM;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class DeletableEntityInherit extends DeletableEntity
{
/**
* @ORM\Column(type="string")
*/
private $name;
/**
* Returns object name.
*
* @return string
*/
public function getName()
{
return $this->name;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace BehaviorFixtures\ORM;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="BehaviorFixtures\ORM\FilterableRepository")
*/
class FilterableEntity
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(type="string", nullable=true)
*/
private $name;
/**
* @ORM\Column(type="integer")
*/
private $code;
/**
* Returns object id.
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Get name.
*
* @return name.
*/
public function getName()
{
return $this->name;
}
/**
* Set name.
*
* @param name the value to set.
*/
public function setName($name)
{
$this->name = $name;
}
/**
* Get code.
*
* @return integer code.
*/
public function getCode()
{
return $this->code;
}
/**
* Set code.
*
* @param integer code the value to set.
*/
public function setCode($code)
{
$this->code = $code;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace BehaviorFixtures\ORM;
use Knp\DoctrineBehaviors\ORM\Filterable;
use Doctrine\ORM\EntityRepository;
/**
* @author Leszek Prabucki <leszek.prabucki@gmail.com>
*/
class FilterableRepository extends EntityRepository
{
use Filterable\FilterableRepository;
public function getILikeFilterColumns()
{
return [];
}
public function getLikeFilterColumns()
{
return ['e:name'];
}
public function getEqualFilterColumns()
{
return ['e:code'];
}
public function getInFilterColumns()
{
return [];
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace BehaviorFixtures\ORM;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model;
use Knp\DoctrineBehaviors\ORM\Geocodable\Type\Point;
/**
* @ORM\Entity(repositoryClass="BehaviorFixtures\ORM\GeocodableEntityRepository")
*/
class GeocodableEntity
{
use Model\Geocodable\Geocodable;
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(type="string", nullable=true)
*/
private $title;
public function __construct($latitude = 0, $longitude = 0)
{
$this->setLocation(new Point($latitude, $longitude));
}
/**
* Returns object id.
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Get title.
*
* @return title.
*/
public function getTitle()
{
return $this->title;
}
/**
* Set title.
*
* @param title the value to set.
*/
public function setTitle($title)
{
$this->title = $title;
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace BehaviorFixtures\ORM;
use Knp\DoctrineBehaviors\ORM\Geocodable;
use Doctrine\ORM\EntityRepository;
/**
* @author Florian Klein <florian.klein@free.fr>
*/
class GeocodableEntityRepository extends EntityRepository
{
use Geocodable\GeocodableRepository;
}

View File

@@ -0,0 +1,116 @@
<?php
namespace BehaviorFixtures\ORM;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model;
/**
* @ORM\Entity
*/
class LoggableEntity
{
use Model\Loggable\Loggable;
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(type="string", nullable=true)
*/
private $title;
/**
* @ORM\Column(type="array", nullable=true)
*/
private $roles;
/**
* @ORM\Column(type="date", nullable=true)
*/
private $date;
/**
* Returns object id.
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Get title.
*
* @return string.
*/
public function getTitle()
{
return $this->title;
}
/**
* Set title.
*
* @param $title the value to set.
*/
public function setTitle($title)
{
$this->title = $title;
}
/**
* Get Roles
*
* @return mixed
*/
public function getRoles()
{
return $this->roles;
}
/**
* Set roles
*
* @param array $roles
*
* @return $this;
*/
public function setRoles(array $roles = null)
{
$this->roles = $roles;
return $this;
}
/**
* Get date
*
* @return mixed
*/
public function getDate()
{
return $this->date;
}
/**
* Set date
*
* @param mixed $date
*
* @return $this;
*/
public function setDate($date)
{
$this->date = $date;
return $this;
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace BehaviorFixtures\ORM;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model;
/**
* @ORM\Entity
*/
class SluggableEntity
{
use Model\Sluggable\Sluggable;
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(type="string")
*/
protected $name;
/**
* @ORM\Column(type="datetime")
*/
protected $date;
public function __construct()
{
$this->date = (new \DateTime)->modify('-1 year');
}
/**
* Returns object id.
*
* @return integer
*/
public function getId()
{
return $this->id;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
return $this;
}
public function getDate()
{
return $this->date;
}
public function setDate($date)
{
$this->date = $date;
return $this;
}
protected function getSluggableFields()
{
return [ 'name' ];
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace BehaviorFixtures\ORM;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model;
/**
* @ORM\Entity
*/
class SluggableMultiEntity
{
use Model\Sluggable\Sluggable;
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(type="string")
*/
protected $name;
/**
* @ORM\Column(type="datetime")
*/
protected $date;
public function __construct()
{
$this->date = (new \DateTime)->modify('-1 year');
}
/**
* Returns object id.
*
* @return integer
*/
public function getId()
{
return $this->id;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
return $this;
}
public function getDate()
{
return $this->date;
}
public function setDate($date)
{
$this->date = $date;
return $this;
}
public function getSluggableFields()
{
return [ 'name', 'title' ];
}
public function getTitle()
{
return 'title';
}
/**
* @param $values
* @return mixed|string
*/
public function generateSlugValue($values)
{
$sluggableText = implode(' ', $values);
return strtolower(str_replace(' ', '+', $sluggableText));
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace BehaviorFixtures\ORM;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model;
/**
* @ORM\Entity
*/
class TimestampableEntity
{
use Model\Timestampable\Timestampable;
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(type="string", nullable=true)
*/
private $title;
/**
* Returns object id.
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Get title.
*
* @return title.
*/
public function getTitle()
{
return $this->title;
}
/**
* Set title.
*
* @param title the value to set.
*/
public function setTitle($title)
{
$this->title = $title;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace BehaviorFixtures\ORM;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model;
/**
* @ORM\Entity
* Used to test translation classes which declare custom translatable classes.
*/
class TranslatableCustomizedEntity
{
use Model\Translatable\Translatable;
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* {@inheritdoc}
*/
public static function getTranslationEntityClass()
{
return '\BehaviorFixtures\ORM\Translation\TranslatableCustomizedEntityTranslation';
}
public function __call($method, $arguments)
{
return $this->proxyCurrentLocaleTranslation($method, $arguments);
}
/**
* Returns object id.
*
* @return integer
*/
public function getId()
{
return $this->id;
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace BehaviorFixtures\ORM;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model;
/**
* @ORM\Entity
*/
class TranslatableEntity
{
use Model\Translatable\Translatable;
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
public function __call($method, $arguments)
{
return $this->proxyCurrentLocaleTranslation($method, $arguments);
}
/**
* Returns object id.
*
* @return integer
*/
public function getId()
{
return $this->id;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace BehaviorFixtures\ORM;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model;
/**
* @ORM\Entity
*/
class TranslatableEntityTranslation
{
use Model\Translatable\Translation;
/**
* @ORM\Column(type="string")
*/
private $title;
public function getTitle()
{
return $this->title;
}
public function setTitle($title)
{
$this->title = $title;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace BehaviorFixtures\ORM\Translation;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model;
/**
* @ORM\Entity
* Used to test translatable classes which declare a custom translation class.
*/
class TranslatableCustomizedEntityTranslation
{
use Model\Translatable\Translation;
/**
* @ORM\Column(type="string")
*/
private $title;
/**
* {@inheritdoc}
*/
public static function getTranslatableEntityClass()
{
return '\BehaviorFixtures\ORM\TranslatableCustomizedEntity';
}
public function getTitle()
{
return $this->title;
}
public function setTitle($title)
{
$this->title = $title;
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace BehaviorFixtures\ORM;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Knp\DoctrineBehaviors\Model\Tree;
/**
* @ORM\Entity(repositoryClass="BehaviorFixtures\ORM\TreeNodeEntityRepository")
*/
class TreeNodeEntity implements Tree\NodeInterface, \ArrayAccess
{
const PATH_SEPARATOR = '/';
use Tree\Node;
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="NONE")
*/
protected $id;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
protected $name;
public function __construct($id = null)
{
$this->children = new ArrayCollection;
$this->id = $id;
}
public function __toString()
{
return (string) $this->name;
}
/**
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* @param string
* @return null
*/
public function setId($id)
{
$this->id = $id;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @param string
* @return null
*/
public function setName($name)
{
$this->name = $name;
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace BehaviorFixtures\ORM;
use Knp\DoctrineBehaviors\ORM\Tree;
use Doctrine\ORM\EntityRepository;
/**
* @author Florian Klein <florian.klein@free.fr>
*/
class TreeNodeEntityRepository extends EntityRepository
{
use Tree\Tree;
}

View File

@@ -0,0 +1,33 @@
<?php
namespace BehaviorFixtures\ORM;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class UserEntity
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(type="string")
*/
private $username;
public function getUsername()
{
return $this->username;
}
public function setUsername($username)
{
$this->username = $username;
}
}