The key to controlling complexity is a good domain model
— Martin Fowler, Domain-Driven Design
A model is a simplification. It is an interpretation of reality that abstracts the aspects relevant to solving the problem at hand and ignores extraneous detail.
— Eric Evans, Domain-Driven Design
class Role extends ConfigEntityBase implements RoleInterface { }
function user_role_change_permissions($rid, array $permissions = array()) { // Grant new permissions for the role. $grant = array_filter($permissions); if (!empty($grant)) { user_role_grant_permissions($rid, array_keys($grant)); } // Revoke permissions for the role. $revoke = array_diff_assoc($permissions, $grant); if (!empty($revoke)) { user_role_revoke_permissions($rid, array_keys($revoke)); } }
class Role extends ConfigEntityBase implements RoleInterface { public function changePermissions($rid, array $permissions = array()) { // Grant new permissions for the role. $grant = array_filter($permissions); if (!empty($grant)) { $this->grantPermissions($rid, array_keys($grant)); } // Revoke permissions for the role. $revoke = array_diff_assoc($permissions, $grant); if (!empty($revoke)) { $this->revokePermissions($rid, array_keys($revoke)); } }
interface CommentInterface extends ContentEntityInterface { }
class Comment extends EntityNG implements CommentInterface { public $cid; public $pid; public $nid; public $node_type; public $langcode; public $subject; // The comment author ID. public $uid; // The comment author's name. public $name; // The comment author's e-mail address. public $mail; // The comment author's home page address. public $homepage; // The comment author's hostname. public $hostname; public $created; // etc }
class Comment extends EntityNG implements CommentInterface { // The comment author ID. public $uid; // The comment author's name. public $name; // The comment author's e-mail address. public $mail; // The comment author's home page address. public $homepage; // The comment author's hostname. public $hostname;
class Comment extends EntityNG implements CommentInterface { protected $uid; protected $name; protected $mail; protected $homepage; protected $hostname; public function authorId() {} public function authorName() {} public function authorEmail() {} public function authorHomepage() {} public function authorHostname() {}
// POPO class CommentAuthor { public function id() {} public function name() {} public function email() {} public function homepage() {} } class Comment extends EntityNG implements CommentInterface { // Instance of CommentAuthor. protected $author; public function author() { return $this->author; }
function comment_prepare_author(Comment $comment) { $account = $comment->uid->entity; if (!$account) { $account = entity_create('user', array( 'uid' => 0, 'name' => $comment->name->value, 'homepage' => $comment->homepage->value) ); } return $account; }
class Comment extends EntityNG implements CommentInterface { public function prepareAuthor() { $account = $this->uid->entity; if (!$account) { $account = new AnonymousCommentAuthor(array( 'name' => $this->name->value, 'homepage' => $this->homepage->value) ); } $this->author = $account; }
function comment_publish_action(Comment $comment) { $subject = $comment->subject->value; $comment->status->value = COMMENT_PUBLISHED; watchdog('action', 'Published comment %subject.', array('%subject' => $subject)); }
function comment_publish_action(Comment $comment) { // Knows how to find "subject" $subject = $comment->subject->value; // Knows what constant to use for it's status, sets directly $comment->status->value = COMMENT_PUBLISHED; watchdog('action', 'Published comment %subject.', array('%subject' => $subject)); }
function comment_publish_action(CommentInterface $comment) { $comment->publish(); watchdog('action', 'Published comment %subject.', array('%subject' => $comment->subject())); }
// Another instance if ($comment->status->value == COMMENT_NOT_PUBLISHED) { $links['comment-approve'] = array( ... // Should be if (!$comment->isPublished()) { $links['comment-approve'] = array( ...
interface PublishableInterface { public function publish(); public function unPublish(); public function isPublished(); } interface CommentInterface extends PublishableInterface {} interface NodeInterface extends PublishableInterface {}
In addition to the increased implementation complexity of each component, the separation immediately robs an object model of cohesion. One of the most fundamental concepts of objects is to encapsulate data with the logic that operates on that data.
— Eric Evans, Domain-Driven Design
(not the Drupal variety)
SERVICES should be used judiciously and not allowed to strip the ENTITIES and VALUE OBJECTS of all their behavior.
...the more common mistake is to give up too easily on fitting the behavior into an appropriate object, gradually slipping toward procedural programming.
– Eric Evans Eric, Domain-Driven Design
In general, the more behavior you find in the services, the more likely you are to be robbing yourself of the benefits of a domain model. If all your logic is in services, you've robbed yourself blind.
– Martin Fowler, AnemicDomainModel
class AliasManager { // Given a path alias, return the internal path it represents. public function getSystemPath($path, $path_language = NULL){} // Given an internal Drupal path, return the alias public function getPathAlias($path, $path_language = NULL){} // Returns an array of system paths that have been looked up. public function getPathLookups(){} // Preload a set of paths for bulk alias lookups. public function preloadPathLookups(array $path_list){} public function cacheClear($source = NULL){} // Given a Drupal system URL return one of its aliases protected function lookupPathAlias($path, $langcode){} // Given an alias, return its Drupal system URL if one exists. protected function lookupPathSource($path, $langcode){} protected function pathAliasWhitelistRebuild($source = NULL){} }
public function getSystemPath($path, $path_language = NULL) { $path_language = $path_language ?: $this->languageManager ->getLanguage(LANGUAGE_TYPE_URL) ->langcode; if ($source = $this->lookupPathSource($path, $path_language)) { $path = $source; } return $path; }
public function getSystemPath($path, $path_language = NULL) { $langcode = $path_language ?: $this->defaultLanguage(); // Contents of lookupPathAlias } protected function defaultLanguage() { return $this->languageManager ->getLanguage(LANGUAGE_TYPE_URL) ->langcode; }
class AliasManager { public function findPathByAlias($alias, $path_language = NULL){} public function findAliasByPath($path, $path_language = NULL){} public function getPathLookups(){} public function preloadPathLookups(array $path_list){} public function cacheClear($source = NULL){} protected function pathAliasWhitelistRebuild($source = NULL){} protected function defaultLanguage(){} }
class AliasManager { public function findPathByAlias($alias, $path_language = NULL){} public function findAliasByPath($path, $path_language = NULL){} public function cacheClear($source = NULL){} protected function pathAliasWhitelistRebuild($source = NULL){} protected function defaultLanguage(){} }
class AliasManager { public function findPathByAlias($alias, $path_language = NULL){} public function findAliasByPath($path, $path_language = NULL){} protected function pathAliasWhitelistRebuild($source = NULL){} protected function defaultLanguage(){} }
class AliasManager { public function findPathByAlias($alias, $path_language = NULL){} public function findAliasByPath($path, $path_language = NULL){} protected function defaultLanguage(){} }
namespace Drupal\Core\Path; use Drupal\Core\Database\Database; use Drupal\Core\Database\Connection; /** * Defines a class for CRUD operations on path aliases. */ class Path { public function __construct(Connection $connection, AliasManager $alias_manager){} public function save($source, $alias, $langcode = LANGUAGE_NOT_SPECIFIED, $pid = NULL){} public function load($conditions){} public function delete($conditions){} }
namespace Drupal\Core\Path; class AliasManager { public function findPathByAlias($alias, $path_language = NULL){} public function findAliasByPath($path, $path_language = NULL){} public function findWhere($conditions){} public function deleteWhere($conditions){} public function save($path, $alias, $langcode = LANGUAGE_NOT_SPECIFIED, $pid = NULL){} protected function defaultLanguage(); }
\Drupal\user\Plugin\Core\Entity\User
\Drupal\user\EntityType\User
\Drupal\user\User
namespace Drupal\custom_block\Controller; class CustomBlockController implements ControllerInterface { public function add() { // some code $types = $this->entityManager ->getStorageController('custom_block_type') ->load();
namespace Drupal\custom_block\Controller; class CustomBlockController implements ControllerInterface { public function add() { // some code $types = CustomBlock::findAll();
namespace Drupal\custom_block\Controller; class CustomBlockController implements ControllerInterface { public function add() { // some code $types = $this->customBlockRepository->findAll();
class AggregatorController implements ControllerInterface { public function adminOverview() { $result = $this->database->query('SELECT f.fid, ...'); $header = array(t('Title'), t('Items'); // etc… $rows = array(); foreach ($result as $feed) { $row = array();
$result = $this->database->query('SELECT f.fid, ...');
class AggregatorController implements ControllerInterface { public function adminOverview() { $feeds = new AggregatorFeeds(); foreach ($feeds->findAll() as $feed) { $row = array();
class AggregatorController implements ControllerInterface { public function adminOverview() { foreach ($this->feeds()->findAll() as $feed) { $row = array(); // more stuff } protected function feeds() { return $this->feeds ?: new AggregatorFeeds(); }
namespace Drupal\Tests\Core\TypedData\Type; use Drupal\Core\TypedData\Type\Map; use Drupal\Tests\UnitTestCase; class MapTest extends UnitTestCase { public function testIteration() { $value = array('one' => 'eins', 'two' => 'zwei'); $this->map = new Map(array('type' => 'map'), $value); $count = 0; foreach ($this->map as $item) { $this->assertTrue($item instanceof \Drupal\Core\TypedData\TypedDataInterface); $count++; } $this->assertEquals(2, $count); }
1) Drupal\Tests\Core\TypedData\Type\MapTest::testIteration Failed asserting that 0 matches expected 2.
namespace Drupal\Tests\Core\TypedData\Type; use Drupal\Core\TypedData\Type\Map; use Drupal\Tests\UnitTestCase; class MapTest extends UnitTestCase { public function testIteration() { $value = array('one' => 'eins', 'two' => 'zwei'); $this->map = new Map(array('type' => 'map'), $value); $this->map->setValue($value, FALSE); $count = 0; foreach ($this->map as $item) { $this->assertTrue($item instanceof \Drupal\Core\TypedData\TypedDataInterface); $count++; } $this->assertEquals(2, $count); }
2. Fatal error: Call to undefined function cache() in core/lib/Drupal/Core/Plugin/Discovery/CacheDecorator.php on line 135
3. Fatal error: Call to undefined function module_implements() in core/lib/Drupal/Core/Plugin/Discovery/HookDiscovery.php on line 48
4. Fatal error: Call to undefined function format_string() in core/lib/Drupal/Core/TypedData/TypedDataFactory.php on line 44
throw new InvalidArgumentException( sprintf('Invalid data type %s has been given.', $plugin_id) );
5. PHP Fatal error: Call to undefined function typed_data() in core/lib/Drupal/Core/TypedData/Type/Map.php on line 119
- $this->properties[$property_name] = typed_data()-> - getPropertyInstance($this, $property_name, $value); + $definition = $this->getPropertyDefinition($property_name); + $manager = new \Drupal\Core\TypedData\TypedDataManager; + $this->properties[$property_name] = $manager->create( + $definition, NULL, $property_name, $this + );
class Entity { public function save() { return \Drupal::entityManager() ->getStorageController($this->entityType)->save($this); } }
class Entity { public function save() { return $this->getStorage->save($this); } protected function getStorage() { if (!isset($this->storage)) { $this->storage = \Drupal::entityManager() ->getStorageController($this->entityType); } return $this->storage; } }
class RouteProviderTest extends UnitTestBase { /** * Confirms that the correct candidate outlines are generated. */ public function testCandidateOutlines() { $connection = Database::getConnection(); // <-- WTF $provider = new RouteProvider($connection);