diff --git a/README.md b/README.md index 73eb412..6839a83 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ entity decorator A Drupal 7 Module to beautifully decorate your entities. +Packagist is here https://packagist.org/packages/alnutile/entity_decorator + Preamble -------- @@ -33,9 +35,13 @@ Entity decorator should work equally well with custom entities and node types, w Usage ----- +Install and update composer using [composer_manager](https://www.drupal.org/project/composer_manager) + Creating the decorator class is simple. Create a class like this. ``` +use EntityDecorator; + class MyDecoratedNodeType extends EntityDecorator { static public $entityType = 'node'; static public $bundle = 'my_node_type'; diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..4683063 --- /dev/null +++ b/composer.json @@ -0,0 +1,30 @@ +{ + "name": "alnutile/entity_decorator", + "type": "library", + "description": "A Drupal 7 Module to beautifully decorate your entities.", + "keywords": ["drupal"], + "homepage": "https://github.com/alnutile/entity_decorator", + "license": "GPLv2", + "authors": [ + { + "name": "dansingerman", + "email": "dansingerman@example.com", + "homepage": "https://github.com/dansingerman", + "role": "Original Developer" + }, + { + "name": "Alfred Nutile", + "email": "alfrednutile@gmail.com", + "homepage": "https://github.com/alnutile", + "role": "Setup PSR-0" + } + ], + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "psr-0": { + "EntityDecorator": "src" + } + } +} \ No newline at end of file diff --git a/entity_decorator.info b/entity_decorator.info index 860f62e..901f236 100644 --- a/entity_decorator.info +++ b/entity_decorator.info @@ -9,5 +9,4 @@ core = 7.x php = 5.3 dependencies[] = entity - -files[] = entity_decorator_finder.class.inc +dependencies[] = composer_manager diff --git a/entity_decorator.module b/entity_decorator.module index 77d2f19..b3d9bbc 100644 --- a/entity_decorator.module +++ b/entity_decorator.module @@ -1,229 +1 @@ entity = $entity; - } - else { - if ($this->getEntityType() == 'node') { - // Node specific preparation. - $this->entity = new stdClass; - $this->entity->type = $this->getBundle(); - node_object_prepare($this->entity); - } - else { - $this->entity = entity_create($this->getEntityType(), array('type' => $this->getBundle())); - } - } - } - - /** - * Create an instance of the class from a raw entity object. - * @param $entity - * @return Instance of subclass of EntityDecorator - */ - static public function buildFromEntity($entity) { - $class = get_called_class(); - $object = new $class($entity); - return $object; - } - - /** - * Find an instance by ID. - * @param int $id - * @return Instance of subclass of EntityDecorator - */ - static public function find($id) { - $class = get_called_class(); - - if ($class::$entityType == 'node') { - $entity = $class::findBy('nid', array($id))->execute(); - } - else { - $entity = $class::findBy('id', array($id))->execute(); - } - - if (count($entity)) { - return current($entity); - } - } - - static protected function getFinder() { - $class = get_called_class(); - return new EntityDecoratorFinder($class, $class::$bundle, $class::$entityType); - } - - /** - * Find entities where the field_name or property has a matching value. - * @param string $field_name the field or property to match on - * @param array or scalar $value the value to match on - * @return EntityDecoratorFinder instance - */ - static public function findBy($field_name, $value) { - return self::getFinder()->findBy($field_name, $value); - } - - /** - * Find the first entity where the field_name or property has a matching value. - * @param string $field_name the field or property to match on - * @param array or scalar $value the value to match on - * @return EntityDecorator subclass instance - */ - static public function findFirstBy($field_name, $value) { - return self::getFinder()->findFirstBy($field_name, $value); - } - - private function getEntityType() { - $class = get_called_class(); - return $class::$entityType; - } - - private function getBundle() { - $class = get_called_class(); - return $class::$bundle; - } - - public function getWrappedEntity() { - return entity_metadata_wrapper($this->getEntityType(), $this->entity); - } - - /** - * Set a field or property of the extended entity. - * - * @param string $field The name of the field or property to set - * @param $value The value the field or property should have - */ - public function set($attr_name, $value) { - $this->getWrappedEntity()->$attr_name->set($value); - } - - /** - * Get a field or property of the extended entity. - * - * @param string $field The name of the field or property to get - */ - public function get($attr_name) { - return $this->getWrappedEntity()->$attr_name->value(); - } - - /** - * Get a field or property of the extended entity that is an object and return a decorated instance. - * Works with arrays of objects too (e.g like a field collection). - * If the field is not an object or an array of objects, it returns the raw value. - * - * @param string $field The name of the field or property to get - * @param string $class_name The name of the class to decorate it with - */ - public function getDecorated($attr_name, $class_name) { - $field = $this->get($attr_name); - - if (is_object($field)) { - return $class_name::buildFromEntity($field); - } - elseif (is_array($field)) { - return array_map(function($item) use ($class_name) { - if (is_object($item)) { - return $class_name::buildFromEntity($item); - } - else { - return $item; - } - }, $field); - } - else { - return $field; - } - } - - /** - * Persist current state of the entity to the database. - */ - public function save() { - return $this->getWrappedEntity()->save(); - } - - /** - * Delete the current entity from the database. - */ - public function delete() { - if ($this->getEntityType() == 'node') { - node_delete($this->get('nid')); - } - else { - entity_delete($this->getEntityType(), $this->get('id')); - } - } - - // Magic methods to act as a decorator (have the same methods and properties as the object we are wrapping). - // Needs to be a reference so we can set nested properties and array values. - public function &__get($name) { - if (isset($this->entity->$name)) { - return $this->entity->$name; - } - } - - public function __set($name, $value) { - return $this->entity->$name = $value; - } - - public function __isset($name) { - return property_exists($this->entity, $name) && !empty($this->entity->$name); - } - - public function __unset($name) { - unset($this->entity->$name); - } - - public function __call($name, array $args) { - // Implement our default get and set. These should be overridden for special cases. - if (drupal_substr($name, 0, 4) == 'get_') { - return $this->get(drupal_substr($name, 4)); - } - elseif (drupal_substr($name, 0, 4) == 'set_') { - return $this->set(drupal_substr($name, 4), $args[0]); - } - elseif (method_exists($this->entity, $name)) { - // Implement decorator pattern by proxying methods to the wrapped entity. - return call_user_func_array(array($this->entity, $name), $args); - } - else { - throw new EntityDecoratorMethodNotFound(get_called_class() . " has no instance method called " . $name); - } - } - - // Magic methods to implement our default finders. These should be overridden for special cases. - public static function __callStatic($name, array $args) { - if (drupal_substr($name, 0, 8) == 'find_by_') { - $class = get_called_class(); - return $class::findBy(drupal_substr($name, 8), $args[0]); - } - - if (drupal_substr($name, 0, 14) == 'find_first_by_') { - $class = get_called_class(); - return $class::findFirstBy(drupal_substr($name, 14), $args[0]); - } - - throw new EntityDecoratorMethodNotFound(get_called_class() . " has no static method called " . $name); - } -} diff --git a/src/EntityDecorator/EntityDecorator.php b/src/EntityDecorator/EntityDecorator.php new file mode 100644 index 0000000..8f692e9 --- /dev/null +++ b/src/EntityDecorator/EntityDecorator.php @@ -0,0 +1,220 @@ +entity = $entity; + } + else { + if ($this->getEntityType() == 'node') { + // Node specific preparation. + $this->entity = new stdClass; + $this->entity->type = $this->getBundle(); + node_object_prepare($this->entity); + } + else { + $this->entity = entity_create($this->getEntityType(), array('type' => $this->getBundle())); + } + } + } + + /** + * Create an instance of the class from a raw entity object. + * @param $entity + * @return Instance of subclass of EntityDecorator + */ + static public function buildFromEntity($entity) { + $class = get_called_class(); + $object = new $class($entity); + return $object; + } + + /** + * Find an instance by ID. + * @param int $id + * @return Instance of subclass of EntityDecorator + */ + static public function find($id) { + $class = get_called_class(); + + if ($class::$entityType == 'node') { + $entity = $class::findBy('nid', array($id))->execute(); + } + else { + $entity = $class::findBy('id', array($id))->execute(); + } + + if (count($entity)) { + return current($entity); + } + } + + static protected function getFinder() { + $class = get_called_class(); + return new EntityDecoratorFinder($class, $class::$bundle, $class::$entityType); + } + + /** + * Find entities where the field_name or property has a matching value. + * @param string $field_name the field or property to match on + * @param array or scalar $value the value to match on + * @return EntityDecoratorFinder instance + */ + static public function findBy($field_name, $value) { + return self::getFinder()->findBy($field_name, $value); + } + + /** + * Find the first entity where the field_name or property has a matching value. + * @param string $field_name the field or property to match on + * @param array or scalar $value the value to match on + * @return EntityDecorator subclass instance + */ + static public function findFirstBy($field_name, $value) { + return self::getFinder()->findFirstBy($field_name, $value); + } + + private function getEntityType() { + $class = get_called_class(); + return $class::$entityType; + } + + private function getBundle() { + $class = get_called_class(); + return $class::$bundle; + } + + public function getWrappedEntity() { + return entity_metadata_wrapper($this->getEntityType(), $this->entity); + } + + /** + * Set a field or property of the extended entity. + * + * @param string $field The name of the field or property to set + * @param $value The value the field or property should have + */ + public function set($attr_name, $value) { + $this->getWrappedEntity()->$attr_name->set($value); + } + + /** + * Get a field or property of the extended entity. + * + * @param string $field The name of the field or property to get + */ + public function get($attr_name) { + return $this->getWrappedEntity()->$attr_name->value(); + } + + /** + * Get a field or property of the extended entity that is an object and return a decorated instance. + * Works with arrays of objects too (e.g like a field collection). + * If the field is not an object or an array of objects, it returns the raw value. + * + * @param string $field The name of the field or property to get + * @param string $class_name The name of the class to decorate it with + */ + public function getDecorated($attr_name, $class_name) { + $field = $this->get($attr_name); + + if (is_object($field)) { + return $class_name::buildFromEntity($field); + } + elseif (is_array($field)) { + return array_map(function($item) use ($class_name) { + if (is_object($item)) { + return $class_name::buildFromEntity($item); + } + else { + return $item; + } + }, $field); + } + else { + return $field; + } + } + + /** + * Persist current state of the entity to the database. + */ + public function save() { + return $this->getWrappedEntity()->save(); + } + + /** + * Delete the current entity from the database. + */ + public function delete() { + if ($this->getEntityType() == 'node') { + node_delete($this->get('nid')); + } + else { + entity_delete($this->getEntityType(), $this->get('id')); + } + } + + // Magic methods to act as a decorator (have the same methods and properties as the object we are wrapping). + // Needs to be a reference so we can set nested properties and array values. + public function &__get($name) { + if (isset($this->entity->$name)) { + return $this->entity->$name; + } + } + + public function __set($name, $value) { + return $this->entity->$name = $value; + } + + public function __isset($name) { + return property_exists($this->entity, $name) && !empty($this->entity->$name); + } + + public function __unset($name) { + unset($this->entity->$name); + } + + public function __call($name, array $args) { + // Implement our default get and set. These should be overridden for special cases. + if (drupal_substr($name, 0, 4) == 'get_') { + return $this->get(drupal_substr($name, 4)); + } + elseif (drupal_substr($name, 0, 4) == 'set_') { + return $this->set(drupal_substr($name, 4), $args[0]); + } + elseif (method_exists($this->entity, $name)) { + // Implement decorator pattern by proxying methods to the wrapped entity. + return call_user_func_array(array($this->entity, $name), $args); + } + else { + throw new EntityDecoratorMethodNotFound(get_called_class() . " has no instance method called " . $name); + } + } + + // Magic methods to implement our default finders. These should be overridden for special cases. + public static function __callStatic($name, array $args) { + if (drupal_substr($name, 0, 8) == 'find_by_') { + $class = get_called_class(); + return $class::findBy(drupal_substr($name, 8), $args[0]); + } + + if (drupal_substr($name, 0, 14) == 'find_first_by_') { + $class = get_called_class(); + return $class::findFirstBy(drupal_substr($name, 14), $args[0]); + } + + throw new EntityDecoratorMethodNotFound(get_called_class() . " has no static method called " . $name); + } +} diff --git a/src/EntityDecorator/EntityDecoratorException.php b/src/EntityDecorator/EntityDecoratorException.php new file mode 100644 index 0000000..fe4ad7a --- /dev/null +++ b/src/EntityDecorator/EntityDecoratorException.php @@ -0,0 +1,14 @@ +entityType = $entityType; + $this->class = $class; + $this->query = new EntityFieldQuery(); + $this->query->entityCondition('entity_type', $entityType) + ->entityCondition('bundle', $bundle); + } + + + /** + * Find entities where the field_name or property has a matching value. + * @param string $field_name the field or property to match on + * @param array or scalar $value the value to match on + * @throws \EntityDecoratorUnsupportedArgument + * @return EntityDecoratorFinder instance + */ + public function findBy($field_name, $value) { + if (is_array($value)) { + $operator = 'IN'; + } + elseif (is_scalar($value)) { + $operator = '='; + } + else { + throw new EntityDecoratorUnsupportedArgument('EntityDecoratorFinders can only take scalars and arrays as arguments'); + } + + if (count($value)) { + // Is the field a property? + if ($this->isProperty($field_name)) { + $this->query->propertyCondition($field_name, $value, $operator); + } + else { + $this->query->fieldCondition($field_name, 'value', $value, $operator); + } + } + + return $this; + } + + /** + * Order results by a field_name or property. + * @param string $field_name the field or property to order by + * @param string $asc_or_desc + * @internal param \EntityDecorator\or $array scalar $asc_or_desc + * @return EntityDecoratorFinder instance + */ + public function orderBy($field_name, $asc_or_desc = 'ASC') { + + // Is the field a property? + if ($this->isProperty($field_name)) { + $this->query->propertyOrderBy($field_name, $asc_or_desc); + } + else { + $this->query->fieldOrderBy($field_name, 'value', $asc_or_desc); + } + + return $this; + } + + /** + * Find the first entity where the field_name or property has a matching value. + * @param string $field_name the field or property to match on + * @param array or scalar $value the value to match on + * @return EntityDecorator subclass instance + */ + public function findFirstBy($field_name, $value) { + return $this->findBy($field_name, $value)->executePrivate(TRUE); + } + + /** + * Is the field name a property? + * @param string $field_name + * @return boolean + */ + protected function isProperty($field_name) { + $property_info = entity_get_property_info($this->entityType); + return in_array($field_name, array_keys($property_info['properties'])); + } + + /** + * Private execute method. We don't expose this as it has different return types based on its parameters. + * @param boolean $first_result_only Are we only interested in the first results? + * @return array of EntityDecorator subclass instances or a single instance + */ + private function executePrivate($first_result_only = FALSE) { + $class = $this->class; + + $result = $this->query->execute(); + + if (isset($result[$this->entityType])) { + + if ($first_result_only) { + $entity_id = current(array_keys($result[$this->entityType])); + $entity = entity_load_single($this->entityType, $entity_id); + return $class::buildFromEntity($entity); + } + + $entities = entity_load($this->entityType, array_keys($result[$this->entityType])); + + return array_map(function($entity) use ($class) { + return $class::buildFromEntity($entity); + }, $entities); + } + + // Fall through return empty array or NULL by default. + if ($first_result_only) { + return NULL; + } + else { + return array(); + } + } + + /** + * Public execute method. Only needs to be called when finding multiple results. + * @return array of EntityDecorator subclass instances + */ + public function execute() { + return $this->executePrivate(FALSE); + } + + /** + * Implements the metaprogramming finders. + */ + public function __call($name, array $args) { + if (drupal_substr($name, 0, 7) == 'find_by_') { + return $this->findBy(drupal_substr($name, 8), $args[0]); + } + + if (drupal_substr($name, 0, 14) == 'find_first_by_') { + return $this->findFirstBy(drupal_substr($name, 14), $args[0]); + } + + // Todo make custom exception. + throw new \Exception(get_called_class() . " has not no method called " . $name); + } +} diff --git a/src/EntityDecorator/EntityDecoratorMethodNotFound.php b/src/EntityDecorator/EntityDecoratorMethodNotFound.php new file mode 100644 index 0000000..c0e3b21 --- /dev/null +++ b/src/EntityDecorator/EntityDecoratorMethodNotFound.php @@ -0,0 +1,5 @@ +