diff --git a/config_entity_example/config/config_entity_example.robot.marvin.yml b/config_entity_example/config/config_entity_example.robot.marvin.yml new file mode 100644 index 0000000..eceaf86 --- /dev/null +++ b/config_entity_example/config/config_entity_example.robot.marvin.yml @@ -0,0 +1,12 @@ +# This file defines a default config entity. This allows the module to include +# config entities that are present 'out of the box'. +# These can be edited by the user. The modified version is saved to the site +# config folder, which this original copy remains untouched. + +# For this system to work, we have to define our config schema in +# config/config_entity_example.robot.marvin.yml. + +# The id of the config entity. +id: marvin +# Our properties follow. +label: 'Marvin, the paranoid android' diff --git a/config_entity_example/config/schema/config_entity_example.schema.yml b/config_entity_example/config/schema/config_entity_example.schema.yml new file mode 100644 index 0000000..38fcdce --- /dev/null +++ b/config_entity_example/config/schema/config_entity_example.schema.yml @@ -0,0 +1,19 @@ +# Schema for the configuration files of the Config Entity Example module. + +# This schema tells the config system how to read our config YML files. +# See for example the file config/config_entity_example.robot.marvin.yml, which +# contains our default config entity. + +config_entity_example.robot.*: + type: mapping + label: 'Robot' + mapping: + id: + type: string + label: 'Robot id' + uuid: + type: string + label: 'UUID' + label: + type: label + label: 'Label' diff --git a/config_entity_example/config_entity_example.info.yml b/config_entity_example/config_entity_example.info.yml new file mode 100644 index 0000000..adea887 --- /dev/null +++ b/config_entity_example/config_entity_example.info.yml @@ -0,0 +1,5 @@ +name: 'Config entity example' +type: module +description: 'An example module showing how to create a config entity type.' +package: Example modules +core: 8.x diff --git a/config_entity_example/config_entity_example.local_actions.yml b/config_entity_example/config_entity_example.local_actions.yml new file mode 100644 index 0000000..b8d8f5b --- /dev/null +++ b/config_entity_example/config_entity_example.local_actions.yml @@ -0,0 +1,15 @@ +# Add some local task links to facilitate navigation. + +config_entity_example_add_action: + route_name: robot.add + title: 'Add robot' + appears_on: + - robot.list + +config_entity_example_list_action: + route_name: robot.list + title: 'List Robots' + appears_on: + - robot.add + - robot.edit + - robot.delete diff --git a/config_entity_example/config_entity_example.module b/config_entity_example/config_entity_example.module new file mode 100644 index 0000000..a1d67b5 --- /dev/null +++ b/config_entity_example/config_entity_example.module @@ -0,0 +1,73 @@ + array( + 'title' => t('Administer robots'), + 'description' => t('Create and edit robots.'), + ), + ); + + return $permissions; +} + +/** + * Implements hook_menu_link_defaults(). + * + * We'll just make a link to the list of robots. + * + * @return array + * An associative array representing the menu items for this module. + */ +function config_entity_example_menu_link_defaults() { + $links['config_entity_example_menu'] = array( + 'link_title' => 'Config Entity Example', + 'route_name' => 'robot.list', + ); + return $links; +} + +/** + * @} End of "defgroup config_entity_example". + */ diff --git a/config_entity_example/config_entity_example.routing.yml b/config_entity_example/config_entity_example.routing.yml new file mode 100644 index 0000000..d72e899 --- /dev/null +++ b/config_entity_example/config_entity_example.routing.yml @@ -0,0 +1,56 @@ +# The routing.yml file defines the paths for our module. +# Here we define the paths for our entity type's admin UI. + +# This is the router item for listing all entities. +robot.list: + path: '/examples/config_entity_example' + defaults: + # '_entity_list' tells Drupal to use an entity list controller. + # We give the entity ID here. Drupal then looks in the entity's annotation + # and looks for the "list" entry under "controllers" for the class to load. + # @see \Drupal\Core\Entity\Enhancer\EntityRouteEnhancer + _entity_list: 'robot' + _title: 'Config Entity Example' + requirements: + _permission: 'administer robots' + +# This is the router item for adding our entity. +robot.add: + path: '/examples/config_entity_example/add' + defaults: + _title: 'Add robot' + # Like _entity_list above, _entity_form gives the entity type ID, only this + # time also lists the form separated by a period. Drupal looks in the + # annotation for the entity and locates the "add" entry under "form" for + # the form class to load. + # @see \Drupal\Core\Entity\Enhancer\EntityRouteEnhancer + _entity_form: robot.add + requirements: + _entity_create_access: robot + +# This is the router item for editing our entity. +robot.edit: + # Parameters may be passed to the form via the URL path. We name the + # parameter in the path by enclosing it in curly braces. For entity forms, + # we include the entity ID in the path by including a parameter with the + # same name as the entity type ID. + path: '/examples/config_entity_example/manage/{robot}' + defaults: + _title: 'Edit robot' + # List our add entry above, this _entity_form entry instructs Drupal to + # read our entity type's annonation, and look for the "edit" entry under + # "form". + _entity_form: robot.edit + requirements: + # This uses our entity access controller. + # @see \Drupal\Core\Entity\EntityAccessCheck + _entity_access: robot.update + +# This is the router item for deleting an instance of our entity. +robot.delete: + path: '/examples/config_entity_example/manage/{robot}/delete' + defaults: + _title: 'Delete robot' + _entity_form: robot.delete + requirements: + _entity_access: robot.delete diff --git a/config_entity_example/lib/Drupal/config_entity_example/Controller/RobotListBuilder.php b/config_entity_example/lib/Drupal/config_entity_example/Controller/RobotListBuilder.php new file mode 100644 index 0000000..76b3193 --- /dev/null +++ b/config_entity_example/lib/Drupal/config_entity_example/Controller/RobotListBuilder.php @@ -0,0 +1,92 @@ +t('Robot'); + $header['machine_name'] = $this->t('Machine Name'); + return $header + parent::buildHeader(); + } + + /** + * Builds a row for an entity in the entity listing. + * + * @param EntityInterface $entity + * The entity for which to build the row. + * + * @return array + * A render array of the table row for displaying the entity. + * + * @see Drupal\Core\Entity\EntityListController::render() + */ + public function buildRow(EntityInterface $entity) { + $row['label'] = $this->getLabel($entity); + + $row['machine_name'] = $entity->id(); + + return $row + parent::buildRow($entity); + } + + /** + * Adds some descriptive text to our entity list. + * + * Typically, there's no need to override render(). You may wish to do so, + * however, if you want to add markup before or after the table. + * + * @return array + * Renderable array. + */ + public function render() { + $build['description'] = array( + '#markup' => $this->t("

The Config Entity Example module defines a" + . " Robot entity type. This is a list of the Robot entities currently" + . " in your Drupal site.

By default, when you enable this" + . " module, one entity is created from configuration. This is why we" + . " call them Config Entities. Marvin, the paranoid android, is created" + . " in the database when the module is enabled.

You can view a" + . " list of Robots here. You can also use the 'Operations' column to" + . " edit and delete Robots.

"), + ); + $build[] = parent::render(); + return $build; + } + +} diff --git a/config_entity_example/lib/Drupal/config_entity_example/Entity/Robot.php b/config_entity_example/lib/Drupal/config_entity_example/Entity/Robot.php new file mode 100644 index 0000000..8c614c8 --- /dev/null +++ b/config_entity_example/lib/Drupal/config_entity_example/Entity/Robot.php @@ -0,0 +1,89 @@ +t('Create Robot'); + return $actions; + } + +} diff --git a/config_entity_example/lib/Drupal/config_entity_example/Form/RobotDeleteForm.php b/config_entity_example/lib/Drupal/config_entity_example/Form/RobotDeleteForm.php new file mode 100644 index 0000000..0df892d --- /dev/null +++ b/config_entity_example/lib/Drupal/config_entity_example/Form/RobotDeleteForm.php @@ -0,0 +1,97 @@ +t('Are you sure you want to delete robot %label?', array( + '%label' => $this->entity->label() + )); + } + + /** + * Gather the confirmation text. + * + * The confirm text is used as a the text in the button that confirms the + * question posed by getQuestion(). + * + * @return string + * Translated string. + */ + public function getConfirmText() { + return $this->t('Delete Robot'); + } + + /** + * Gets the cancel route. + * + * Provides the route name to go to if the user cancels the action. For entity + * delete forms, this is typically the route that points at the list + * controller. + * + * @return array + * The route to go to if the user cancels the deletion. The key is + * 'route_name'. The value is the route name. + */ + public function getCancelRoute() { + return array( + 'route_name' => 'robot.list', + ); + } + + /** + * The submit handler for the confirm form. + * + * For entity delete forms, you use this to delete the entity in + * $this->entity. + * + * @param array $form + * An associative array containing the structure of the form. + * @param array $form_state + * An associative array containing the current state of the form. + */ + public function submit(array $form, array &$form_state) { + // Delete the entity. + $this->entity->delete(); + + // Set a message that the entity was deleted. + drupal_set_message(t('Robot %label was deleted.', array( + '%label' => $this->entity->label(), + ))); + + // Redirect the user to the list controller when complete. + $form_state['redirect_route']['route_name'] = 'robot.list'; + } + +} diff --git a/config_entity_example/lib/Drupal/config_entity_example/Form/RobotEditForm.php b/config_entity_example/lib/Drupal/config_entity_example/Form/RobotEditForm.php new file mode 100644 index 0000000..c866153 --- /dev/null +++ b/config_entity_example/lib/Drupal/config_entity_example/Form/RobotEditForm.php @@ -0,0 +1,40 @@ +entityQueryFactory = $query_factory; + } + + /** + * Factory method for RobotFormBase. + * + * When Drupal builds this class it does not call the constructor directly. + * Instead, it relies on this method to build the new object. Why? The class + * constructor may take multiple arguments that are unknown to Drupal. The + * create() method always takes one parameter -- the container. The purpose + * of the create() method is twofold: It provides a standard way for Drupal + * to construct the object, meanwhile it provides you a place to get needed + * constructor parameters from the container. + * + * In this case, we ask the container for an entity query factory. We then + * pass the factory to our class as a constructor parameter. + * + */ + public static function create(ContainerInterface $container) { + return new static($container->get('entity.query')); + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::form(). + * + * Builds the entity add/edit form. + * + * @param array $form + * An associative array containing the structure of the form. + * @param array $form_state + * An associative array containing the current state of the form. + * + * @return array + * An associative array containing the robot add/edit form. + */ + public function buildForm(array $form, array &$form_state) { + // Get anything we need form the base class. + $form = parent::buildForm($form, $form_state); + + // Drupal provides the entity to us as a class variable. If this is an + // existing entity, it will be populated with existing values as class + // variables. If this is a new entity, it will be a new object with the + // class of our entity. Drupal knows which class to call from the + // annotation on our Robot class. + $robot = $this->entity; + + // Build the form. + $form['label'] = array( + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => $robot->label(), + '#required' => TRUE, + ); + $form['id'] = array( + '#type' => 'machine_name', + '#title' => $this->t('Machine name'), + '#default_value' => $robot->id(), + '#machine_name' => array( + 'exists' => array($this, 'exists'), + 'replace_pattern' =>'([^a-z0-9_]+)|(^custom$)', + 'error' => 'The machine-readable name must be unique, and can only contain lowercase letters, numbers, and underscores. Additionally, it can not be the reserved word "custom".', + ), + '#disabled' => !$robot->isNew(), + ); + + // Return the form. + return $form; + } + + /** + * Checks for an existing robot. + * + * @param string|int $entity_id + * The entity ID. + * @param array $element + * The form element. + * @param array $form_state + * The form state. + * + * @return bool + * TRUE if this format already exists, FALSE otherwise. + */ + public function exists($entity_id, array $element, array $form_state) { + // Use the query factory to build a new robot entity query. + $query = $this->entityQueryFactory->get('robot'); + + // Query the entity ID to see if its in use. + $result = $query->condition('id', $element['#field_prefix'] . $entity_id) + ->execute(); + + // We don't need to return the ID, only if it exists or not. + return (bool) $result; + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::actions(). + * + * To set the submit button text, we need to override actions(). + * + * @param array $form + * An associative array containing the structure of the form. + * @param array $form_state + * An associative array containing the current state of the form. + * + * @return array + * An array of supported actions for the current entity form. + */ + protected function actions(array $form, array &$form_state) { + // Get the basic actins from the base class. + $actions = parent::actions($form, $form_state); + + // Change the submit button text. + $actions['submit']['#value'] = $this->t('Save'); + + // Return the result. + return $actions; + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::validate(). + * + * @param array $form + * An associative array containing the structure of the form. + * @param array $form_state + * An associative array containing the current state of the form. + */ + public function validate(array $form, array &$form_state) { + parent::validate($form, $form_state); + + // Add code here to validate your config entity's form elements. + // Nothing to do here. + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::save(). + * + * Saves the entity. This is called after submit() has built the entity from + * the form values. Do not override submit() as save() is the preferred + * method for entity form controllers. + * + * @param array $form + * An associative array containing the structure of the form. + * @param array $form_state + * An associative array containing the current state of the form. + */ + public function save(array $form, array &$form_state) { + // Get the entity from the class variable. We don't need to do this, but + // it often makes the code easier to read. + $robot = $this->entity; + + // Drupal already populated the form values in the entity object. Each + // form field was saved as a public variable in the entity class. PHP + // allows Drupal to do this even if the method is not defined ahead of + // time. + $status = $robot->save(); + + // Grab the URL of the new entity. We'll use it in the message. + $uri = $robot->url(); + + if ($status == SAVED_UPDATED) { + // If we edited an existing entity... + drupal_set_message($this->t('Robot %label has been updated.', array('%label' => $robot->label()))); + watchdog('contact', 'Robot %label has been updated.', array('%label' => $robot->label()), WATCHDOG_NOTICE, l($this->t('Edit'), $uri . '/edit')); + } + else { + // If we created a new entity... + drupal_set_message($this->t('Robot %label has been added.', array('%label' => $robot->label()))); + watchdog('contact', 'Robot %label has been added.', array('%label' => $robot->label()), WATCHDOG_NOTICE, l($this->t('Edit'), $uri . '/edit')); + } + + // Redirect the user to the following path after the save optionation. + $form_state['route_redirect'] = new Url('robot.list'); + } + +} diff --git a/config_entity_example/lib/Drupal/config_entity_example/RobotAccessController.php b/config_entity_example/lib/Drupal/config_entity_example/RobotAccessController.php new file mode 100644 index 0000000..501ba05 --- /dev/null +++ b/config_entity_example/lib/Drupal/config_entity_example/RobotAccessController.php @@ -0,0 +1,39 @@ + 'Config Entity example functionality', + 'description' => 'Test the Config Entity Example module.', + 'group' => 'Examples', + ); + } + + /** + * Various functional test of the Config Entity Example module. + * + * 1) Verify that the Marvin entity was created when the module was installed. + * + * 2) Verify that permissions are applied to the various defined paths. + * + * 3) Verify that we can manage entities through the user interface. + */ + public function testConfigEntityExample() { + // 1) Verify that the Marvin entity was created when the module was + // installed. + $entity = entity_load('robot', 'marvin'); + $this->assertNotNull($entity, 'Marvin was created during installation.'); + + // 2) Verify that permissions are applied to the various defined paths. + // Define some paths. Since the Marvin entity is defined, we can use it + // in our management paths. + $forbidden_paths = array( + 'examples/config_entity_example', + 'examples/config_entity_example/add', + 'examples/config_entity_example/manage/marvin', + 'examples/config_entity_example/manage/marvin/delete', + ); + // Check each of the paths to make sure we don't have access. At this point + // we haven't logged in any users, so the client is anonymous. + foreach($forbidden_paths as $path) { + $this->drupalGet($path); + $this->assertResponse(403, "Access denied to anonymous for path: $path"); + } + + // Create a user with no permissions. + $noperms_user = $this->drupalCreateUser(); + $this->drupalLogin($noperms_user); + // Should be the same result for forbidden paths, since the user needs + // special permissions for these paths. + foreach($forbidden_paths as $path) { + $this->drupalGet($path); + $this->assertResponse(403, "Access denied to generic user for path: $path"); + } + + // Create a user who can administer robots. + $admin_user = $this->drupalCreateUser(array('administer robots')); + $this->drupalLogin($admin_user); + // Forbidden paths aren't forbidden any more. + foreach($forbidden_paths as $unforbidden) { + $this->drupalGet($unforbidden); + $this->assertResponse(200, "Access granted to admin user for path: $unforbidden"); + } + + // 3) Verify that we can manage entities through the user interface. + // We still have the admin user logged in, so we'll create, update, and + // delete an entity. + // Go to the list page. + $this->drupalGet('examples/config_entity_example'); + $this->clickLink('Add robot'); + $robot_machine_name = 'roboname'; + $this->drupalPostForm( + NULL, + array( + 'label' => $robot_machine_name, + 'id' => $robot_machine_name, + ), + t('Create Robot') + ); + // @todo: More Tests. + } + +}