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,132 @@
<?php
namespace Gedmo\Sluggable\Handler;
use Doctrine\Common\Persistence\ObjectManager;
use Gedmo\Sluggable\SluggableListener;
use Gedmo\Sluggable\Mapping\Event\SluggableAdapter;
use Gedmo\Tool\Wrapper\AbstractWrapper;
use Gedmo\Exception\InvalidMappingException;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
/**
* Sluggable handler which should be used for inversed relation mapping
* used together with RelativeSlugHandler. Updates back related slug on
* relation changes
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class InversedRelativeSlugHandler implements SlugHandlerInterface
{
/**
* @var ObjectManager
*/
protected $om;
/**
* @var SluggableListener
*/
protected $sluggable;
/**
* $options = array(
* 'relationClass' => 'objectclass',
* 'inverseSlugField' => 'slug',
* 'mappedBy' => 'relationField'
* )
* {@inheritDoc}
*/
public function __construct(SluggableListener $sluggable)
{
$this->sluggable = $sluggable;
}
/**
* {@inheritDoc}
*/
public function onChangeDecision(SluggableAdapter $ea, array &$config, $object, &$slug, &$needToChangeSlug)
{
}
/**
* {@inheritDoc}
*/
public function postSlugBuild(SluggableAdapter $ea, array &$config, $object, &$slug)
{
}
/**
* {@inheritDoc}
*/
public static function validate(array $options, ClassMetadata $meta)
{
if (!isset($options['relationClass']) || !strlen($options['relationClass'])) {
throw new InvalidMappingException("'relationClass' option must be specified for object slug mapping - {$meta->name}");
}
if (!isset($options['mappedBy']) || !strlen($options['mappedBy'])) {
throw new InvalidMappingException("'mappedBy' option must be specified for object slug mapping - {$meta->name}");
}
if (!isset($options['inverseSlugField']) || !strlen($options['inverseSlugField'])) {
throw new InvalidMappingException("'inverseSlugField' option must be specified for object slug mapping - {$meta->name}");
}
}
/**
* {@inheritDoc}
*/
public function onSlugCompletion(SluggableAdapter $ea, array &$config, $object, &$slug)
{
$this->om = $ea->getObjectManager();
$isInsert = $this->om->getUnitOfWork()->isScheduledForInsert($object);
if (!$isInsert) {
$options = $config['handlers'][get_called_class()];
$wrapped = AbstractWrapper::wrap($object, $this->om);
$oldSlug = $wrapped->getPropertyValue($config['slug']);
$mappedByConfig = $this->sluggable->getConfiguration(
$this->om,
$options['relationClass']
);
if ($mappedByConfig) {
$meta = $this->om->getClassMetadata($options['relationClass']);
if (!$meta->isSingleValuedAssociation($options['mappedBy'])) {
throw new InvalidMappingException("Unable to find ".$wrapped->getMetadata()->name." relation - [{$options['mappedBy']}] in class - {$meta->name}");
}
if (!isset($mappedByConfig['slugs'][$options['inverseSlugField']])) {
throw new InvalidMappingException("Unable to find slug field - [{$options['inverseSlugField']}] in class - {$meta->name}");
}
$mappedByConfig['slug'] = $mappedByConfig['slugs'][$options['inverseSlugField']]['slug'];
$mappedByConfig['mappedBy'] = $options['mappedBy'];
$ea->replaceInverseRelative($object, $mappedByConfig, $slug, $oldSlug);
$uow = $this->om->getUnitOfWork();
// update in memory objects
foreach ($uow->getIdentityMap() as $className => $objects) {
// for inheritance mapped classes, only root is always in the identity map
if ($className !== $mappedByConfig['useObjectClass']) {
continue;
}
foreach ($objects as $object) {
if (property_exists($object, '__isInitialized__') && !$object->__isInitialized__) {
continue;
}
$oid = spl_object_hash($object);
$objectSlug = $meta->getReflectionProperty($mappedByConfig['slug'])->getValue($object);
if (preg_match("@^{$oldSlug}@smi", $objectSlug)) {
$objectSlug = str_replace($oldSlug, $slug, $objectSlug);
$meta->getReflectionProperty($mappedByConfig['slug'])->setValue($object, $objectSlug);
$ea->setOriginalObjectProperty($uow, $oid, $mappedByConfig['slug'], $objectSlug);
}
}
}
}
}
}
/**
* {@inheritDoc}
*/
public function handlesUrlization()
{
return false;
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace Gedmo\Sluggable\Handler;
use Doctrine\Common\Persistence\ObjectManager;
use Gedmo\Sluggable\SluggableListener;
use Gedmo\Sluggable\Mapping\Event\SluggableAdapter;
use Gedmo\Tool\Wrapper\AbstractWrapper;
use Gedmo\Exception\InvalidMappingException;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
/**
* Sluggable handler which should be used in order to prefix
* a slug of related object. For instance user may belong to a company
* in this case user slug could look like 'company-name/user-firstname'
* where path separator separates the relative slug
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class RelativeSlugHandler implements SlugHandlerInterface
{
const SEPARATOR = '/';
/**
* @var ObjectManager
*/
protected $om;
/**
* @var SluggableListener
*/
protected $sluggable;
/**
* Used options
*
* @var array
*/
private $usedOptions;
/**
* Callable of original transliterator
* which is used by sluggable
*
* @var callable
*/
private $originalTransliterator;
/**
* $options = array(
* 'separator' => '/',
* 'relationField' => 'something',
* 'relationSlugField' => 'slug'
* )
* {@inheritDoc}
*/
public function __construct(SluggableListener $sluggable)
{
$this->sluggable = $sluggable;
}
/**
* {@inheritDoc}
*/
public function onChangeDecision(SluggableAdapter $ea, array &$config, $object, &$slug, &$needToChangeSlug)
{
$this->om = $ea->getObjectManager();
$isInsert = $this->om->getUnitOfWork()->isScheduledForInsert($object);
$this->usedOptions = $config['handlers'][get_called_class()];
if (!isset($this->usedOptions['separator'])) {
$this->usedOptions['separator'] = self::SEPARATOR;
}
if (!$isInsert && !$needToChangeSlug) {
$changeSet = $ea->getObjectChangeSet($this->om->getUnitOfWork(), $object);
if (isset($changeSet[$this->usedOptions['relationField']])) {
$needToChangeSlug = true;
}
}
}
/**
* {@inheritDoc}
*/
public function postSlugBuild(SluggableAdapter $ea, array &$config, $object, &$slug)
{
$this->originalTransliterator = $this->sluggable->getTransliterator();
$this->sluggable->setTransliterator(array($this, 'transliterate'));
}
/**
* {@inheritDoc}
*/
public static function validate(array $options, ClassMetadata $meta)
{
if (!$meta->isSingleValuedAssociation($options['relationField'])) {
throw new InvalidMappingException("Unable to find slug relation through field - [{$options['relationField']}] in class - {$meta->name}");
}
}
/**
* {@inheritDoc}
*/
public function onSlugCompletion(SluggableAdapter $ea, array &$config, $object, &$slug)
{
}
/**
* Transliterates the slug and prefixes the slug
* by relative one
*
* @param string $text
* @param string $separator
* @param object $object
*
* @return string
*/
public function transliterate($text, $separator, $object)
{
$result = call_user_func_array(
$this->originalTransliterator,
array($text, $separator, $object)
);
$wrapped = AbstractWrapper::wrap($object, $this->om);
$relation = $wrapped->getPropertyValue($this->usedOptions['relationField']);
if ($relation) {
$wrappedRelation = AbstractWrapper::wrap($relation, $this->om);
$slug = $wrappedRelation->getPropertyValue($this->usedOptions['relationSlugField']);
if (isset($this->usedOptions['urilize']) && $this->usedOptions['urilize']) {
$slug = call_user_func_array(
$this->originalTransliterator,
array($slug, $separator, $object)
);
}
$result = $slug.$this->usedOptions['separator'].$result;
}
$this->sluggable->setTransliterator($this->originalTransliterator);
return $result;
}
/**
* {@inheritDoc}
*/
public function handlesUrlization()
{
return true;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Gedmo\Sluggable\Handler;
use Gedmo\Sluggable\SluggableListener;
use Gedmo\Sluggable\Mapping\Event\SluggableAdapter;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
/**
* Sluggable handler interface is a common pattern for all
* slug handlers which can be attached to the sluggable listener.
* Usage is intended only for internal access of sluggable.
* Should not be used outside of sluggable extension
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
interface SlugHandlerInterface
{
/**
* Construct the slug handler
*
* @param SluggableListener $sluggable
*/
public function __construct(SluggableListener $sluggable);
/**
* Callback on slug handlers before the decision
* is made whether or not the slug needs to be
* recalculated
*
* @param SluggableAdapter $ea
* @param array $config
* @param object $object
* @param string $slug
* @param boolean $needToChangeSlug
*
* @return void
*/
public function onChangeDecision(SluggableAdapter $ea, array &$config, $object, &$slug, &$needToChangeSlug);
/**
* Callback on slug handlers right after the slug is built
*
* @param SluggableAdapter $ea
* @param array $config
* @param object $object
* @param string $slug
*
* @return void
*/
public function postSlugBuild(SluggableAdapter $ea, array &$config, $object, &$slug);
/**
* Callback for slug handlers on slug completion
*
* @param SluggableAdapter $ea
* @param array $config
* @param object $object
* @param string $slug
*
* @return void
*/
public function onSlugCompletion(SluggableAdapter $ea, array &$config, $object, &$slug);
/**
* @return boolean whether or not this handler has already urlized the slug
*/
public function handlesUrlization();
/**
* Validate handler options
*
* @param array $options
* @param ClassMetadata $meta
*/
public static function validate(array $options, ClassMetadata $meta);
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Gedmo\Sluggable\Handler;
use Gedmo\Sluggable\Mapping\Event\SluggableAdapter;
/**
* This adds the ability to a SlugHandler to change the slug just before its
* uniqueness is ensured. It is also called if the unique options is _not_
* set.
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
interface SlugHandlerWithUniqueCallbackInterface extends SlugHandlerInterface
{
/**
* Callback for slug handlers before it is made unique
*
* @param SluggableAdapter $ea
* @param array $config
* @param object $object
* @param string $slug
*
* @return void
*/
public function beforeMakingUnique(SluggableAdapter $ea, array &$config, $object, &$slug);
}

View File

@@ -0,0 +1,201 @@
<?php
namespace Gedmo\Sluggable\Handler;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Gedmo\Sluggable\SluggableListener;
use Gedmo\Sluggable\Mapping\Event\SluggableAdapter;
use Gedmo\Tool\Wrapper\AbstractWrapper;
use Gedmo\Exception\InvalidMappingException;
/**
* Sluggable handler which slugs all parent nodes
* recursively and synchronizes on updates. For instance
* category tree slug could look like "food/fruits/apples"
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class TreeSlugHandler implements SlugHandlerWithUniqueCallbackInterface
{
const SEPARATOR = '/';
/**
* @var ObjectManager
*/
protected $om;
/**
* @var SluggableListener
*/
protected $sluggable;
/**
* @var string
*/
private $prefix;
/**
* @var string
*/
private $suffix;
/**
* True if node is being inserted
*
* @var boolean
*/
private $isInsert = false;
/**
* Transliterated parent slug
*
* @var string
*/
private $parentSlug;
/**
* Used path separator
*
* @var string
*/
private $usedPathSeparator;
/**
* {@inheritDoc}
*/
public function __construct(SluggableListener $sluggable)
{
$this->sluggable = $sluggable;
}
/**
* {@inheritDoc}
*/
public function onChangeDecision(SluggableAdapter $ea, array &$config, $object, &$slug, &$needToChangeSlug)
{
$this->om = $ea->getObjectManager();
$this->isInsert = $this->om->getUnitOfWork()->isScheduledForInsert($object);
$options = $config['handlers'][get_called_class()];
$this->usedPathSeparator = isset($options['separator']) ? $options['separator'] : self::SEPARATOR;
$this->prefix = isset($options['prefix']) ? $options['prefix'] : '';
$this->suffix = isset($options['suffix']) ? $options['suffix'] : '';
if (!$this->isInsert && !$needToChangeSlug) {
$changeSet = $ea->getObjectChangeSet($this->om->getUnitOfWork(), $object);
if (isset($changeSet[$options['parentRelationField']])) {
$needToChangeSlug = true;
}
}
}
/**
* {@inheritDoc}
*/
public function postSlugBuild(SluggableAdapter $ea, array &$config, $object, &$slug)
{
$options = $config['handlers'][get_called_class()];
$this->parentSlug = '';
$wrapped = AbstractWrapper::wrap($object, $this->om);
if ($parent = $wrapped->getPropertyValue($options['parentRelationField'])) {
$parent = AbstractWrapper::wrap($parent, $this->om);
$this->parentSlug = $parent->getPropertyValue($config['slug']);
// if needed, remove suffix from parentSlug, so we can use it to prepend it to our slug
if (isset($options['suffix'])) {
$suffix = $options['suffix'];
if (substr($this->parentSlug, -strlen($suffix)) === $suffix) { //endsWith
$this->parentSlug = substr_replace($this->parentSlug, '', -1 * strlen($suffix));
}
}
}
}
/**
* {@inheritDoc}
*/
public static function validate(array $options, ClassMetadata $meta)
{
if (!$meta->isSingleValuedAssociation($options['parentRelationField'])) {
throw new InvalidMappingException("Unable to find tree parent slug relation through field - [{$options['parentRelationField']}] in class - {$meta->name}");
}
}
/**
* {@inheritDoc}
*/
public function beforeMakingUnique(SluggableAdapter $ea, array &$config, $object, &$slug)
{
$slug = $this->transliterate($slug, $config['separator'], $object);
}
/**
* {@inheritDoc}
*/
public function onSlugCompletion(SluggableAdapter $ea, array &$config, $object, &$slug)
{
if (!$this->isInsert) {
$wrapped = AbstractWrapper::wrap($object, $this->om);
$meta = $wrapped->getMetadata();
$target = $wrapped->getPropertyValue($config['slug']);
$config['pathSeparator'] = $this->usedPathSeparator;
$ea->replaceRelative($object, $config, $target.$config['pathSeparator'], $slug);
$uow = $this->om->getUnitOfWork();
// update in memory objects
foreach ($uow->getIdentityMap() as $className => $objects) {
// for inheritance mapped classes, only root is always in the identity map
if ($className !== $wrapped->getRootObjectName()) {
continue;
}
foreach ($objects as $object) {
if (property_exists($object, '__isInitialized__') && !$object->__isInitialized__) {
continue;
}
$oid = spl_object_hash($object);
$objectSlug = $meta->getReflectionProperty($config['slug'])->getValue($object);
if (preg_match("@^{$target}{$config['pathSeparator']}@smi", $objectSlug)) {
$objectSlug = str_replace($target, $slug, $objectSlug);
$meta->getReflectionProperty($config['slug'])->setValue($object, $objectSlug);
$ea->setOriginalObjectProperty($uow, $oid, $config['slug'], $objectSlug);
}
}
}
}
}
/**
* Transliterates the slug and prefixes the slug
* by collection of parent slugs
*
* @param string $text
* @param string $separator
* @param object $object
*
* @return string
*/
public function transliterate($text, $separator, $object)
{
$slug = $text . $this->suffix;
if (strlen($this->parentSlug)) {
$slug = $this->parentSlug.$this->usedPathSeparator.$slug;
} else {
// if no parentSlug, apply our prefix
$slug = $this->prefix.$slug;
}
return $slug;
}
/**
* {@inheritDoc}
*/
public function handlesUrlization()
{
return false;
}
}

View File

@@ -0,0 +1,173 @@
<?php
namespace Gedmo\Sluggable\Mapping\Driver;
use Gedmo\Mapping\Annotation\SlugHandler;
use Gedmo\Mapping\Annotation\SlugHandlerOption;
use Gedmo\Mapping\Driver\AbstractAnnotationDriver;
use Gedmo\Exception\InvalidMappingException;
/**
* This is an annotation mapping driver for Sluggable
* behavioral extension. Used for extraction of extended
* metadata from Annotations specifically for Sluggable
* extension.
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class Annotation extends AbstractAnnotationDriver
{
/**
* Annotation to identify field as one which holds the slug
* together with slug options
*/
const SLUG = 'Gedmo\\Mapping\\Annotation\\Slug';
/**
* SlugHandler extension annotation
*/
const HANDLER = 'Gedmo\\Mapping\\Annotation\\SlugHandler';
/**
* SlugHandler option annotation
*/
const HANDLER_OPTION = 'Gedmo\\Mapping\\Annotation\\SlugHandlerOption';
/**
* List of types which are valid for slug and sluggable fields
*
* @var array
*/
protected $validTypes = array(
'string',
'text',
'integer',
'int',
'datetime',
'citext',
);
/**
* {@inheritDoc}
*/
public function readExtendedMetadata($meta, array &$config)
{
$class = $this->getMetaReflectionClass($meta);
// property annotations
foreach ($class->getProperties() as $property) {
if ($meta->isMappedSuperclass && !$property->isPrivate() ||
$meta->isInheritedField($property->name) ||
isset($meta->associationMappings[$property->name]['inherited'])
) {
continue;
}
$config = $this->retrieveSlug($meta, $config, $property, '');
}
// Embedded entity
if (property_exists($meta, 'embeddedClasses') && $meta->embeddedClasses) {
foreach ($meta->embeddedClasses as $propertyName => $embeddedClassInfo) {
$embeddedClass = new \ReflectionClass($embeddedClassInfo['class']);
foreach ($embeddedClass->getProperties() as $embeddedProperty) {
$config = $this->retrieveSlug($meta, $config, $embeddedProperty, $propertyName);
}
}
}
return $config;
}
/**
* @param $meta
* @param array $config
* @param $property
* @param $fieldNamePrefix
* @return array
*/
private function retrieveSlug($meta, array &$config, $property, $fieldNamePrefix)
{
$fieldName = $fieldNamePrefix ? ($fieldNamePrefix . '.' . $property->getName()) : $property->getName();
// slug property
if ($slug = $this->reader->getPropertyAnnotation($property, self::SLUG)) {
if (!$meta->hasField($fieldName)) {
throw new InvalidMappingException("Unable to find slug [{$fieldName}] as mapped property in entity - {$meta->name}");
}
if (!$this->isValidField($meta, $fieldName)) {
throw new InvalidMappingException("Cannot use field - [{$fieldName}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->name}");
}
// process slug handlers
$handlers = array();
if (is_array($slug->handlers) && $slug->handlers) {
foreach ($slug->handlers as $handler) {
if (!$handler instanceof SlugHandler) {
throw new InvalidMappingException("SlugHandler: {$handler} should be instance of SlugHandler annotation in entity - {$meta->name}");
}
if (!strlen($handler->class)) {
throw new InvalidMappingException("SlugHandler class: {$handler->class} should be a valid class name in entity - {$meta->name}");
}
$class = $handler->class;
$handlers[$class] = array();
foreach ((array)$handler->options as $option) {
if (!$option instanceof SlugHandlerOption) {
throw new InvalidMappingException("SlugHandlerOption: {$option} should be instance of SlugHandlerOption annotation in entity - {$meta->name}");
}
if (!strlen($option->name)) {
throw new InvalidMappingException("SlugHandlerOption name: {$option->name} should be valid name in entity - {$meta->name}");
}
$handlers[$class][$option->name] = $option->value;
}
$class::validate($handlers[$class], $meta);
}
}
// process slug fields
if (empty($slug->fields) || !is_array($slug->fields)) {
throw new InvalidMappingException("Slug must contain at least one field for slug generation in class - {$meta->name}");
}
foreach ($slug->fields as $slugField) {
$slugFieldWithPrefix = $fieldNamePrefix ? ($fieldNamePrefix . '.' . $slugField) : $slugField;
if (!$meta->hasField($slugFieldWithPrefix)) {
throw new InvalidMappingException("Unable to find slug [{$slugFieldWithPrefix}] as mapped property in entity - {$meta->name}");
}
if (!$this->isValidField($meta, $slugFieldWithPrefix)) {
throw new InvalidMappingException("Cannot use field - [{$slugFieldWithPrefix}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->name}");
}
}
if (!is_bool($slug->updatable)) {
throw new InvalidMappingException("Slug annotation [updatable], type is not valid and must be 'boolean' in class - {$meta->name}");
}
if (!is_bool($slug->unique)) {
throw new InvalidMappingException("Slug annotation [unique], type is not valid and must be 'boolean' in class - {$meta->name}");
}
if (!empty($meta->identifier) && $meta->isIdentifier($fieldName) && !(bool)$slug->unique) {
throw new InvalidMappingException("Identifier field - [{$fieldName}] slug must be unique in order to maintain primary key in class - {$meta->name}");
}
if ($slug->unique === false && $slug->unique_base) {
throw new InvalidMappingException("Slug annotation [unique_base] can not be set if unique is unset or 'false'");
}
if ($slug->unique_base && !$meta->hasField($slug->unique_base) && !$meta->hasAssociation($slug->unique_base)) {
throw new InvalidMappingException("Unable to find [{$slug->unique_base}] as mapped property in entity - {$meta->name}");
}
$sluggableFields = array();
foreach ($slug->fields as $field) {
$sluggableFields[] = $fieldNamePrefix ? ($fieldNamePrefix . '.' . $field) : $field;
}
// set all options
$config['slugs'][$fieldName] = array(
'fields' => $sluggableFields,
'slug' => $fieldName,
'style' => $slug->style,
'dateFormat' => $slug->dateFormat,
'updatable' => $slug->updatable,
'unique' => $slug->unique,
'unique_base' => $slug->unique_base,
'separator' => $slug->separator,
'prefix' => $slug->prefix,
'suffix' => $slug->suffix,
'handlers' => $handlers,
);
}
return $config;
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace Gedmo\Sluggable\Mapping\Driver;
use Gedmo\Mapping\Driver\Xml as BaseXml;
use Gedmo\Exception\InvalidMappingException;
/**
* This is a xml mapping driver for Sluggable
* behavioral extension. Used for extraction of extended
* metadata from xml specifically for Sluggable
* extension.
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @author Miha Vrhovnik <miha.vrhovnik@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class Xml extends BaseXml
{
/**
* List of types which are valid for slug and sluggable fields
*
* @var array
*/
private $validTypes = array(
'string',
'text',
'integer',
'int',
'datetime',
'citext',
);
/**
* {@inheritDoc}
*/
public function readExtendedMetadata($meta, array &$config)
{
/**
* @var \SimpleXmlElement $xml
*/
$xml = $this->_getMapping($meta->name);
if (isset($xml->field)) {
foreach ($xml->field as $mapping) {
$field = $this->_getAttribute($mapping, 'name');
$this->buildFieldConfiguration($meta, $field, $mapping, $config);
}
}
if (isset($xml->{'attribute-overrides'})) {
foreach ($xml->{'attribute-overrides'}->{'attribute-override'} as $mapping) {
$field = $this->_getAttribute($mapping, 'name');
$this->buildFieldConfiguration($meta, $field, $mapping->field, $config);
}
}
}
private function buildFieldConfiguration($meta, $field, \SimpleXMLElement $mapping, array &$config)
{
/**
* @var \SimpleXmlElement $mapping
*/
$mapping = $mapping->children(self::GEDMO_NAMESPACE_URI);
if (isset($mapping->slug)) {
/**
* @var \SimpleXmlElement $slug
*/
$slug = $mapping->slug;
if (!$this->isValidField($meta, $field)) {
throw new InvalidMappingException("Cannot use field - [{$field}] for slug storage, type is not valid and must be 'string' in class - {$meta->name}");
}
$fields = array_map('trim', explode(',', (string) $this->_getAttribute($slug, 'fields')));
foreach ($fields as $slugField) {
if (!$meta->hasField($slugField)) {
throw new InvalidMappingException("Unable to find slug [{$slugField}] as mapped property in entity - {$meta->name}");
}
if (!$this->isValidField($meta, $slugField)) {
throw new InvalidMappingException("Cannot use field - [{$slugField}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->name}");
}
}
$handlers = array();
if (isset($slug->handler)) {
foreach ($slug->handler as $handler) {
$class = (string) $this->_getAttribute($handler, 'class');
$handlers[$class] = array();
foreach ($handler->{'handler-option'} as $option) {
$handlers[$class][(string) $this->_getAttribute($option, 'name')]
= (string) $this->_getAttribute($option, 'value')
;
}
$class::validate($handlers[$class], $meta);
}
}
// set all options
$config['slugs'][$field] = array(
'fields' => $fields,
'slug' => $field,
'style' => $this->_isAttributeSet($slug, 'style') ?
$this->_getAttribute($slug, 'style') : 'default',
'updatable' => $this->_isAttributeSet($slug, 'updatable') ?
$this->_getBooleanAttribute($slug, 'updatable') : true,
'dateFormat' => $this->_isAttributeSet($slug, 'dateFormat') ?
$this->_getAttribute($slug, 'dateFormat') : 'Y-m-d-H:i',
'unique' => $this->_isAttributeSet($slug, 'unique') ?
$this->_getBooleanAttribute($slug, 'unique') : true,
'unique_base' => $this->_isAttributeSet($slug, 'unique-base') ?
$this->_getAttribute($slug, 'unique-base') : null,
'separator' => $this->_isAttributeSet($slug, 'separator') ?
$this->_getAttribute($slug, 'separator') : '-',
'prefix' => $this->_isAttributeSet($slug, 'prefix') ?
$this->_getAttribute($slug, 'prefix') : '',
'suffix' => $this->_isAttributeSet($slug, 'suffix') ?
$this->_getAttribute($slug, 'suffix') : '',
'handlers' => $handlers,
);
if (!$meta->isMappedSuperclass && $meta->isIdentifier($field) && !$config['slugs'][$field]['unique']) {
throw new InvalidMappingException("Identifier field - [{$field}] slug must be unique in order to maintain primary key in class - {$meta->name}");
}
$ubase = $config['slugs'][$field]['unique_base'];
if ($config['slugs'][$field]['unique'] === false && $ubase) {
throw new InvalidMappingException("Slug annotation [unique_base] can not be set if unique is unset or 'false'");
}
if ($ubase && !$meta->hasField($ubase) && !$meta->hasAssociation($ubase)) {
throw new InvalidMappingException("Unable to find [{$ubase}] as mapped property in entity - {$meta->name}");
}
}
}
/**
* Checks if $field type is valid as Sluggable field
*
* @param object $meta
* @param string $field
*
* @return boolean
*/
protected function isValidField($meta, $field)
{
$mapping = $meta->getFieldMapping($field);
return $mapping && in_array($mapping['type'], $this->validTypes);
}
}

View File

@@ -0,0 +1,155 @@
<?php
namespace Gedmo\Sluggable\Mapping\Driver;
use Gedmo\Mapping\Driver\File;
use Gedmo\Mapping\Driver;
use Gedmo\Exception\InvalidMappingException;
/**
* This is a yaml mapping driver for Sluggable
* behavioral extension. Used for extraction of extended
* metadata from yaml specifically for Sluggable
* extension.
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class Yaml extends File implements Driver
{
/**
* File extension
* @var string
*/
protected $_extension = '.dcm.yml';
/**
* List of types which are valid for slug and sluggable fields
*
* @var array
*/
private $validTypes = array(
'string',
'text',
'integer',
'int',
'datetime',
'citext',
);
/**
* {@inheritDoc}
*/
public function readExtendedMetadata($meta, array &$config)
{
$mapping = $this->_getMapping($meta->name);
if (isset($mapping['fields'])) {
foreach ($mapping['fields'] as $field => $fieldMapping) {
$this->buildFieldConfiguration($field, $fieldMapping, $meta, $config);
}
}
if (isset($mapping['attributeOverride'])) {
foreach ($mapping['attributeOverride'] as $field => $overrideMapping) {
$this->buildFieldConfiguration($field, $overrideMapping, $meta, $config);
}
}
}
/**
* {@inheritDoc}
*/
protected function _loadMappingFile($file)
{
return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file));
}
/**
* Checks if $field type is valid as Sluggable field
*
* @param object $meta
* @param string $field
*
* @return boolean
*/
protected function isValidField($meta, $field)
{
$mapping = $meta->getFieldMapping($field);
return $mapping && in_array($mapping['type'], $this->validTypes);
}
private function buildFieldConfiguration($field, array $fieldMapping, $meta, array &$config)
{
if (isset($fieldMapping['gedmo'])) {
if (isset($fieldMapping['gedmo']['slug'])) {
$slug = $fieldMapping['gedmo']['slug'];
if (!$this->isValidField($meta, $field)) {
throw new InvalidMappingException("Cannot use field - [{$field}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->name}");
}
// process slug handlers
$handlers = array();
if (isset($slug['handlers'])) {
foreach ($slug['handlers'] as $handlerClass => $options) {
if (!strlen($handlerClass)) {
throw new InvalidMappingException("SlugHandler class: {$handlerClass} should be a valid class name in entity - {$meta->name}");
}
$handlers[$handlerClass] = $options;
$handlerClass::validate($handlers[$handlerClass], $meta);
}
}
// process slug fields
if (empty($slug['fields']) || !is_array($slug['fields'])) {
throw new InvalidMappingException("Slug must contain at least one field for slug generation in class - {$meta->name}");
}
foreach ($slug['fields'] as $slugField) {
if (!$meta->hasField($slugField)) {
throw new InvalidMappingException("Unable to find slug [{$slugField}] as mapped property in entity - {$meta->name}");
}
if (!$this->isValidField($meta, $slugField)) {
throw new InvalidMappingException("Cannot use field - [{$slugField}] for slug storage, type is not valid and must be 'string' or 'text' in class - {$meta->name}");
}
}
$config['slugs'][$field]['fields'] = $slug['fields'];
$config['slugs'][$field]['handlers'] = $handlers;
$config['slugs'][$field]['slug'] = $field;
$config['slugs'][$field]['style'] = isset($slug['style']) ?
(string) $slug['style'] : 'default';
$config['slugs'][$field]['dateFormat'] = isset($slug['dateFormat']) ?
(string) $slug['dateFormat'] : 'Y-m-d-H:i';
$config['slugs'][$field]['updatable'] = isset($slug['updatable']) ?
(bool) $slug['updatable'] : true;
$config['slugs'][$field]['unique'] = isset($slug['unique']) ?
(bool) $slug['unique'] : true;
$config['slugs'][$field]['unique_base'] = isset($slug['unique_base']) ?
$slug['unique_base'] : null;
$config['slugs'][$field]['separator'] = isset($slug['separator']) ?
(string) $slug['separator'] : '-';
$config['slugs'][$field]['prefix'] = isset($slug['prefix']) ?
(string) $slug['prefix'] : '';
$config['slugs'][$field]['suffix'] = isset($slug['suffix']) ?
(string) $slug['suffix'] : '';
if (!$meta->isMappedSuperclass && $meta->isIdentifier($field) && !$config['slugs'][$field]['unique']) {
throw new InvalidMappingException("Identifier field - [{$field}] slug must be unique in order to maintain primary key in class - {$meta->name}");
}
$ubase = $config['slugs'][$field]['unique_base'];
if ($config['slugs'][$field]['unique'] === false && $ubase) {
throw new InvalidMappingException("Slug annotation [unique_base] can not be set if unique is unset or 'false'");
}
if ($ubase && !$meta->hasField($ubase) && !$meta->hasAssociation($ubase)) {
throw new InvalidMappingException("Unable to find [{$ubase}] as mapped property in entity - {$meta->name}");
}
}
}
}
}

View File

@@ -0,0 +1,123 @@
<?php
namespace Gedmo\Sluggable\Mapping\Event\Adapter;
use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM;
use Doctrine\ODM\MongoDB\Cursor;
use Gedmo\Sluggable\Mapping\Event\SluggableAdapter;
use Gedmo\Tool\Wrapper\AbstractWrapper;
/**
* Doctrine event adapter for ODM adapted
* for sluggable behavior
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
final class ODM extends BaseAdapterODM implements SluggableAdapter
{
/**
* {@inheritDoc}
*/
public function getSimilarSlugs($object, $meta, array $config, $slug)
{
$dm = $this->getObjectManager();
$wrapped = AbstractWrapper::wrap($object, $dm);
$qb = $dm->createQueryBuilder($config['useObjectClass']);
if (($identifier = $wrapped->getIdentifier()) && !$meta->isIdentifier($config['slug'])) {
$qb->field($meta->identifier)->notEqual($identifier);
}
$qb->field($config['slug'])->equals(new \MongoRegex('/^'.preg_quote($slug, '/').'/'));
// use the unique_base to restrict the uniqueness check
if ($config['unique'] && isset($config['unique_base'])) {
if (is_object($ubase = $wrapped->getPropertyValue($config['unique_base']))) {
$qb->field($config['unique_base'].'.$id')->equals(new \MongoId($ubase->getId()));
} elseif ($ubase) {
$qb->where('/^'.preg_quote($ubase, '/').'/.test(this.'.$config['unique_base'].')');
} else {
$qb->field($config['unique_base'])->equals(null);
}
}
$q = $qb->getQuery();
$q->setHydrate(false);
$result = $q->execute();
if ($result instanceof Cursor) {
$result = $result->toArray();
}
return $result;
}
/**
* This query can cause some data integrity failures since it does not
* execute automatically
*
* {@inheritDoc}
*/
public function replaceRelative($object, array $config, $target, $replacement)
{
$dm = $this->getObjectManager();
$meta = $dm->getClassMetadata($config['useObjectClass']);
$q = $dm
->createQueryBuilder($config['useObjectClass'])
->where("function() {
return this.{$config['slug']}.indexOf('{$target}') === 0;
}")
->getQuery()
;
$q->setHydrate(false);
$result = $q->execute();
if ($result instanceof Cursor) {
$result = $result->toArray();
foreach ($result as $targetObject) {
$slug = preg_replace("@^{$target}@smi", $replacement.$config['pathSeparator'], $targetObject[$config['slug']]);
$dm
->createQueryBuilder()
->update($config['useObjectClass'])
->field($config['slug'])->set($slug)
->field($meta->identifier)->equals($targetObject['_id'])
->getQuery()
->execute()
;
}
}
}
/**
* This query can cause some data integrity failures since it does not
* execute atomically
*
* {@inheritDoc}
*/
public function replaceInverseRelative($object, array $config, $target, $replacement)
{
$dm = $this->getObjectManager();
$wrapped = AbstractWrapper::wrap($object, $dm);
$meta = $dm->getClassMetadata($config['useObjectClass']);
$q = $dm
->createQueryBuilder($config['useObjectClass'])
->field($config['mappedBy'].'.'.$meta->identifier)->equals($wrapped->getIdentifier())
->getQuery()
;
$q->setHydrate(false);
$result = $q->execute();
if ($result instanceof Cursor) {
$result = $result->toArray();
foreach ($result as $targetObject) {
$slug = preg_replace("@^{$replacement}@smi", $target, $targetObject[$config['slug']]);
$dm
->createQueryBuilder()
->update($config['useObjectClass'])
->field($config['slug'])->set($slug)
->field($meta->identifier)->equals($targetObject['_id'])
->getQuery()
->execute()
;
}
}
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Gedmo\Sluggable\Mapping\Event\Adapter;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM;
use Doctrine\ORM\Query;
use Gedmo\Sluggable\Mapping\Event\SluggableAdapter;
use Gedmo\Tool\Wrapper\AbstractWrapper;
/**
* Doctrine event adapter for ORM adapted
* for sluggable behavior
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class ORM extends BaseAdapterORM implements SluggableAdapter
{
/**
* {@inheritDoc}
*/
public function getSimilarSlugs($object, $meta, array $config, $slug)
{
$em = $this->getObjectManager();
$wrapped = AbstractWrapper::wrap($object, $em);
$qb = $em->createQueryBuilder();
$qb->select('rec.'.$config['slug'])
->from($config['useObjectClass'], 'rec')
->where($qb->expr()->like(
'rec.'.$config['slug'],
':slug')
)
;
$qb->setParameter('slug',$slug.'%');
// use the unique_base to restrict the uniqueness check
if ($config['unique'] && isset($config['unique_base'])) {
$ubase = $wrapped->getPropertyValue($config['unique_base']);
if (array_key_exists($config['unique_base'], $wrapped->getMetadata()->getAssociationMappings())) {
$mapping = $wrapped->getMetadata()->getAssociationMapping($config['unique_base']);
} else {
$mapping = false;
}
if (($ubase || $ubase === 0) && !$mapping) {
$qb->andWhere('rec.'.$config['unique_base'].' = :unique_base');
$qb->setParameter(':unique_base', $ubase);
} elseif ($ubase && $mapping && in_array($mapping['type'], array(ClassMetadataInfo::ONE_TO_ONE, ClassMetadataInfo::MANY_TO_ONE))) {
$mappedAlias = 'mapped_'.$config['unique_base'];
$wrappedUbase = AbstractWrapper::wrap($ubase, $em);
$qb->innerJoin('rec.'.$config['unique_base'], $mappedAlias);
foreach (array_keys($mapping['targetToSourceKeyColumns']) as $i => $mappedKey) {
$mappedProp = $wrappedUbase->getMetadata()->fieldNames[$mappedKey];
$qb->andWhere($qb->expr()->eq($mappedAlias.'.'.$mappedProp, ':assoc'.$i));
$qb->setParameter(':assoc'.$i, $wrappedUbase->getPropertyValue($mappedProp));
}
} else {
$qb->andWhere($qb->expr()->isNull('rec.'.$config['unique_base']));
}
}
// include identifiers
foreach ((array) $wrapped->getIdentifier(false) as $id => $value) {
if (!$meta->isIdentifier($config['slug'])) {
$namedId = str_replace('.', '_', $id);
$qb->andWhere($qb->expr()->neq('rec.'.$id, ':'.$namedId));
$qb->setParameter($namedId, $value, $meta->getTypeOfField($namedId));
}
}
$q = $qb->getQuery();
$q->setHydrationMode(Query::HYDRATE_ARRAY);
return $q->execute();
}
/**
* {@inheritDoc}
*/
public function replaceRelative($object, array $config, $target, $replacement)
{
$em = $this->getObjectManager();
$qb = $em->createQueryBuilder();
$qb->update($config['useObjectClass'], 'rec')
->set('rec.'.$config['slug'], $qb->expr()->concat(
$qb->expr()->literal($replacement),
$qb->expr()->substring('rec.'.$config['slug'], mb_strlen($target))
))
->where($qb->expr()->like(
'rec.'.$config['slug'],
$qb->expr()->literal($target.'%'))
)
;
// update in memory
$q = $qb->getQuery();
return $q->execute();
}
/**
* {@inheritDoc}
*/
public function replaceInverseRelative($object, array $config, $target, $replacement)
{
$em = $this->getObjectManager();
$qb = $em->createQueryBuilder();
$qb->update($config['useObjectClass'], 'rec')
->set('rec.'.$config['slug'], $qb->expr()->concat(
$qb->expr()->literal($target),
$qb->expr()->substring('rec.'.$config['slug'], mb_strlen($replacement)+1)
))
->where($qb->expr()->like('rec.'.$config['slug'], $qb->expr()->literal($replacement . '%')))
;
$q = $qb->getQuery();
return $q->execute();
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Gedmo\Sluggable\Mapping\Event;
use Gedmo\Mapping\Event\AdapterInterface;
/**
* Doctrine event adapter interface
* for Sluggable behavior
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
interface SluggableAdapter extends AdapterInterface
{
/**
* Loads the similar slugs
*
* @param object $object
* @param object $meta
* @param array $config
* @param string $slug
*
* @return array
*/
public function getSimilarSlugs($object, $meta, array $config, $slug);
/**
* Replace part of slug to all objects
* matching $target pattern
*
* @param object $object
* @param array $config
* @param string $target
* @param string $replacement
*
* @return integer
*/
public function replaceRelative($object, array $config, $target, $replacement);
/**
* Replace part of slug to all objects
* matching $target pattern and having $object
* related
*
* @param object $object
* @param array $config
* @param string $target
* @param string $replacement
*
* @return integer
*/
public function replaceInverseRelative($object, array $config, $target, $replacement);
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Gedmo\Sluggable;
/**
* This interface is not necessary but can be implemented for
* Entities which in some cases needs to be identified as
* Sluggable
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
interface Sluggable
{
// use now annotations instead of predefined methods, this interface is not necessary
/**
* @gedmo:Sluggable
* to mark the field as sluggable use property annotation @gedmo:Sluggable
* this field value will be included in built slug
*/
/**
* @gedmo:Slug - to mark property which will hold slug use annotation @gedmo:Slug
* available options:
* updatable (optional, default=true) - true to update the slug on sluggable field changes, false - otherwise
* unique (optional, default=true) - true if slug should be unique and if identical it will be prefixed, false - otherwise
* unique_base (optional, default="") - used in conjunction with unique. The name of the entity property that should be used as a key when doing a uniqueness check
* separator (optional, default="-") - separator which will separate words in slug
* prefix (optional, default="") - prefix which will be added to the generated slug
* suffix (optional, default="") - suffix which will be added to the generated slug
* style (optional, default="default") - "default" all letters will be lowercase, "camel" - first word letter will be uppercase
* dateFormat (optional, default="default") - "default" all letters will be lowercase, "camel" - first word letter will be uppercase
*
* example:
*
* @gedmo:Slug(style="camel", separator="_", prefix="", suffix="", updatable=false, unique=false)
* @Column(type="string", length=64)
* $property
*/
}

View File

@@ -0,0 +1,569 @@
<?php
namespace Gedmo\Sluggable;
use Doctrine\Common\EventArgs;
use Gedmo\Mapping\MappedEventSubscriber;
use Gedmo\Sluggable\Handler\SlugHandlerWithUniqueCallbackInterface;
use Gedmo\Sluggable\Mapping\Event\SluggableAdapter;
use Doctrine\Common\Persistence\ObjectManager;
use Gedmo\Tool\Wrapper\AbstractWrapper;
/**
* The SluggableListener handles the generation of slugs
* for documents and entities.
*
* This behavior can impact the performance of your application
* since it does some additional calculations on persisted objects.
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @author Klein Florian <florian.klein@free.fr>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class SluggableListener extends MappedEventSubscriber
{
/**
* The power exponent to jump
* the slug unique number by tens.
*
* @var integer
*/
private $exponent = 0;
/**
* Transliteration callback for slugs
*
* @var callable
*/
private $transliterator = array('Gedmo\Sluggable\Util\Urlizer', 'transliterate');
/**
* Urlize callback for slugs
*
* @var callable
*/
private $urlizer = array('Gedmo\Sluggable\Util\Urlizer', 'urlize');
/**
* List of inserted slugs for each object class.
* This is needed in case there are identical slug
* composition in number of persisted objects
* during the same flush
*
* @var array
*/
private $persisted = array();
/**
* List of initialized slug handlers
*
* @var array
*/
private $handlers = array();
/**
* List of filters which are manipulated when slugs are generated
*
* @var array
*/
private $managedFilters = array();
/**
* Specifies the list of events to listen
*
* @return array
*/
public function getSubscribedEvents()
{
return array(
'onFlush',
'loadClassMetadata',
'prePersist',
);
}
/**
* Set the transliteration callable method
* to transliterate slugs
*
* @param callable $callable
*
* @throws \Gedmo\Exception\InvalidArgumentException
*
* @return void
*/
public function setTransliterator($callable)
{
if (!is_callable($callable)) {
throw new \Gedmo\Exception\InvalidArgumentException('Invalid transliterator callable parameter given');
}
$this->transliterator = $callable;
}
/**
* Set the urlization callable method
* to urlize slugs
*
* @param callable $callable
*/
public function setUrlizer($callable)
{
if (!is_callable($callable)) {
throw new \Gedmo\Exception\InvalidArgumentException('Invalid urlizer callable parameter given');
}
$this->urlizer = $callable;
}
/**
* Get currently used transliterator callable
*
* @return callable
*/
public function getTransliterator()
{
return $this->transliterator;
}
/**
* Get currently used urlizer callable
*
* @return callable
*/
public function getUrlizer()
{
return $this->urlizer;
}
/**
* Enables or disables the given filter when slugs are generated
*
* @param string $name
* @param bool $disable True by default
*/
public function addManagedFilter($name, $disable = true)
{
$this->managedFilters[$name] = array('disabled' => $disable);
}
/**
* Removes a filter from the managed set
*
* @param string $name
*/
public function removeManagedFilter($name)
{
unset($this->managedFilters[$name]);
}
/**
* Mapps additional metadata
*
* @param EventArgs $eventArgs
*
* @return void
*/
public function loadClassMetadata(EventArgs $eventArgs)
{
$ea = $this->getEventAdapter($eventArgs);
$this->loadMetadataForObjectClass($ea->getObjectManager(), $eventArgs->getClassMetadata());
}
/**
* Allows identifier fields to be slugged as usual
*
* @param EventArgs $args
*
* @return void
*/
public function prePersist(EventArgs $args)
{
$ea = $this->getEventAdapter($args);
$om = $ea->getObjectManager();
$object = $ea->getObject();
$meta = $om->getClassMetadata(get_class($object));
if ($config = $this->getConfiguration($om, $meta->name)) {
foreach ($config['slugs'] as $slugField => $options) {
if ($meta->isIdentifier($slugField)) {
$meta->getReflectionProperty($slugField)->setValue($object, '__id__');
}
}
}
}
/**
* Generate slug on objects being updated during flush
* if they require changing
*
* @param EventArgs $args
*
* @return void
*/
public function onFlush(EventArgs $args)
{
$this->persisted = array();
$ea = $this->getEventAdapter($args);
$om = $ea->getObjectManager();
$uow = $om->getUnitOfWork();
$this->manageFiltersBeforeGeneration($om);
// process all objects being inserted, using scheduled insertions instead
// of prePersist in case if record will be changed before flushing this will
// ensure correct result. No additional overhead is encountered
foreach ($ea->getScheduledObjectInsertions($uow) as $object) {
$meta = $om->getClassMetadata(get_class($object));
if ($this->getConfiguration($om, $meta->name)) {
// generate first to exclude this object from similar persisted slugs result
$this->generateSlug($ea, $object);
$this->persisted[$ea->getRootObjectClass($meta)][] = $object;
}
}
// we use onFlush and not preUpdate event to let other
// event listeners be nested together
foreach ($ea->getScheduledObjectUpdates($uow) as $object) {
$meta = $om->getClassMetadata(get_class($object));
if ($this->getConfiguration($om, $meta->name) && !$uow->isScheduledForInsert($object)) {
$this->generateSlug($ea, $object);
$this->persisted[$ea->getRootObjectClass($meta)][] = $object;
}
}
$this->manageFiltersAfterGeneration($om);
AbstractWrapper::clear();
}
/**
* {@inheritDoc}
*/
protected function getNamespace()
{
return __NAMESPACE__;
}
/**
* Get the slug handler instance by $class name
*
* @param string $class
*
* @return \Gedmo\Sluggable\Handler\SlugHandlerInterface
*/
private function getHandler($class)
{
if (!isset($this->handlers[$class])) {
$this->handlers[$class] = new $class($this);
}
return $this->handlers[$class];
}
/**
* Creates the slug for object being flushed
*
* @param SluggableAdapter $ea
* @param object $object
*
* @return void
*/
private function generateSlug(SluggableAdapter $ea, $object)
{
$om = $ea->getObjectManager();
$meta = $om->getClassMetadata(get_class($object));
$uow = $om->getUnitOfWork();
$changeSet = $ea->getObjectChangeSet($uow, $object);
$isInsert = $uow->isScheduledForInsert($object);
$config = $this->getConfiguration($om, $meta->name);
foreach ($config['slugs'] as $slugField => $options) {
$hasHandlers = count($options['handlers']);
$options['useObjectClass'] = $config['useObjectClass'];
// collect the slug from fields
$slug = $meta->getReflectionProperty($slugField)->getValue($object);
// if slug should not be updated, skip it
if (!$options['updatable'] && !$isInsert && (!isset($changeSet[$slugField]) || $slug === '__id__')) {
continue;
}
// must fetch the old slug from changeset, since $object holds the new version
$oldSlug = isset($changeSet[$slugField]) ? $changeSet[$slugField][0] : $slug;
$needToChangeSlug = false;
// if slug is null, regenerate it, or needs an update
if (null === $slug || $slug === '__id__' || !isset($changeSet[$slugField])) {
$slug = '';
foreach ($options['fields'] as $sluggableField) {
if (isset($changeSet[$sluggableField]) || isset($changeSet[$slugField])) {
$needToChangeSlug = true;
}
$value = $meta->getReflectionProperty($sluggableField)->getValue($object);
// Remove `$value instanceof \DateTime` check when PHP version is bumped to >=5.5
$slug .= ($value instanceof \DateTime || $value instanceof \DateTimeInterface) ? $value->format($options['dateFormat']) : $value;
$slug .= ' ';
}
// trim generated slug as it will have unnecessary trailing space
$slug = trim($slug);
} else {
// slug was set manually
$needToChangeSlug = true;
}
// notify slug handlers --> onChangeDecision
if ($hasHandlers) {
foreach ($options['handlers'] as $class => $handlerOptions) {
$this->getHandler($class)->onChangeDecision($ea, $options, $object, $slug, $needToChangeSlug);
}
}
// if slug is changed, do further processing
if ($needToChangeSlug) {
$mapping = $meta->getFieldMapping($slugField);
// notify slug handlers --> postSlugBuild
$urlized = false;
if ($hasHandlers) {
foreach ($options['handlers'] as $class => $handlerOptions) {
$this->getHandler($class)->postSlugBuild($ea, $options, $object, $slug);
if ($this->getHandler($class)->handlesUrlization()) {
$urlized = true;
}
}
}
// build the slug
// Step 1: transliteration, changing 北京 to 'Bei Jing'
$slug = call_user_func_array(
$this->transliterator,
array($slug, $options['separator'], $object)
);
// Step 2: urlization (replace spaces by '-' etc...)
if (!$urlized) {
$slug = call_user_func_array(
$this->urlizer,
array($slug, $options['separator'], $object)
);
}
// add suffix/prefix
$slug = $options['prefix'].$slug.$options['suffix'];
// Step 3: stylize the slug
switch ($options['style']) {
case 'camel':
$quotedSeparator = preg_quote($options['separator']);
$slug = preg_replace_callback('/^[a-z]|'.$quotedSeparator.'[a-z]/smi', function ($m) {
return strtoupper($m[0]);
}, $slug);
break;
case 'lower':
if (function_exists('mb_strtolower')) {
$slug = mb_strtolower($slug);
} else {
$slug = strtolower($slug);
}
break;
case 'upper':
if (function_exists('mb_strtoupper')) {
$slug = mb_strtoupper($slug);
} else {
$slug = strtoupper($slug);
}
break;
default:
// leave it as is
break;
}
// cut slug if exceeded in length
if (isset($mapping['length']) && strlen($slug) > $mapping['length']) {
$slug = substr($slug, 0, $mapping['length']);
}
if (isset($mapping['nullable']) && $mapping['nullable'] && strlen($slug) === 0) {
$slug = null;
}
// notify slug handlers --> beforeMakingUnique
if ($hasHandlers) {
foreach ($options['handlers'] as $class => $handlerOptions) {
$handler = $this->getHandler($class);
if ($handler instanceof SlugHandlerWithUniqueCallbackInterface) {
$handler->beforeMakingUnique($ea, $options, $object, $slug);
}
}
}
// make unique slug if requested
if ($options['unique'] && null !== $slug) {
$this->exponent = 0;
$slug = $this->makeUniqueSlug($ea, $object, $slug, false, $options);
}
// notify slug handlers --> onSlugCompletion
if ($hasHandlers) {
foreach ($options['handlers'] as $class => $handlerOptions) {
$this->getHandler($class)->onSlugCompletion($ea, $options, $object, $slug);
}
}
// set the final slug
$meta->getReflectionProperty($slugField)->setValue($object, $slug);
// recompute changeset
$ea->recomputeSingleObjectChangeSet($uow, $meta, $object);
// overwrite changeset (to set old value)
$uow->propertyChanged($object, $slugField, $oldSlug, $slug);
}
}
}
/**
* Generates the unique slug
*
* @param SluggableAdapter $ea
* @param object $object
* @param string $preferredSlug
* @param boolean $recursing
* @param array $config[$slugField]
*
* @return string - unique slug
*/
private function makeUniqueSlug(SluggableAdapter $ea, $object, $preferredSlug, $recursing = false, $config = array())
{
$om = $ea->getObjectManager();
$meta = $om->getClassMetadata(get_class($object));
$similarPersisted = array();
// extract unique base
$base = false;
if ($config['unique'] && isset($config['unique_base'])) {
$base = $meta->getReflectionProperty($config['unique_base'])->getValue($object);
}
// collect similar persisted slugs during this flush
if (isset($this->persisted[$class = $ea->getRootObjectClass($meta)])) {
foreach ($this->persisted[$class] as $obj) {
if ($base !== false && $meta->getReflectionProperty($config['unique_base'])->getValue($obj) !== $base) {
continue; // if unique_base field is not the same, do not take slug as similar
}
$slug = $meta->getReflectionProperty($config['slug'])->getValue($obj);
$quotedPreferredSlug = preg_quote($preferredSlug);
if (preg_match("@^{$quotedPreferredSlug}.*@smi", $slug)) {
$similarPersisted[] = array($config['slug'] => $slug);
}
}
}
// load similar slugs
$result = array_merge((array) $ea->getSimilarSlugs($object, $meta, $config, $preferredSlug), $similarPersisted);
// leave only right slugs
if (!$recursing) {
// filter similar slugs
$quotedSeparator = preg_quote($config['separator']);
$quotedPreferredSlug = preg_quote($preferredSlug);
foreach ($result as $key => $similar) {
if (!preg_match("@{$quotedPreferredSlug}($|{$quotedSeparator}[\d]+$)@smi", $similar[$config['slug']])) {
unset($result[$key]);
}
}
}
if ($result) {
$generatedSlug = $preferredSlug;
$sameSlugs = array();
foreach ((array) $result as $list) {
$sameSlugs[] = $list[$config['slug']];
}
$i = pow(10, $this->exponent);
if ($recursing || in_array($generatedSlug, $sameSlugs)) {
do {
$generatedSlug = $preferredSlug.$config['separator'].$i++;
} while (in_array($generatedSlug, $sameSlugs));
}
$mapping = $meta->getFieldMapping($config['slug']);
if (isset($mapping['length']) && strlen($generatedSlug) > $mapping['length']) {
$generatedSlug = substr(
$generatedSlug,
0,
$mapping['length'] - (strlen($i) + strlen($config['separator']))
);
$this->exponent = strlen($i) - 1;
if (substr($generatedSlug,-strlen($config['separator'])) == $config['separator']) {
$generatedSlug = substr($generatedSlug,0,strlen($generatedSlug) - strlen($config['separator']));
}
$generatedSlug = $this->makeUniqueSlug($ea, $object, $generatedSlug, true, $config);
}
$preferredSlug = $generatedSlug;
}
return $preferredSlug;
}
/**
* @param \Doctrine\Common\Persistence\ObjectManager $om
*/
private function manageFiltersBeforeGeneration(ObjectManager $om)
{
$collection = $this->getFilterCollectionFromObjectManager($om);
$enabledFilters = array_keys($collection->getEnabledFilters());
// set each managed filter to desired status
foreach ($this->managedFilters as $name => &$config) {
$enabled = in_array($name, $enabledFilters);
$config['previouslyEnabled'] = $enabled;
if ($config['disabled']) {
if ($enabled) {
$collection->disable($name);
}
} else {
$collection->enable($name);
}
}
}
/**
* @param \Doctrine\Common\Persistence\ObjectManager $om
*/
private function manageFiltersAfterGeneration(ObjectManager $om)
{
$collection = $this->getFilterCollectionFromObjectManager($om);
// Restore managed filters to their original status
foreach ($this->managedFilters as $name => &$config) {
if ($config['previouslyEnabled'] === true) {
$collection->enable($name);
}
unset($config['previouslyEnabled']);
}
}
/**
* Retrieves a FilterCollection instance from the given ObjectManager.
*
* @param \Doctrine\Common\Persistence\ObjectManager $om
*
* @throws \Gedmo\Exception\InvalidArgumentException
*
* @return mixed
*/
private function getFilterCollectionFromObjectManager(ObjectManager $om)
{
if (is_callable(array($om, 'getFilters'))) {
return $om->getFilters();
} elseif (is_callable(array($om, 'getFilterCollection'))) {
return $om->getFilterCollection();
}
throw new \Gedmo\Exception\InvalidArgumentException("ObjectManager does not support filters");
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Gedmo\Sluggable\Util;
use Behat\Transliterator\Transliterator;
/**
* Transliteration utility
*/
class Urlizer extends Transliterator
{
}