Table of Contents for
Drupal 8 Module Development

Version ebook / Retour

Cover image for bash Cookbook, 2nd Edition Drupal 8 Module Development by Daniel Sipos Published by Packt Publishing, 2017
  1. Drupal 8 Module Development
  2. Title Page
  3. Copyright
  4. Drupal 8 Module Development
  5. Credits
  6. About the Author
  7. About the Reviewers
  8. www.PacktPub.com
  9. Why subscribe?
  10. Customer Feedback
  11. Table of Contents
  12. Preface
  13. What this book covers
  14. What you need for this book
  15. Who this book is for
  16. Conventions
  17. Reader feedback
  18. Customer support
  19. Downloading the example code
  20. Downloading the color images of this book 
  21. Errata
  22. Piracy
  23. Questions
  24. Developing for Drupal 8
  25. Introducing Drupal (for developers)
  26. Developing for Drupal 8
  27. Technologies that drive Drupal
  28. PHP
  29. Databases and MySQL
  30. The web server
  31. HTML, CSS, and JavaScript
  32. Drupal architecture
  33. Drupal core, modules, and themes
  34. Hooks, plugins, and events
  35. Services and the dependency injection container
  36. From request to response
  37. Drupal's major subsystems
  38. Routing
  39. Entities
  40. Fields
  41. Menus
  42. Views
  43. Forms
  44. Configuration
  45. Plugins
  46. The theme system
  47. Caching
  48. Other subsystems
  49. Tools for developing in Drupal
  50. Version control
  51. Composer
  52. The API site and coding standards
  53. The developer (Devel) module
  54. Drush (the Drupal shell)
  55. Drupal Console
  56. Developer settings
  57. Summary
  58. Creating Your First Module
  59. Creating a module
  60. Your first hook implementation
  61. Route and controller
  62. The route
  63. Route variables
  64. Namespaces
  65. The Controller
  66. Services
  67. What is a service?
  68. The HelloWorldSalutation service
  69. Tagged services
  70. Using services in Drupal 8
  71. Injecting the service into our Controller
  72. The form
  73. Altering forms
  74. Custom submit handlers
  75. Rendering forms
  76. Service dependencies
  77. Blocks
  78. Our first block plugin
  79. Block configuration
  80. Working with links
  81. The URL
  82. The link
  83. Which way to link?
  84. Event Dispatcher and redirects
  85. Redirecting from a Controller
  86. Redirecting from a subscriber
  87. Dispatching events
  88. Summary
  89. Logging and Mailing
  90. Logging
  91. The Drupal 8 logging theory
  92. Our own logger channel
  93. Our own logger
  94. Logging for Hello World
  95. Logging summary
  96. Mail API
  97. The theory of the Mail API
  98. Implementing hook_mail()
  99. Sending emails
  100. Altering someone else's emails
  101. Custom mail plugins
  102. The mail plugin
  103. Using mail plugins
  104. Tokens
  105. The Token API
  106. Using tokens
  107. Defining new tokens
  108. Token summary
  109. Summary
  110. Theming
  111. Business logic versus presentation logic
  112. Twig
  113. Theme hooks
  114. Theme hook suggestions
  115. Render arrays
  116. The structure of a render array
  117. #type
  118. #theme
  119. #markup
  120. The render pipeline
  121. Assets and libraries
  122. Libraries
  123. Attaching libraries
  124. Common theme hooks
  125. Lists
  126. Links
  127. Tables
  128. Attributes
  129. Theming our Hello World module
  130. Summary
  131. Menus and Menu Links
  132. The menu system
  133. Menus
  134. Menu links
  135. Multiple types of menu links
  136. Local tasks
  137. Local actions
  138. Contextual links
  139. MenuLink trees
  140. Menu link tree manipulators
  141. Menu active trail
  142. Rendering menus
  143. Working with menu links
  144. Defining menu links
  145. Working with menu links
  146. Defining local tasks
  147. Defining local actions
  148. Defining contextual links
  149. Summary
  150. Data Modeling and Storage
  151. Different types of data storage
  152. State API
  153. Tempstore
  154. PrivateTempStore
  155. A note about anonymous users
  156. SharedTempStore
  157. Tempstore conclusion
  158. UserData
  159. Configuration
  160. Introduction
  161. What is configuration used for?
  162. Managing configuration
  163. Different types of configuration
  164. Configuration storage
  165. Schema
  166. Overrides
  167. Global overrides
  168. Module overrides
  169. Language overrides
  170. Priority
  171. Interacting with simple configuration
  172. Entities
  173. Content versus configuration entity types
  174. Entity type plugins
  175. Identifiers
  176. Bundles
  177. Database tables
  178. Entity keys
  179. Links
  180. Entity translation
  181. Entity revisions
  182. Configuration export
  183. Handlers
  184. Fields
  185. Configuration entity fields
  186. Content entity fields
  187. Base fields
  188. Configurable fields
  189. Field storage
  190. Entity types summary
  191. TypedData
  192. Why?
  193. What?
  194. The low-level API
  195. DataType plugins
  196. Data definitions
  197. Content entities
  198. TypedData summary
  199. Interacting with the Entity API
  200. Querying and loading entities
  201. Building queries
  202. Loading entities
  203. Reading entities
  204. Manipulating entities
  205. Creating entities
  206. Rendering content entities
  207. Pseudo-fields
  208. Content entity validation
  209. Validation summary
  210. Summary
  211. Your Own Custom Entity and Plugin Types
  212. Custom content entity type
  213. Custom plugin type
  214. Custom configuration entity type
  215. The Importer plugin
  216. Content entity bundles
  217. Drush command
  218. Summary
  219. The Database API
  220. The Schema API
  221. Running queries
  222. Select queries
  223. Handling the result
  224. More complex select queries
  225. Range queries
  226. Pagers
  227. Insert queries
  228. Update queries
  229. Delete queries
  230. Transactions
  231. Query alters
  232. Update hooks
  233. Summary
  234. Custom Fields
  235. Field type
  236. Field widget
  237. Field formatter
  238. Field settings
  239. Using as a base field
  240. Summary
  241. Access Control
  242. Introduction to the Drupal access system
  243. Roles and permissions under the hood
  244. Defining permissions
  245. Checking the user credentials
  246. Route access
  247. Custom route access
  248. Static approach
  249. Service approach
  250. Programmatically checking access on routes
  251. Bonus - dynamic route options for access control
  252. CSRF protection on routes
  253. Altering routes
  254. Entity access
  255. Injecting services into Entity handlers
  256. Entity access hooks
  257. Field access
  258. Entity access in routes
  259. Node access grants
  260. Block access
  261. Summary
  262. Caching
  263. Introduction
  264. Cacheability metadata
  265. Cache tags
  266. Cache contexts
  267. Max-age
  268. Using the cache metadata
  269. Caching in block plugins
  270. Caching access results
  271. Placeholders and lazy building
  272. Lazy builders
  273. Using the Cache API
  274. Creating our own cache bin
  275. Summary
  276. JavaScript and the Ajax API
  277. JavaScript in Drupal
  278. Drupal behaviors
  279. Our library
  280. The JavaScript
  281. Drupal settings
  282. Ajax API
  283. Ajax links
  284. Ajax in forms
  285. States (Form) system
  286. Summary
  287. Internationalization and Languages
  288. Introduction
  289. Language
  290. Content Translation
  291. Configuration Translation
  292. Interface Translation
  293. Internationalization
  294. Content entities and the Translation API
  295. Summary
  296. Batches, Queues, and Cron
  297. Batch powered update hooks
  298. Batch operations
  299. Creating the batch
  300. Batch operations
  301. Cron
  302. Queues
  303. Introduction to the Queue API
  304. Cron based queue
  305. Processing a queue programmatically
  306. Lock API
  307. Summary
  308. Views
  309. Entities in Views
  310. Exposing custom data to Views
  311. Views data
  312. Views fields
  313. Views relationships
  314. Views sorts and filters
  315. Views arguments
  316. Altering Views data
  317. Custom Views field
  318. Field configuration
  319. Custom Views filter
  320. Custom Views argument
  321. Views theming
  322. Views hooks
  323. Summary
  324. Working with Files and Images
  325. The filesystem
  326. Stream wrappers
  327. Managed versus unmanaged files
  328. Using the File and Image fields
  329. Working with managed files
  330. Attaching managed files to entities
  331. Helpful functions for dealing with managed files
  332. Managed file uploads
  333. Managed file form element
  334. Entity CRUD hooks
  335. Managed file usage service
  336. Processing the CSV file
  337. Our own stream wrapper
  338. Working with unmanaged files
  339. Private file system
  340. Images
  341. Image toolkits
  342. Image styles
  343. Rendering images
  344. Summary
  345. Automated Testing
  346. Testing methodologies in Drupal 8
  347. PHPUnit
  348. Registering tests
  349. Unit tests
  350. Mocked dependencies
  351. Kernel tests
  352. TeamCleaner test
  353. CsvImporter test
  354. Functional tests
  355. Configuration for functional tests
  356. Hello World page test
  357. Hello World form test
  358. Functional JavaScript tests
  359. Time test
  360. CsvImporter test
  361. Summary
  362. Drupal 8 Security
  363. Cross-Site Scripting (XSS)
  364. Sanitization methods in Drupal 8
  365. Double escaping
  366. SQL Injection
  367. Cross-Site Request Forgery (CSRF)
  368. Summary

Custom content entity type

As we saw in the preceding chapter, when looking at the Node and NodeType entity types, entity type definitions belong inside the Entity folder of our module's namespace. In there, we will create a class called Product, which will have an annotation at the top to tell Drupal this is a content entity type. This is the most important part in defining a new entity type:

namespace Drupal\products\Entity;

use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;

/**
* Defines the Product entity.
*
* @ContentEntityType(
* id = "product",
* label = @Translation("Product"),
* handlers = {
* "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
* "list_builder" = "Drupal\products\ProductListBuilder",
*
* "form" = {
* "default" = "Drupal\products\Form\ProductForm",
* "add" = "Drupal\products\Form\ProductForm",
* "edit" = "Drupal\products\Form\ProductForm",
* "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm",
* },
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider"
* }
* },
* base_table = "product",
* admin_permission = "administer site configuration",
* entity_keys = {
* "id" = "id",
* "label" = "name",
* "uuid" = "uuid",
* },
* links = {
* "canonical" = "/admin/structure/product/{product}",
* "add-form" = "/admin/structure/product/add",
* "edit-form" = "/admin/structure/product/{product}/edit",
* "delete-form" = "/admin/structure/product/{product}/delete",
* "collection" = "/admin/structure/product",
* }
* )
*/
class Product extends ContentEntityBase implements ProductInterface {}

In the preceding code block, I omitted the actual contents of the class to first focus on the annotation and some other aspects. We will see the rest of it shortly. However, the entire working code can be found in the accompanying repository.

If you remember from the preceding chapter, we have the ContentEntityType annotation with the entity type plugin definition. Our example is relatively barebones compared to Node, for example, because I wanted to keep things simple. It has no bundles and is not revisionable, nor translatable. Also, for some of its handlers, we fall back to Entity API defaults.

The entity type ID and label are immediately visible, so no need to explain that; however, we can instead skip to the handlers section.

For the view builder handler, we choose to default to the basic EntityViewBuilder because there is nothing our products especially need for being rendered. Many times, this will be enough, but you can also extend this class and create your own.

For the list builder, although still keeping things simple, we needed our own implementation in order to take care of things such as the list headers. We will see this class soon. The form handler to create and edit products is our own implementation found inside the Form namespace of our module, and we will see it soon to get a better understanding. We rely on Drupal 8 to help us out with the delete form, though.

Finally, for the route provider, we used the default AdminHtmlRouteProvider, which takes care of all the routes necessary for an entity type to be managed in the admin UI. This means that we no longer need to do anything for routing the links referenced in the links section of the annotation. Speaking of links, it makes sense to place them under the admin/structure section of our administration for our example, but you can choose another place if you want.

The database table our products will be stored in is products, and the permission needed for users to manage them is administer site configuration. I have purposefully omitted creating permissions specific to this entity type because we will cover this topic in a chapter dedicated to access.

Finally, we also have some basic entity keys to map to the respective fields.

Our Product class extends the ContentEntityBase class to inherit all the necessary stuff from the API and implements our very own ProductInterface, which will contain all the methods used to access relevant field values. Let's create that one real quick in the same Entity folder:

namespace Drupal\products\Entity;

use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityChangedInterface;

/**
* Represents a Product entity.
*/
interface ProductInterface extends ContentEntityInterface, EntityChangedInterface {

/**
* Gets the Product name.
*
* @return string
*/
public function getName();

/**
* Sets the Product name.
*
* @param string $name
*
* @return \Drupal\products\Entity\ProductInterface
* The called Product entity.
*/
public function setName($name);

/**
* Gets the Product number.
*
* @return int
*/
public function getProductNumber();

/**
* Sets the Product number.
*
* @param int $number
*
* @return \Drupal\products\Entity\ProductInterface
* The called Product entity.
*/
public function setProductNumber($number);

/**
* Gets the Product remote ID.
*
* @return string
*/
public function getRemoteId();

/**
* Sets the Product remote ID.
*
* @param string $id
*
* @return \Drupal\products\Entity\ProductInterface
* The called Product entity.
*/
public function setRemoteId($id);

/**
* Gets the Product source.
*
* @return string
*/
public function getSource();

/**
* Sets the Product source.
*
* @param string $source
*
* @return \Drupal\products\Entity\ProductInterface
* The called Product entity.
*/
public function setSource($source);

/**
* Gets the Product creation timestamp.
*
* @return int
*/
public function getCreatedTime();

/**
* Sets the Product creation timestamp.
*
* @param int $timestamp
*
* @return \Drupal\products\Entity\ProductInterface
* The called Product entity.
*/
public function setCreatedTime($timestamp);
}

As you can see, we are extending the obligatory ContentEntityInterface but also the EntityChangedInterface, which provides some handy methods to manage the last changed date of the entities. Those method implementations will be added to our Product class via the EntityChangedTrait:

use EntityChangedTrait;

The methods on the ProductInterface are relatively self-explanatory. We will have a product name, number, remote ID, and source field, so it's nice to have getters and setters for those. If you remember, the Entity API provides the get() and set() methods using which we can consistently access and store field values across all entity types. However, I find that using an interface with well-defined methods makes code much clearer, not to mention that IDE autocompletion is a great time-saver. We also have a getter and setter for the created date field, which is a typical field content entities have.

Now, we can take a look at the baseFieldDefinitions() method of our Product entity type and see how we actually defined these fields:

public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);

$fields['name'] = BaseFieldDefinition::create('string')
->setLabel(t('Name'))
->setDescription(t('The name of the Product.'))
->setSettings([
'max_length' => 255,
'text_processing' => 0,
])
->setDefaultValue('')
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'string',
'weight' => -4,
])
->setDisplayOptions('form', [
'type' => 'string_textfield',
'weight' => -4,
])
->setDisplayConfigurable('form', TRUE)
->setDisplayConfigurable('view', TRUE);

$fields['number'] = BaseFieldDefinition::create('integer')
->setLabel(t('Number'))
->setDescription(t('The Product number.'))
->setSettings([
'min' => 1,
'max' => 10000
])
->setDefaultValue(NULL)
->setDisplayOptions('view', [
'label' => 'above',
'type' => 'number_unformatted',
'weight' => -4,
])
->setDisplayOptions('form', [
'type' => 'number',
'weight' => -4,
])
->setDisplayConfigurable('form', TRUE)
->setDisplayConfigurable('view', TRUE);

$fields['remote_id'] = BaseFieldDefinition::create('string')
->setLabel(t('Remote ID'))
->setDescription(t('The remote ID of the Product.'))
->setSettings([
'max_length' => 255,
'text_processing' => 0,
])
->setDefaultValue('');

$fields['source'] = BaseFieldDefinition::create('string')
->setLabel(t('Source'))
->setDescription(t('The source of the Product.'))
->setSettings([
'max_length' => 255,
'text_processing' => 0,
])
->setDefaultValue('');

$fields['created'] = BaseFieldDefinition::create('created')
->setLabel(t('Created'))
->setDescription(t('The time that the entity was created.'));

$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
->setDescription(t('The time that the entity was last edited.'));

return $fields;
}

First and foremost, we will need to inherit the base fields of the parent class. This includes things such as the ID and UUID fields.

Second, we define our own fields, starting with the product name field, which is of the string type. This string type is nothing more than a FieldType plugin I mentioned in the preceding chapter. If you remember, this plugin extends a TypedData class itself. Apart from the obvious label and description, it has some settings, most notably a maximum length for the value, which is 255 characters. The view and form display options reference FieldFormatter and FieldWidget plugins, respectively, which together with the FieldType make up a field. Lastly, with the setDisplayConfigurable(), we state that some of the options on this field should be configurable through the UI. For example, we can change the label in the UI.

Then, we have the number field that is of the integer type and for this example is restricted to a number between 1 and 10,000. This restriction setting turns into a constraint under the hood. The rest of the options are similar to the name field.

Next, we have the remote_id string field, but it doesn't have any widget or display settings because we don't necessarily want to display or edit this value. It is mostly for internal use to keep track of the product ID of the remote source it came from. Similarly, the source string field is also not displayed or configurable because we want to use it to store the source of the product, where it has been imported from and also to keep track programmatically.

Finally, the created and changed fields are special fields that store the timestamps for when the entity is created and modified. Not much more than that is needed to do because these fields automatically set the current timestamps as necessary as the field values.

By now, we can also see the rest of the class contents, which is mostly made up of the methods required by the ProductInterface:

 use EntityChangedTrait;

/**
* {@inheritdoc}
*/
public function getName() {
return $this->get('name')->value;
}

/**
* {@inheritdoc}
*/
public function setName($name) {
$this->set('name', $name);
return $this;
}

/**
* {@inheritdoc}
*/
public function getProductNumber() {
return $this->get('number')->value;
}

/**
* {@inheritdoc}
*/
public function setProductNumber($number) {
$this->set('number', $number);
return $this;
}

/**
* {@inheritdoc}
*/
public function getRemoteId() {
return $this->get('remote_id')->value;
}

/**
* {@inheritdoc}
*/
public function setRemoteId($id) {
$this->set('remote_id', $id);
return $this;
}

/**
* {@inheritdoc}
*/
public function getSource() {
return $this->get('source')->value;
}

/**
* {@inheritdoc}
*/
public function setSource($source) {
$this->set('source', $source);
return $this;
}

/**
* {@inheritdoc}
*/
public function getCreatedTime() {
return $this->get('created')->value;
}

/**
* {@inheritdoc}
*/
public function setCreatedTime($timestamp) {
$this->set('created', $timestamp);
return $this;
}

As promised, we are making use of the EntityChangedTrait to handle the changed field and implement simple getters and setters for the values found in the fields we defined as base fields. If you remember the TypedData section, the way we access a value (since the cardinality is always 1 for these fields) is by running the following command:

$this->get('field_name')->value

Let's now move through the Entity type plugin annotation and create the handlers we've been referencing there. Also, we can start with the list builder, which we can place at the root of our namespace:

namespace Drupal\products;

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityListBuilder;
use Drupal\Core\Link;
use Drupal\Core\;Url;

/**
* EntityListBuilderInterface implementation responsible for the Product entities.
*/
class ProductListBuilder extends EntityListBuilder {

/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['id'] = $this->t('Product ID');
$header['name'] = $this->t('Name');
return $header + parent::buildHeader();
}

/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
/* @var $entity \Drupal\products\Entity\Product */
$row['id'] = $entity->id();
$row['name'] = Link::fromTextAndUrl(
$entity->label(),
new Url(
'entity.product.canonical', [
'product' => $entity->id(),
]
)
);
return $row + parent::buildRow($entity);
}

}

The purpose of this handler is to build the administration page that lists the available entities. On this page, we will then have some info about the entities and operation links to edit and delete and whatever else we might need. For our products, we will simply extend from the default EntityListBuilder class, but override the buildHeader() and builderRow() methods to add some information specific to our products. The names of these methods are self-explanatory, but one thing to keep in mind is that keys from the $header array we return need to match the keys from the $row array we return. Also, of course, the arrays need to have the same number of records so that the table header matches the individual rows. If you look inside EntityListBuilder, you note some other handy methods you might want to override, such as the one that builds the query and the one that loads the entities. For us, this is enough.

Our Products list builder will have, for now, only two columns--the ID and the name. For the latter, each row will be actually a link to the Product canonical URL (the main URL for this entity in Drupal). The construct for this route is in the entity.[entity_type].canonical format. Other useful entity links can be built by replacing the word canonical with the keys from the links definition of the Entity type plugin annotation. Finally, you remember, from Chapter 2, Creating Your First Module, how to build links with the Link class, right?

That is pretty much it for the list builder, and we can move on to the form handler. Since creating and editing an entity share so much in terms of what we need in the form, we use the same ProductForm for both those operations. Let's create that form class now:

namespace Drupal\products\Form;

use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Form\FormStateInterface;

/**
* Form for creating/editing Product entities.
*/
class ProductForm extends ContentEntityForm {

/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
/* @var $entity \Drupal\products\Entity\Product */
$form = parent::buildForm($form, $form_state);
return $form;
}

/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$entity = &$this->entity;

$status = parent::save($form, $form_state);

switch ($status) {
case SAVED_NEW:
drupal_set_message($this->t('Created the %label Product.', [
'%label' => $entity->label(),
]));
break;

default:
drupal_set_message($this->t('Saved the %label Product.', [
'%label' => $entity->label(),
]));
}
$form_state->setRedirect('entity.product.canonical', ['product' => $entity->id()]);
}

}

We extend ContentEntityForm, which is a specialized form class for content entities. It itself extends EntityForm, which then subclasses the FormBase we’ve already encountered in Chapter 2, Creating Your First Module. However, the former two give us a lot of functionalities needed to manage our entities without writing much code ourselves.

First, inside the buildForm() method, we will do nothing--not a thing. We might if we wanted to, but the parent classes are smart enough to read our Product entity and prepare all the necessary form elements with the right widgets (FieldWidget plugins) to build our form. Second, we skip the submit and validate handlers because there is nothing we need to do in them for our products. The only thing we actually want to do is override the save() method in order to write a message to the user informing them that the product has either been created or updated. This we can deduce because the EntityInterface::save() method returns a specific constant to denote the type of saving that happened. Lastly, we also want to redirect to the canonical URL of the product entity. This we do with a very handy method on the FormStateInterface by which we can specify a route (and any necessary parameters), and it will make sure that when the form is submitted, the user will be redirected to that route. Neat, isn't it?

As I mentioned, for the delete operation, we just use the ContentEntityDeleteForm, which does all we need--it presents a confirmation form where we submit and trigger the delete. This is a typical flow for deleting resources in Drupal. As we will see a bit later, for configuration entities, there will be some methods we will need to write ourselves for this same process to happen.

All our handlers are done now, and our product entity type is operational. However, in order to be able to work with it, let's create some links in the admin menu for being able to easily manage them. First, create the products.links.menu.yml file:

# Product entity menu items
entity.product.collection:
title: 'Product list'
route_name: entity.product.collection
description: 'List Product entities'
parent: system.admin_structure
weight: 100

This defines a menu link under the Structure link for the product list (the page built with our list builder handler).

Next, let's create some local tasks (tabs) so that we get handy links on the product page to edit and delete the product entity. So, inside the products.links.task.yml file:

# Product entity task items
entity.product.canonical:
route_name: entity.product.canonical
base_route: entity.product.canonical
title: 'View'

entity.product.edit_form:
route_name: entity.product.edit_form
base_route: entity.product.canonical
title: 'Edit'

entity.product.delete_form:
route_name: entity.product.delete_form
base_route: entity.product.canonical
title: Delete
weight: 10

You remember this from Chapter 5, Menus and Menu Links, don't you? The base route is always the canonical route for the entity, which essentially groups the tabs together. Then, the routes we use for the other two tasks are the edit_form and delete_form links of the entity type. You can refer to the links section of the Entity type plugin annotation to understand where these come from. The reason we don't need to specify any parameters here (since those routes do require a product ID) is because the base route has that parameter already in the URL. So, the tasks will use that one.

Finally, we also want an action link to create a new product entity, which will be on the product list page. So, inside the products.links.action.yml file:

entity.product.add_form:
route_name: entity.product.add_form
title: 'Add Product'
appears_on:
- entity.product.collection

Again, none of this should be new, as we covered it in detail in Chapter 5, Menus and Menu Links.

We are finally done. If the products module was enabled on your site before writing all the entity code, you will need to run the drush entity-updates command in order for all the necessary tables to be created in the database. Otherwise, installing the module will do that automatically. However, keep the first point in mind for when you add new content entity types and fields or even change existing fields on an entity type. The underlying storage might need to be changed to accommodate your modifications. Moreover, another thing to keep in mind is that changing fields that already have data in them will not be okay with Drupal and will prevent you from making those changes. So, you might need to delete existing entities.

Now that we've done that, we can go to admin/structure/product and take a look at our (empty) product entity list:

We can now create new products, edit them, and finally delete them. Remember, due to our field configuration, the manual product creation/edit does not permit the remote_id and source fields to be managed. For our purpose, we want those to be only programmatically available since any manual products will be considered as not needing that data. For example, if we want to make the source field show up as a form widget, all we have to do is change its base field definition to this:

$fields['source'] = BaseFieldDefinition::create('string')
->setLabel(t('Source'))
->setDescription(t('The source of the Product.'))
->setSettings([
'max_length' => 255,
'text_processing' => 0,
])
->setDefaultValue('')
->setDisplayOptions('form', [
'type' => 'string_textfield',
'weight' => -4,
]);

Also, we need to clear the cache. This will make the form element for the source field show up, but the value will still not be displayed on the canonical page of the entity because we have not set any view display options. In other words, we have not chosen a formatter.

However, in any case, our product entity is ready to store data, and all the TypedData APIs we practiced in the preceding chapter with the Node entity type will work just as well with this one. So, we can now turn to writing our importer logic to get some remote products into our website.