Actualización

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

View File

@@ -0,0 +1,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);
}
}