Created
January 10, 2015 08:20
-
-
Save duskohu/34aaf4c7ba690f65d4b8 to your computer and use it in GitHub Desktop.
TreePathListener
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
services: | |
- | |
class: Nas\PagesModule\Model\Listeners\TreePathListener | |
tags: [kdyby.subscriber] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* This file is part of the Nas of Nette Framework | |
* | |
* Copyright (c) 2013 Dusan Hudak (http://dusan-hudak.com) | |
* | |
* For the full copyright and license information, please view | |
* the file license.txt that was distributed with this source code. | |
*/ | |
namespace Nas\PagesModule\Model; | |
use Gedmo\Mapping\Annotation as Gedmo; | |
use Doctrine\ORM\Mapping as ORM; | |
use Kdyby\Doctrine\Entities\Attributes\Identifier; | |
use Nette\InvalidArgumentException; | |
use Nas\PagesModule\Model\Annotations\TreePath; | |
/** | |
* @Gedmo\Tree(type="nested") | |
* @ORM\Table(name="pages") | |
* @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository") | |
*/ | |
class Page | |
{ | |
use Identifier; | |
const PUBLICITY_PUBLIC = TRUE; | |
const PUBLICITY_NO_PUBLIC = FALSE; | |
/** | |
* @ORM\Column(name="title", type="string", length=255) | |
* @var string | |
*/ | |
private $title; | |
/** | |
* @Gedmo\TreeLeft | |
* @ORM\Column(name="lft", type="integer") | |
* @var int | |
*/ | |
private $lft; | |
/** | |
* @Gedmo\TreeLevel | |
* @ORM\Column(name="lvl", type="integer") | |
* @var int | |
*/ | |
private $lvl; | |
/** | |
* @Gedmo\TreeRight | |
* @ORM\Column(name="rgt", type="integer") | |
* @var int | |
*/ | |
private $rgt; | |
/** | |
* @Gedmo\TreeRoot | |
* @ORM\Column(name="root", type="integer", nullable=true) | |
* @var int | |
*/ | |
private $root; | |
/** | |
* @Gedmo\TreeParent | |
* @ORM\ManyToOne(targetEntity="Page", inversedBy="children") | |
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") | |
* @var int | |
*/ | |
private $parent; | |
/** | |
* @ORM\OneToMany(targetEntity="Page", mappedBy="parent") | |
* @ORM\OrderBy({"lft" = "ASC"}) | |
* @var int | |
*/ | |
private $children; | |
/** | |
* @ORM\Column(name="publicity", type="boolean") | |
* @var bool | |
*/ | |
private $publicity; | |
/** | |
* @ORM\Column(name="keywords", type="text", nullable=true) | |
* @var string | |
*/ | |
private $keywords; | |
/** | |
* @ORM\Column(name="context", type="text", nullable=true) | |
* @var string | |
*/ | |
private $context; | |
/** | |
* @ORM\Column(name="description", type="text", nullable=true) | |
* @var string | |
*/ | |
private $description; | |
/** | |
* @ORM\Column(name="slug", type="text", nullable=true) | |
* @var string | |
*/ | |
private $slug; | |
/** | |
* @Nas\PagesModule\Model\Annotations\TreePath(slugField="slug", separator="/", parentRelationField="parent") | |
* @ORM\Column(name="path", type="string", length=255, unique=true)) | |
*/ | |
private $path; | |
/** | |
* @param string $title | |
*/ | |
public function setTitle($title) | |
{ | |
$this->title = $title; | |
} | |
/** | |
* @return int string | |
*/ | |
public function getTitle() | |
{ | |
return $this->title; | |
} | |
/** | |
* @param Page $parent | |
*/ | |
public function setParent(Page $parent = NULL) | |
{ | |
$this->parent = $parent; | |
} | |
/** | |
* @return Page|NULL | |
*/ | |
public function getParent() | |
{ | |
return $this->parent; | |
} | |
/** | |
* @return \Doctrine\ORM\PersistentCollection | |
*/ | |
public function getChildren() | |
{ | |
return $this->children; | |
} | |
/** | |
* @return int | |
*/ | |
public function getRoot() | |
{ | |
return $this->root; | |
} | |
/** | |
* @return int | |
*/ | |
public function getLvl() | |
{ | |
return $this->lvl; | |
} | |
/** | |
* @return int | |
*/ | |
public function getRgt() | |
{ | |
return $this->rgt; | |
} | |
/** | |
* @return int | |
*/ | |
public function getLft() | |
{ | |
return $this->lft; | |
} | |
/** | |
* @param bool $publicity | |
* @throws \Nette\InvalidArgumentException | |
*/ | |
public function setPublicity($publicity) | |
{ | |
if ($publicity !== self::PUBLICITY_PUBLIC && $publicity !== self::PUBLICITY_NO_PUBLIC) { | |
throw new InvalidArgumentException('Parameter $publicity can be only "' . __CLASS__ . '::PUBLICITY_PUBLIC" or "' . __CLASS__ . '::PUBLICITY_NO_PUBLIC"'); | |
} | |
$this->publicity = $publicity; | |
} | |
/** | |
* @return bool | |
*/ | |
public function getPublicity() | |
{ | |
return $this->publicity; | |
} | |
/** | |
* @return string | |
*/ | |
public function getKeywords() | |
{ | |
return $this->keywords; | |
} | |
/** | |
* @param string $keywords | |
*/ | |
public function setKeywords($keywords) | |
{ | |
$this->keywords = $keywords; | |
} | |
/** | |
* @return string | |
*/ | |
public function getDescription() | |
{ | |
return $this->description; | |
} | |
/** | |
* @param string $description | |
*/ | |
public function setDescription($description) | |
{ | |
$this->description = $description; | |
} | |
/** | |
* @return string | |
*/ | |
public function getContext() | |
{ | |
return $this->context; | |
} | |
/** | |
* @param string $context | |
*/ | |
public function setContext($context) | |
{ | |
$this->context = $context; | |
} | |
/** | |
* @return string | |
*/ | |
public function getSlug() | |
{ | |
return $this->slug; | |
} | |
/** | |
* @param string $slug | |
*/ | |
public function setSlug($slug) | |
{ | |
$this->slug = $slug; | |
} | |
/** | |
* @return string | |
*/ | |
public function getPath() | |
{ | |
return $this->path; | |
} | |
/** | |
* @return string | |
*/ | |
public static function getClassName() | |
{ | |
return __CLASS__; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* This file is part of the Nas of Nette Framework | |
* | |
* Copyright (c) 2013 Dusan Hudak (http://dusan-hudak.com) | |
* | |
* For the full copyright and license information, please view | |
* the file license.txt that was distributed with this source code. | |
*/ | |
namespace Nas\PagesModule\Model\Annotations; | |
use Doctrine\Common\Annotations\Annotation; | |
/** | |
* TreePath annotation for TreePathListener | |
* | |
* @Annotation | |
* @Target("PROPERTY") | |
* | |
* @author Dusan Hudak <[email protected]> | |
*/ | |
final class TreePath extends Annotation | |
{ | |
/** @var string @required */ | |
public $slugField; | |
/** @var string @required */ | |
public $separator; | |
/** @var string @required */ | |
public $parentRelationField; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* This file is part of the Nas of Nette Framework | |
* | |
* Copyright (c) 2013 Dusan Hudak (http://dusan-hudak.com) | |
* | |
* For the full copyright and license information, please view | |
* the file license.txt that was distributed with this source code. | |
*/ | |
namespace Nas\PagesModule\Model\Listeners; | |
use Doctrine\Common\Annotations\AnnotationReader; | |
use Doctrine\ORM\EntityManager; | |
use Doctrine\ORM\Event\OnFlushEventArgs; | |
use Doctrine\ORM\Events; | |
use Doctrine\ORM\Query; | |
use Kdyby\Events\Subscriber; | |
/** | |
* @author Dusan Hudak <[email protected]> | |
*/ | |
class TreePathListener implements Subscriber | |
{ | |
const SLUG_SEPARATOR = '-'; | |
/** | |
* Specifies the list of events to listen | |
* | |
* @return array | |
*/ | |
public function getSubscribedEvents() | |
{ | |
return array( | |
Events::onFlush, | |
); | |
} | |
/** | |
* @param $entity | |
* @param EntityManager $em | |
* @return array | |
*/ | |
private function getPathColumns($entity, EntityManager $em) | |
{ | |
$meta = $em->getClassMetadata(get_class($entity)); | |
$reader = new AnnotationReader(); | |
$reader->getClassAnnotations($meta->getReflectionClass()); | |
$pathColumns = array(); | |
foreach ($meta->getReflectionProperties() as $name => $property) { | |
$annotation = $reader->getPropertyAnnotation($property, 'Nas\PagesModule\Model\Annotations\TreePath'); | |
if ($annotation !== NULL) { | |
$pathColumns[$name] = $annotation; | |
} | |
} | |
return $pathColumns; | |
} | |
/** | |
* @param EntityManager $em | |
* @param object $object | |
* @param string $path | |
* @param string $columnName | |
* @return array | |
*/ | |
private function getSimilarPaths($em, $path, $columnName, $object) | |
{ | |
$meta = $em->getClassMetadata(get_class($object)); | |
$reflectionClass = $meta->getReflectionClass(); | |
$uow = $em->getUnitOfWork(); | |
$qb = $em->createQueryBuilder(); | |
$qb->select('rec.' . $columnName) | |
->from($reflectionClass->name, 'rec') | |
->where($qb->expr()->like( | |
'rec.' . $columnName, | |
$qb->expr()->literal($path . '%')) | |
); | |
// Find also path without increment | |
$pos = strrpos($path, self::SLUG_SEPARATOR); | |
if ($pos !== FALSE) { | |
$number = substr($path, $pos + 1); | |
if (is_numeric($number)) { | |
$pos = strrpos($path, self::SLUG_SEPARATOR); | |
if ($pos !== FALSE) { | |
$qb->orWhere($qb->expr()->like( | |
'rec.' . $columnName, | |
$qb->expr()->literal(substr($path, 0, $pos) . '%')) | |
); | |
} | |
} | |
} | |
// Without actual object | |
$identifierValue = $uow->getSingleIdentifierValue($object); | |
if ($identifierValue) { | |
$qb->andWhere('rec.' . $meta->identifier[0] . ' != :identifier'); | |
$qb->setParameter('identifier', $uow->getSingleIdentifierValue($object)); | |
} | |
$q = $qb->getQuery(); | |
$q->setHydrationMode(Query::HYDRATE_ARRAY); | |
return $q->execute(); | |
} | |
/** | |
* @param EntityManager $em | |
* @param string $path | |
* @param string $columnName | |
* @param object $object | |
* @return string | |
*/ | |
private function regeneratePath($em, $path, $columnName, $object) | |
{ | |
$similarPath = $this->getSimilarPaths($em, $path, $columnName, $object); | |
$generatedPath = $path; | |
if (!empty($similarPath)) { | |
$samePath = array(); | |
foreach ($similarPath as $similar) { | |
$samePath[] = $similar[$columnName]; | |
} | |
$i = 1; | |
// If path have of the end number remove number from path | |
$pos = strrpos($path, self::SLUG_SEPARATOR); | |
if ($pos !== FALSE) { | |
$number = substr($path, $pos + 1); | |
if (is_numeric($number)) { | |
$i = $number; | |
$pos = strrpos($path, self::SLUG_SEPARATOR); | |
if ($pos !== FALSE) { | |
$path = substr($path, 0, $pos); | |
} | |
} | |
} | |
if (in_array($generatedPath, $samePath)) { | |
do { | |
$generatedPath = $path . self::SLUG_SEPARATOR . $i++; | |
} while (in_array($generatedPath, $samePath)); | |
} | |
} | |
return $generatedPath; | |
} | |
/** | |
* @param EntityManager $em | |
* @param string $oldPath | |
* @param string $newPath | |
* @param string $columnName | |
* @param object $object | |
* @return mixed | |
*/ | |
public function replaceRelative($em, $oldPath, $newPath, $columnName, $object) | |
{ | |
$meta = $em->getClassMetadata(get_class($object)); | |
$reflectionClass = $meta->getReflectionClass(); | |
$qb = $em->createQueryBuilder(); | |
$qb->update($reflectionClass->name, 'rec') | |
->set('rec.' . $columnName, $qb->expr()->concat( | |
$qb->expr()->literal($newPath), | |
$qb->expr()->substring('rec.' . $columnName, strlen($oldPath) + 1) | |
)) | |
->where($qb->expr()->like( | |
'rec.' . $columnName, | |
$qb->expr()->literal($oldPath . '%')) | |
); | |
$q = $qb->getQuery(); | |
return $q->execute(); | |
} | |
/** | |
* Generate slug on objects being updated during flush | |
* if they require changing | |
* | |
* @param OnFlushEventArgs $args | |
* @return void | |
*/ | |
public function onFlush(OnFlushEventArgs $args) | |
{ | |
$em = $args->getEntityManager(); | |
$uow = $em->getUnitOfWork(); | |
// Process Insertions | |
foreach ($uow->getScheduledEntityInsertions() as $key => $object) { | |
$pathColumns = $this->getPathColumns($object, $em); | |
if (!empty($pathColumns)) { | |
$meta = $em->getClassMetadata(get_class($object)); | |
foreach ($pathColumns as $name => $property) { | |
$slug = $meta->getReflectionProperty($property->slugField)->getValue($object); | |
$separator = $property->separator; | |
$parentRelationField = $meta->getReflectionProperty($property->parentRelationField)->getValue($object); | |
$parentPath = ''; | |
if ($parentRelationField) { | |
$parentPath = $meta->getReflectionProperty($name)->getValue($parentRelationField); | |
} | |
$path = ($parentPath !== $separator) ? $parentPath . $separator : $parentPath; | |
$path .= $slug; | |
// Regenerate Path | |
$newPath = $this->regeneratePath($em, $path, $name, $object); | |
// Change slug if change path | |
if ($newPath !== $path) { | |
$pos = strrpos($newPath, $separator); | |
if ($pos !== FALSE) { | |
$uow->propertyChanged($object, $property->slugField, $slug, substr($newPath, $pos + 1)); | |
} | |
} | |
$oldPath = $meta->getReflectionProperty($name)->getValue($object); | |
$uow->propertyChanged($object, $name, $oldPath, $newPath); | |
} | |
} | |
} | |
// Process Updates | |
foreach ($uow->getScheduledEntityUpdates() as $key => $object) { | |
$pathColumns = $this->getPathColumns($object, $em); | |
if (!empty($pathColumns)) { | |
$meta = $em->getClassMetadata(get_class($object)); | |
foreach ($pathColumns as $name => $property) { | |
$slug = $meta->getReflectionProperty($property->slugField)->getValue($object); | |
$separator = $property->separator; | |
$parentRelationField = $meta->getReflectionProperty($property->parentRelationField)->getValue($object); | |
$parentPath = ''; | |
if ($parentRelationField) { | |
$parentPath = $meta->getReflectionProperty($name)->getValue($parentRelationField); | |
} | |
$path = ($parentPath !== $separator) ? $parentPath . $separator : $parentPath; | |
$path .= $slug; | |
// Regenerate Path | |
$newPath = $this->regeneratePath($em, $path, $name, $object); | |
// Change slug if change path | |
if ($newPath !== $path) { | |
$pos = strrpos($newPath, $separator); | |
if ($pos !== FALSE) { | |
$uow->propertyChanged($object, $property->slugField, $slug, substr($newPath, $pos + 1)); | |
} | |
} | |
$oldPath = $meta->getReflectionProperty($name)->getValue($object); | |
$uow->propertyChanged($object, $name, $oldPath, $newPath); | |
//Change path of child items | |
$this->replaceRelative($em, $oldPath, $newPath, $name, $object); | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment