Jekyll2021-12-12T05:48:50+00:00https://mateuaguilo.com/atom.xmlMateu Aguiló BoschPersonal blog about web development, Drupal, software ethics, privacy, and other topicsMateu Aguiló BoschDrupal’s Bundle Classes Empower Better Code2021-11-08T00:00:00+00:002021-11-08T00:00:00+00:00https://mateuaguilo.com/web-development/drupal/2021/11/08/drupals-bundle-classes-empower-better-code<p>A highlight of the new features enabled by the recent commit to Drupal core and a comparison with the contributed Typed Entity module.</p>
<!-- more -->
<p><em>This is a re-post of the article on the <a href="https://www.lullabot.com/articles/drupals-bundle-classes-empower-better-code">Lullabot blog</a>.</em></p>
<p>Custom bundle classes have <a href="https://www.drupal.org/project/drupal/issues/2570593">landed in core</a>, which creates the possibility of improving the maintainability and quality of our PHP code. This article will explore how bundle classes can be leveraged and how this might lead to better code. This new feature will be available in Drupal core starting in 9.3.0. If you cannot wait for your site to upgrade to 9.3, you can use the <a href="https://www.drupal.org/files/issues/2021-10-20/2570593-209.patch">final patch</a> in the issue queue for your site until then.</p>
<p>First, what makes “better” code? Better code should:</p>
<ul>
<li>Be more declarative. This means that the intentions of what the code is doing should be more clear.</li>
<li>Be more testable. Testable code has explicit declaration of its dependencies and clear inputs and their outputs.</li>
</ul>
<p>With that in mind, let’s explore what custom bundle classes are. According to the <a href="https://www.drupal.org/node/3191609">change record</a>, bundle classes are classes that encapsulate business logic around entities.</p>
<blockquote>
<p>Entity bundles are essentially business objects, and now they can declare their own class, encapsulating the required business logic.</p>
</blockquote>
<p>The change record offers a good amount of situations where this will be useful. These are organized in the following section on that page:</p>
<ul>
<li><em>Moving from template preprocess to get*() methods</em>. The Twig integration in Drupal has always let you call methods like getFoo inside of the templates. In the past, we could not write our own getters for classes like Node or Term, so that feature was underused. Now that we can write these classes, we can drop <strong>some</strong> code from preprocess functions, calling getFoo directly in the template.</li>
<li><em>Sharing code</em>. Following from the previous point, once we take code from procedural preprocess functions to a class (directly or via a trait, base class, etc.), we can reuse that code more easily in other places.</li>
<li><em>Writing automated tests</em>. The more code we put in classes instead of procedural functions, the easier to test that code becomes. We will be able to leverage <a href="https://www.drupal.org/docs/automated-testing/types-of-tests">kernel testing</a> (but not unit tests) for our custom logic encapsulated into these bundle classes.</li>
</ul>
<h2 id="using-bundle-classes">Using Bundle Classes</h2>
<p>The <a href="https://www.drupal.org/node/3191609">change record</a> covers how to make Drupal return the custom classes after loading an entity. Please refer to that. This section will reason about a hypothetical use case of this new feature.</p>
<p>Imagine you are building a website for a public library. It is reasonable to model books as a content type. Your team has decided that a node for books is a good approach since you will need a page for each book, along with moderation, translation, etc. Then, you proceed to create a bundle class called Book:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c1">// Book.php</span>
<span class="kd">class</span> <span class="nc">Book</span> <span class="kd">extends</span> <span class="nc">Node</span> <span class="kd">implements</span> <span class="nc">BookInterface</span> <span class="p">{}</span>
</code></pre></div></div>
<p>So far, this class does very little, but we can already see some benefits. When writing code that uses nodes, you can know that “a node is a book” from a static perspective using <code class="language-plaintext highlighter-rouge">$node instanceof BookInterface</code>, as opposed to the previous runtime perspective <code class="language-plaintext highlighter-rouge">$node->bundle() === 'book'</code>. This is important because your IDE can leverage this information and autocomplete for <code class="language-plaintext highlighter-rouge">BookInterface</code> but not <code class="language-plaintext highlighter-rouge">SectionInterface</code>. Not only will IDEs benefit from this, but also other tools like Psalm and PHPStan. You opened up so many possibilities with that little code!</p>
<p>Now it’s time for your team to turn the attention to the <code class="language-plaintext highlighter-rouge">BookInterface</code>. You need that interface to encode the logic of your project, the business logic (remember, we said <em>bundles are essentially business objects</em>). You decide that books need to implement <code class="language-plaintext highlighter-rouge">AccessibleInterface</code> because they asked your team to restrict access to books based on their parental rating and the user’s age.</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c1">// BookInterface.php</span>
<span class="kd">interface</span> <span class="nc">BookInterface</span> <span class="kd">extends</span> <span class="nc">AccessibleInterface</span> <span class="p">{}</span>
</code></pre></div></div>
<p>This small piece of code, again, opens a ton of possibilities. You can now implement <code class="language-plaintext highlighter-rouge">hook_entity_access</code> and call the <code class="language-plaintext highlighter-rouge">->access(...)</code> method in the <code class="language-plaintext highlighter-rouge">$entity</code> if <code class="language-plaintext highlighter-rouge">$node instanceof AccessibleInterface</code>. This has two benefits. One, your access logic is contained within the Book class, along with the rest of the book methods (and can be tested). Two, if another bundle needs access checks, you don’t need to update your <code class="language-plaintext highlighter-rouge">hook_entity_access</code> implementation, your team can just add <code class="language-plaintext highlighter-rouge">AccessibleInterface</code> to the bundle’s interface.</p>
<p>Let’s keep (fake-)coding. These two small changes have delivered so much! Next, you want to ensure labels display as the stakeholder requires, using the pattern “Title - Editorial (Year).” However, on cards, the label should omit the word “Editorial.”</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c1">// Book.php</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">getTitle</span><span class="p">(</span><span class="kt">string</span> <span class="nv">$view_mode</span> <span class="o">=</span> <span class="s1">''</span><span class="p">):</span> <span class="kt">TranslatableMarkup</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$view_mode</span> <span class="o">===</span> <span class="s1">'card'</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">t</span><span class="p">(</span><span class="s1">'@title (@year)'</span><span class="p">,</span> <span class="p">[</span>
<span class="s1">'@title'</span> <span class="o">=></span> <span class="nv">$this</span><span class="o">-></span><span class="nf">get</span><span class="p">(</span><span class="s1">'title'</span><span class="p">)</span><span class="o">-></span><span class="n">value</span><span class="p">,</span>
<span class="s1">'@year'</span> <span class="o">=></span> <span class="nv">$this</span><span class="o">-></span><span class="nf">yearFromTimestamp</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="nf">get</span><span class="p">(</span><span class="s1">'field_release_date'</span><span class="p">)</span><span class="o">-></span><span class="n">value</span><span class="p">),</span>
<span class="p">]);</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">t</span><span class="p">(</span><span class="s1">'@title - @editorial (@year)'</span><span class="p">,</span> <span class="p">[</span>
<span class="s1">'@title'</span> <span class="o">=></span> <span class="nv">$this</span><span class="o">-></span><span class="nf">get</span><span class="p">(</span><span class="s1">'title'</span><span class="p">)</span><span class="o">-></span><span class="n">value</span><span class="p">,</span>
<span class="s1">'@editorial'</span> <span class="o">=></span> <span class="nv">$this</span><span class="o">-></span><span class="nf">get</span><span class="p">(</span><span class="s1">'field_editorial'</span><span class="p">)</span><span class="o">-></span><span class="n">entity</span><span class="o">-></span><span class="nf">label</span><span class="p">(),</span>
<span class="s1">'@year'</span> <span class="o">=></span> <span class="nv">$this</span><span class="o">-></span><span class="nf">yearFromTimestamp</span><span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="nf">get</span><span class="p">(</span><span class="s1">'field_release_date'</span><span class="p">)</span><span class="o">-></span><span class="n">value</span><span class="p">),</span>
<span class="p">]);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>In the Twig template, we can call this to invoke the new method:</p>
<div class="language-twig highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c">{# node--book-twig.html #}</span>
<span class="nt"><h2></span>{ { node.getTitle() }}<span class="nt"></h2></span>
</code></pre></div></div>
<p>Your <code class="language-plaintext highlighter-rouge">node--book.twig.html</code> template can use this method to print the book title as defined by the <code class="language-plaintext highlighter-rouge">Book</code> class. You just created a small and testable method for rendering the book title. More importantly, you can do this for other fields and keep them organized. In the past, this could have been a deeply nested <code class="language-plaintext highlighter-rouge">if</code> statement in a preprocess function living alongside all the other field preprocessing for all the content types. This may not seem big from a ‘preprocess tangle just works’ perspective. However, when you need to apply that same logic anywhere else (for instance, some Views code), untangling the preprocess code to extract that logic to a custom method you can reuse will be cumbersome and likely to result in code duplication.</p>
<p>Additionally, if developers want to see if they could reuse something, this new approach allows them to look in a single place. Without bundle classes, the logic could be in your theme, in a custom module <code class="language-plaintext highlighter-rouge">.module</code>, abstracted in a service, or anywhere!</p>
<p>As you can see, using bundle classes leads to many benefits, even with our tiny examples. Imagine what it can do for your complex project.</p>
<h2 id="where-bundle-classes-fall-short">Where Bundle Classes Fall Short</h2>
<p>Bundle classes are a huge step forward. Unfortunately, they fall short for many use cases. They may be enough for your needs. As shown above, just by using bundle classes, you are already improving your codebase.</p>
<p>But in the future, you may feel you need “something” more. When that happens, you may want to look at the <a href="https://www.drupal.org/project/typed_entity">Typed Entity</a> module.</p>
<p>The first version of Typed Entity for Drupal 7 was released in February 2015. Back then, the classes extended the entities (<a href="https://git.drupalcode.org/project/typed_entity/-/blob/7.x-1.x/src/TypedEntity/TypedEntity.php#L215-232">sort</a> <a href="https://git.drupalcode.org/project/typed_entity/-/blob/7.x-1.x/src/TypedEntity/TypedEntity.php#L215-232">of</a>, entity objects were not a thing then). Now, the module utilizes <a href="https://en.wikipedia.org/wiki/Composition_over_inheritance">composition over inheritance</a>.</p>
<p>If you want to dive deeper into this topic, take a look at <a href="https://www.lullabot.com/articles/maintainable-code-drupal-wrapped-entities">Maintainable Code in Drupal: Wrapped Entities</a> and <a href="https://www.lullabot.com/articles/write-better-code-typed-entity">Write Better Code with Typed Entity</a>. If you learn better with video, <a href="https://www.youtube.com/watch?v=TQMrfH9oDX4">this presentation</a> is available.</p>
<p>For our immediate purposes, bundle classes are compared with the Typed Entity approach in <a href="https://youtu.be/TQMrfH9oDX4?t=503">minute 8:23 of the video presentation</a>. <a href="https://www.drupal.org/project/typed_entity/issues/3245990">This issue in Typed Entity’s issue</a> queue prompted that exploration. Let’s explore some more!</p>
<p><img src="/assets/images/bundles-1.png" style="background-color: white; padding: 15px" alt="Comparison" /></p>
<h3 id="bundles-are-not-atomic"><strong>Bundles are not Atomic</strong></h3>
<p>We have established that <em>entity bundles are essentially business objects</em>. However, they are almost always <em>multi__-__purpose</em> business objects. Imagine that you add a <code class="language-plaintext highlighter-rouge">field_book_type</code> field to allow editors to specify whether a book is <em>paper</em> or <em>audio book</em>, and then you add another a <code class="language-plaintext highlighter-rouge">field_book_popularity</code> to select <em>hidden gem</em>, <em>popular</em>, <em>best seller</em>. It is reasonable to think there will be specific logic for audiobooks or bestsellers, just like we had specific logic for articles before, but we only had the <code class="language-plaintext highlighter-rouge">Node</code> class.</p>
<p>How will that look in code when using bundle classes?</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c1">// Book.php</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">getLink</span><span class="p">():</span> <span class="kt">Link</span> <span class="p">{</span>
<span class="cm">/* ... */</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="nb">getType</span><span class="p">()</span> <span class="o">===</span> <span class="s1">'audio book'</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// Link to the MP3 player page.</span>
<span class="cm">/* ... */</span>
<span class="p">}</span>
<span class="k">else</span> <span class="p">{</span>
<span class="c1">// Build the link to the PDF asset and create the Link object.</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">getTitle</span><span class="p">(</span><span class="kt">string</span> <span class="nv">$view_mode</span> <span class="o">=</span> <span class="s1">''</span><span class="p">):</span> <span class="kt">TranslatableMarkup</span> <span class="p">{</span>
<span class="cm">/* ... */</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="nf">getPopularity</span><span class="p">()</span> <span class="o">===</span> <span class="s1">'best seller'</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// Add the the "best seller badge".</span>
<span class="cm">/* ... */</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>At this point, you might be asking yourself:</p>
<ol>
<li><em>How is this</em> <code class="language-plaintext highlighter-rouge">_$this->getType() === 'audio book'_</code> <em>different from the</em> <code class="language-plaintext highlighter-rouge">_$node->bundle() === 'book'_</code> <em>we were trying to avoid above?</em></li>
<li>You might follow that up with <em>We have improved greatly moving away from the big tangle of nested</em> <code class="language-plaintext highlighter-rouge">_if_</code> <em>in the preprocess functions, but I have a smaller mess in my</em> <code class="language-plaintext highlighter-rouge">_getXYZ_</code> <em>functions</em>.</li>
<li>Then, you realize that <em>I still want those IDE and static analysis benefits!</em></li>
</ol>
<p><a href="https://www.drupal.org/project/typed_entity">Typed Entity</a> addresses this problem and lets you create book “variants” based on arbitrary conditions. In this case, we could create a the <code class="language-plaintext highlighter-rouge">AudioBook</code> and <code class="language-plaintext highlighter-rouge">BestSellerBook</code> variants. Then we will benefit from everything bundle classes introduced inside of the bundles.</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c1">// AudioBook.php (extends Book.php)</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">getLink</span><span class="p">():</span> <span class="kt">Link</span> <span class="p">{</span>
<span class="c1">// Link to the MP3 player page.</span>
<span class="k">return</span> <span class="cm">/* ... */</span>
<span class="p">}</span>
<span class="c1">// Method specific for audio books.</span>
<span class="k">public</span> <span class="kt">getTranscriptions</span><span class="p">()</span><span class="o">:</span> <span class="n">string</span> <span class="p">{</span>
<span class="cm">/* ... */</span>
<span class="p">}</span>
<span class="c1">// Book.php</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">getLink</span><span class="p">():</span> <span class="kt">Link</span> <span class="p">{</span>
<span class="c1">// Build the link to the PDF asset and create the Link object.</span>
<span class="k">return</span> <span class="cm">/* ... */</span>
<span class="p">}</span>
</code></pre></div></div>
<p>As you can see, Typed Entity will empower you to have <strong>dedicated classes to your business objects</strong>, regardless of whether they are entity bundles or not.</p>
<h3 id="mixing-framework-logic-and-business-logic"><strong>Mixing Framework Logic and Business Logic</strong></h3>
<p>From the Wikipedia page on <a href="https://en.wikipedia.org/wiki/Business_logic">Business Logic</a>:</p>
<blockquote>
<p>In computer software, business logic or domain logic is the part of the program that encodes the real-world business rules that determine how data can be created, stored, and changed. It is contrasted with the remainder of the software that might be concerned with lower-level details of managing a database or displaying the user interface, system infrastructure, or generally connecting various parts of the program.</p>
</blockquote>
<p>In Drupal words, entities and bundles are Drupal concepts. Books, audiobooks, buildings, etc., are concepts from <em>your project</em>. Ensuring they are separate will improve code quality, as reasoned by <a href="https://simple.wikipedia.org/wiki/SOLID_(object-oriented_design)">SOLID</a> design principles, particularly the single responsibility principle.</p>
<p>When we make Book a bundle class that extends from Node, we are dragging all the framework logic necessary to make nodes function (database, render, routing, …) into our business logic.</p>
<p>Instead, the Book class should <strong>use</strong> the node (database, render, routing, etc.) and not <strong>be</strong> a node. With that in mind, when you use Typed Entity, your Book class will have a $node property attached to use instead of extending the Node class.</p>
<p>When your Book has a node in it, you can write Unit tests for it <strong>mocking the dependency</strong> on the $node. You cannot mock the node to test the book with bundle classes because the book is the node.</p>
<p>The major drawback is that you will need to “wrap” your entities in the integration points between framework and business logic. This is why in Typed Entity, they are called <em>wrapped entities</em>. This <code class="language-plaintext highlighter-rouge">wrap($entity)</code> is likely to be in hooks, events, services, etc. This is simple to do, but some feel it is tedious. Bundle classes will not suffer from this because they <strong>are</strong> the entities.</p>
<p>This is what wrapping an entity looks like:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">function</span> <span class="n">mymodules_entity_presave</span><span class="p">(</span><span class="kt">EntityInterface</span> <span class="nv">$entity</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$wrapped_entity</span> <span class="o">=</span> <span class="nf">typed_entity_repository_manager</span><span class="p">()</span><span class="o">-></span><span class="nf">wrap</span><span class="p">(</span><span class="nv">$entity</span><span class="p">);</span>
<span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Within your business classes, you will not need to wrap your entities manually; you can use the <code class="language-plaintext highlighter-rouge">wrapReference()</code> and <code class="language-plaintext highlighter-rouge">wrapReferences()</code> methods. This inconvenience happens only where business logic interacts with framework logic.</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c1">// AudioBook.php</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">getNarrator</span><span class="p">():</span> <span class="kt">Person</span> <span class="p">{</span>
<span class="c1">// Do NOT use this:</span>
<span class="c1">// return typed_entity_repository_manager()->wrap($this->getEntity()->get(static::FIELD_NAME_NARRATOR)->entity);</span>
<span class="c1">//</span>
<span class="c1">// Use this instead:</span>
<span class="k">return</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">wrapReference</span><span class="p">(</span><span class="k">static</span><span class="o">::</span><span class="no">FIELD_NAME_NARRATOR</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Notice how <code class="language-plaintext highlighter-rouge">getNarrator</code> returns a business object <code class="language-plaintext highlighter-rouge">Person</code>, not an entity.</p>
<h3 id="business-logic-for-multiple-entities"><strong>Business Logic for Multiple Entities</strong></h3>
<p>The last inconvenience with bundle classes is that they do not offer a solution for logic that applies to multiple items or no particular item. In our imaginary project, we might want to update the lending status to “unavailable” for all the books with no copies left in the building. With bundle classes, we will need to resort to <a href="https://en.wikipedia.org/wiki/Method_%28computer_programming%29#Static_methods">static methods</a>. However, static methods are just procedural functions with a namespace. They suffer from the same problems we want to solve.</p>
<p>To address this, Typed Entity uses typed repositories. Hence, we will have a BookRepository class that will be in charge of these types of operations:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c1">// BookRepository.php</span>
<span class="kd">class</span> <span class="nc">BookRepository</span> <span class="kd">extends</span> <span class="nc">TypedRepositoryBase</span> <span class="p">{</span>
<span class="c1">// To be run by a drush command every day at night.</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">updateBooksFromLendRecords</span><span class="p">(</span><span class="kt">array</span> <span class="nv">$isbns</span><span class="p">):</span> <span class="kt">void</span> <span class="p">{</span>
<span class="nv">$books</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">loadFromMultipleIsbns</span><span class="p">(</span><span class="nv">$isbns</span><span class="p">);</span>
<span class="nb">array_map</span><span class="p">(</span><span class="k">function</span> <span class="p">(</span><span class="kt">BookInterface</span> <span class="nv">$book</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$book</span><span class="o">-></span><span class="nf">loan</span><span class="p">();</span>
<span class="p">},</span> <span class="nv">$books</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>With typed entity, we end up with a regular method we can test. We could easily mock <code class="language-plaintext highlighter-rouge">loan</code> on the <code class="language-plaintext highlighter-rouge">Book</code> class. On the other hand, the static functions in bundle classes are not testable. Moreover, if this logic needs to be used in a Drush command, it would call <code class="language-plaintext highlighter-rouge">Book::updateBooksFromLendRecords</code>, which you will not be able to mock. This makes the Drush command untestable as well.</p>
<p>With Typed Entity, the Drush command receives the book repository via dependency injection so that you can test as usual.</p>
<figure>
<img src="/assets/images/bundles-2.png" style="background-color: white; padding: 15px" alt="Diagram" />
<figcaption>The book repository is in charge of operations on multiple books. Each object can be an AudioBook, a plain Book, etc.</figcaption>
</figure>
<h3 id="other-useful-features"><strong>Other Useful Features</strong></h3>
<p>In addition to the features above, Typed Entity also introduces the concept of <a href="https://git.drupalcode.org/project/typed_entity/-/blob/4.x/src/Render/TypedEntityRendererInterface.php"><em>renderers</em></a>. Renderers are business objects for rendering an entity. They facilitate writing code for <a href="https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Render!theme.api.php/function/hook_preprocess"><code class="language-plaintext highlighter-rouge">hook_preprocess</code></a>, <a href="https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Entity!entity.api.php/function/hook_entity_view_alter"><code class="language-plaintext highlighter-rouge">hook_entity_view_alter</code></a>, and <a href="https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Entity!entity.api.php/function/hook_entity_display_build_alter"><code class="language-plaintext highlighter-rouge">hook_entity_display_build_alter</code></a> in a more reusable and testable way. They also allow you to organize the business logic for rendering a particular entity. Renderers exist as a statement to say: <em>rendering entities into HTML is the most common goal for them to exist,</em> <em>so</em> <em>let’s provide helpers for that custom code to be more manageable.</em></p>
<p>Typed Entity also comes with a sub-module for developers called Typed Entity UI. This tries to help with onboarding new developers into a project so that they can see the business objects for entities with a glance. Something similar for bundle classes would also be useful.</p>
<p><img src="/assets/images/bundles-3.png" alt="UI" /></p>
<h2 id="in-conclusion">In Conclusion</h2>
<p>Bundle classes are a great step forward for Drupal development. They open the door to more maintainable code with little effort. Just one extra step that will make your projects more sustainable and reliable. It also makes projects more enjoyable to work with for developers.</p>
<p>Both Typed Entity and bundle classes are very useful. They will allow you to improve the quality and maintainability of your codebases to different degrees. However, they are just tools. They will not do the work for you. But they will help you leverage object-oriented programming design patterns, set internal conventions within your team for code organization, leverage automated testing of business logic where it makes sense, and much more.</p>
<p>For many reasons, bundle classes still fall short. There are still things that need to be improved. Because improvements to Drupal core are always complex, while contributed modules can be more nimble, Typed Entity will remain a place where these pain points can continue to be addressed.</p>
<p><small>Photo by <a href="https://unsplash.com/@amyshamblen">Amy Shamblen</a> on <a href="https://unsplash.com/s/photos/fruit">Unsplash</a></small></p>Mateu Aguiló BoschA highlight of the new features enabled by the recent commit to Drupal core and a comparison with the contributed Typed Entity module.Moving More of My Content Away from YouTube2021-10-04T00:00:00+00:002021-10-04T00:00:00+00:00https://mateuaguilo.com/web-development/drupal/2021/10/04/simple-oauth-moved-peertube<p>I just moved my video playlist containing documentation for Simple OAuth from YouTube to my self hosted PeerTube instance. I did this because I belive on our digital rights and our <a href="https://techautonomy.org/">digital autonomy</a>.</p>
<!-- more -->
<p>I believe in a better model for the digital world. This is extremely important as a society because there is no boundary between the digital and the physical world anymore. Our lack of rights in the digital world lead to abuse and inequality in the physical world (sometimes called the “real” world).</p>
<p>Moving a playlist for documentating a Drupal module will not solve any of those problems. However, this is my prerrogativet to do (it is my content), and it may help to inspire others to follow this initiative.</p>
<p>This is the link to the new playlist: <a href="https://video.mateuaguilo.com/my-library/video-playlists/b3b96ff7-8791-4815-a2a5-54091b6da326">https://video.mateuaguilo.com/my-library/video-playlists/b3b96ff7-8791-4815-a2a5-54091b6da326</a>.</p>
<div class="video-wrapper"><iframe width="560" height="315" sandbox="allow-same-origin allow-scripts allow-popups" title="Simple OAuth" src="https://video.mateuaguilo.com/video-playlists/embed/b3b96ff7-8791-4815-a2a5-54091b6da326" frameborder="0" allowfullscreen=""></iframe></div>Mateu Aguiló BoschI just moved my video playlist containing documentation for Simple OAuth from YouTube to my self hosted PeerTube instance. I did this because I belive on our digital rights and our digital autonomy.Help Links in Drupal Modules2021-09-20T00:00:00+00:002021-09-20T00:00:00+00:00https://mateuaguilo.com/web-development/drupal/2021/09/20/cards-drupal-admin<p>This idea is separate and completely unrelated to the
<a href="https://www.drupal.org/community-initiatives/documentation-and-help-initiative">Documentation and Help Initiative</a>.
My goal is to be able to link to the article and videos I create for my Drupal
contributions, which I create outside drupal.org. This comes after I have had to
point to multiple people to those links, even if they are in the project’s page.</p>
<!-- more -->
<p>My idea is to add a card on some appropriate admin pages related to the tasks
described in the articles and videos. The card should be unobtrusive and
dismissible. Once dismissed, it will not be shown again.</p>
<p><img src="/assets/images/typed-entity-video.gif" alt="Typed Entity with a help card" /></p>
<p>What do you think? Is it a cool pattern, or something that feels spammy and gets
in the way?</p>Mateu Aguiló BoschThis idea is separate and completely unrelated to the Documentation and Help Initiative. My goal is to be able to link to the article and videos I create for my Drupal contributions, which I create outside drupal.org. This comes after I have had to point to multiple people to those links, even if they are in the project’s page.Progressive Decoupling Made Easy2021-09-14T00:00:00+00:002021-09-14T00:00:00+00:00https://mateuaguilo.com/web-development/drupal/2021/09/14/progressive-decoupling-made-easy<p>Decoupling separates the system that stores the content from how that content is displayed on other independent systems. This can come with many benefits but also some downsides and tradeoffs. With progressive decoupling, you can get some of the benefits of decoupling while avoiding some of the downsides.</p>
<!-- more -->
<p><em>This is a re-post of the article on the <a href="https://www.lullabot.com/articles/progressive-decoupling-made-easy">Lullabot blog</a>.</em></p>
<div class="video-wrapper"><iframe sandbox="allow-same-origin allow-scripts allow-popups" src="https://video.mateuaguilo.com/videos/embed/2aadbe24-2a9a-4676-9cd5-9b33a0f66736" frameborder="0" allowfullscreen=""></iframe></div>
<p><a href="https://mateuaguilo.com/assets/documents/new-standard-progressive-decoupling.pdf">Slides available here</a>.</p>
<p>Decoupling separates the system that stores the content from how that content is displayed on other independent systems. This can come with many benefits but also some downsides and tradeoffs.</p>
<p>With progressive decoupling, you can get some of the benefits of decoupling while avoiding some of the downsides.</p>
<p>There are several ways to decouple a website progressively, but this article makes the case that widgets provide the most flexibility.</p>
<h2>What are widgets?</h2>
<p>Widgets are stand-alone JavaScript (JS) applications that are framework-agnostic and are designed to be embedded and configured by CMS editors.</p>
<p>Widgets can be vanilla JS or use frameworks like Vue or React.</p>
<h2>Why JS over server-generated HTML?</h2>
<h3>Better reactivity and interactivity</h3>
<p>The pages can be static or served from cache (very fast), and JS can be sprinkled on top. The server can provide the unchanging parts, while the JS application adds interactivity.</p>
<p>This reduces the load on your servers while increasing website performance. You keep the benefits of built-in CMS performance tooling.</p>
<h3>Distributed delivery</h3>
<p>Different development teams can write software independently. They can publish software on the same platform without coordinating complex deployment efforts.</p>
<ul>
<li>Teams write the JS code in isolation</li>
<li>The browser executes the JS</li>
<li>Different deployment pipelines and servers can be used.</li>
</ul>
<p><img src="/assets/images/progressive-decoupling/distributedexample.png" alt="Distributed example" /></p>
<p>One team works on the navigation, one team works on the main feature set, and one team works on a price calculator.</p>
<h3>Biggest talent pool</h3>
<p>According to extensive surveys, JS and TypeScript (a superset of JS) are the most commonly used languages, based on <a href="https://insights.stackoverflow.com/survey/2020#overview" target="_blank">Stackoverflow’s yearly survey</a>.</p>
<p>By building pages and experiences in JS, you can pull talent from a bigger pool. You have more options.</p>
<h3>Better developer experience</h3>
<p>Since JS is so popular, your developers can leverage many tools, services, and frameworks. Jest, Storybook, Husky, Gulp, for things like unit testing, component management, setting githooks, etc. Many services integrate with the technology.</p>
<p>Many platforms will give you better support, which leads to better workflows, which hopefully leads to better code—things like visual diffs, code quality analysis, and code deployment. Popularity leads to a flourishing ecosystem.</p>
<p>In addition, frameworks like Vue can take care of some of the rough edges.</p>
<h3>Should we just build JS applications then?</h3>
<p>Yes and no. We still care about the content. Content is the heart of the web. You can have a great user experience, but without content, your project is doomed to fail.</p>
<p>To manage content, you need a CMS. Especially if <em>content</em> is your product or is central to your business. A CMS provides many features that are hard to build from scratch.</p>
<ul>
<li>Managing pages and setting up URLs</li>
<li>Users and access restrictions</li>
<li>SEO metadata</li>
<li>Media library</li>
<li>Security patches</li>
<li>Editorially controlled layouts</li>
<li>Moderation and previews</li>
</ul>
<h2>Why widgets?</h2>
<p>We have a CMS. We know we want to use some JS. Why not put JS in our CMS templates?</p>
<p>This works. You can certainly go that route. But widgets have some advantages over JS in the template.</p>
<h3>They require no CMS deployments</h3>
<p>A developer creates a new widget in the registry, and it appears in the CMS editorial interface for embedding. No additional effort. Bug fixes and enhancements are also instantaneous.</p>
<p>Here is what a traditional deployment might look like:</p>
<p><img src="/assets/images/progressive-decoupling/js-deploy-1.png" alt="JS Deploy 1" /></p>
<ol start="1">
<li>Develop JS app</li>
<li>Integrate it with a CMS template (and with the content model if you want the app to receive editorial input)</li>
<li>Deploy both in conjunction since they are coupled together</li>
<li>Editors can expose the JS app to end-users</li>
</ol>
<p>Widgets allow you to skip the two middle steps. When you use the existing CMS integrations, development is only done in JS, and it can be deployed on its own. No need to call in a CMS developer to add new widgets or update existing widgets.</p>
<p>A widget deployment looks like this:</p>
<p><img src="/assets/images/progressive-decoupling/js-deploy-2.png" alt="JS Deploy 2" /></p>
<h3>Embedded and controlled by editors</h3>
<p>JS developers can create flexible applications that allow for tweaked experiences and configuration. A single widget can act as multiple similar widgets.</p>
<p>JS developers define the input data they expect from editors, and the CMS creates a form for the editors to input that data. This allows many instances of the same type of widget to be embedded with different configurations: different content, color palettes, external integrations, etc.</p>
<p>The following example defines a customizable button that the editor can configure.</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">settingsSchema</span><span class="p">:</span> <span class="p">{</span>
<span class="nl">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">object</span><span class="dl">'</span><span class="p">,</span>
<span class="nx">additionalProperties</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="nx">properties</span><span class="p">:</span> <span class="p">{</span>
<span class="nl">fields</span><span class="p">:</span> <span class="p">{</span>
<span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">object</span><span class="dl">'</span><span class="p">,</span>
<span class="na">properties</span><span class="p">:</span> <span class="p">{</span>
<span class="dl">'</span><span class="s1">button-text</span><span class="dl">'</span><span class="p">:</span> <span class="p">{</span>
<span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">string</span><span class="dl">'</span><span class="p">,</span>
<span class="na">title</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Button text</span><span class="dl">'</span><span class="p">,</span>
<span class="na">description</span><span class="p">:</span>
<span class="dl">'</span><span class="s1">Some random string to be displayed.</span><span class="dl">'</span><span class="p">,</span>
<span class="na">examples</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">I am a button</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">Please, click me</span><span class="dl">'</span><span class="p">],</span>
<span class="p">},</span>
<span class="p">},</span>
<span class="p">},</span>
<span class="p">},</span>
<span class="p">},</span>
<span class="nx">title</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Example Widget</span><span class="dl">'</span><span class="p">,</span>
<span class="nx">status</span><span class="p">:</span> <span class="dl">'</span><span class="s1">stable</span><span class="dl">'</span><span class="p">,</span>
</code></pre></div></div>
<p>The CMS integration, which can be defined up-front, reads the definition and presents the proper form elements to the editor.</p>
<p><img src="/assets/images/progressive-decoupling/customized-button-example.png" alt="Customized button example" /></p>
<h3>Embedded anywhere</h3>
<p>Since widgets are not embedded at build time, but editorially, they can be placed anywhere. If the JS is in the template, you can’t choose, for example, to insert the JS app between two paragraphs of the body field. And changing the position would require a CMS deployment.</p>
<p><img src="/assets/images/progressive-decoupling/body-field-insert.png" alt="Body field insert" /></p>
<p>With widgets, editors can insert them anywhere.</p>
<ul>
<li>Using layout building tools</li>
<li>Using WYSIWYG integrations</li>
<li>Using content modeling tools (entity reference field that points to a widget instance)</li>
<li>Using 3rd party JavaScript</li>
</ul>
<p><img src="/assets/images/progressive-decoupling/wysiwyg-layout-builder.png" alt="WYSIWYG layout builder" /></p>
<p>And the same widget can work for any CMS. As long as the CMS subscribes to the registry and can read the schema, it can embed the JS application. When you change or fix something in the JS app, it is distributed to all CMSs. Widgets can also work in static HTML pages and Optimizely pages. Anywhere.</p>
<h2>When are widgets a good fit?</h2>
<p>Structured content is still the way to go. You don’t have to use widgets everywhere, but they are useful in several contexts.</p>
<ul>
<li>Interacting with 3rd party APIs - reviews sites (g2crowd), commenting</li>
<li>Interactive tools - pricing calculators, checklists saving progress</li>
<li>Data visualizations - maps, charts of COVID data</li>
<li>Adding some pop to a page - you can do some things with JS that may be difficult to achieve when limited to HTML and CSS</li>
</ul>
<h2>How to get started</h2>
<h3>Create a widget</h3>
<p>From a technical perspective, a widget is a function that takes a DOM id and renders JS in it. A widget can also receive arguments as HTML data.</p>
<p>Here is an example of rendering a React component:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nb">window</span><span class="p">.</span><span class="nx">renderExampleWidget</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">instanceId</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">element</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="nx">instanceId</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">title</span> <span class="o">=</span> <span class="nx">element</span><span class="p">.</span><span class="nx">getAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">data-button-text</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">ReactDOM</span><span class="p">.</span><span class="nx">render</span><span class="p">(</span>
<span class="o"><</span><span class="nx">Widget</span> <span class="nx">title</span><span class="o">=</span><span class="p">{</span><span class="nx">title</span><span class="p">}</span> <span class="sr">/></span><span class="err">,
</span> <span class="nx">element</span><span class="p">,</span>
<span class="p">);</span>
<span class="p">};</span>
</code></pre></div></div>
<p>It is very easy to port existing components and re-use them.</p>
<h3>Upload the app code</h3>
<p>The code needs to live somewhere accessible to the internet (Github pages, Amazon s3 bucket, etc.). The CMS can use this to either download the files or serve them from there. We don’t want to bundle the files within the CMS because that introduces coupling again.</p>
<h3>Publish the metadata</h3>
<p>This is the tricky part. Without the metadata, this is just another JS application in some repo.</p>
<p>We need a registry, which is just a JSON document containing the metadata about all the available apps that can be downloaded from the internet. An array of objects. This includes the “directoryUrl,” which defines exactly where the files live. You can also see the “settingsSchema” property, which defines the shape of the input data this widget will accept.</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"repositoryUrl"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://github.com/js-widgets/example-widget"</span><span class="p">,</span><span class="w">
</span><span class="nl">"shortcode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"example-widget"</span><span class="p">,</span><span class="w">
</span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"v1.0.4"</span><span class="p">,</span><span class="w">
</span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Example Widget"</span><span class="w">
</span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"This is a widget example that showcases some of the features of the JS Widgets project."</span><span class="w">
</span><span class="nl">"directoryUrl"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://static.mateuaguilo.com/widgets/sandbox/example-widget/v1"</span><span class="p">,</span><span class="w">
</span><span class="nl">"files"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"css/main.css"</span><span class="p">,</span><span class="w">
</span><span class="s2">"js/main.js"</span><span class="p">,</span><span class="w">
</span><span class="s2">"media/logo.png"</span><span class="p">,</span><span class="w">
</span><span class="s2">"thumbnail.svg"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"availableTranslations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"en"</span><span class="p">,</span><span class="w">
</span><span class="s2">"de"</span><span class="p">,</span><span class="w">
</span><span class="s2">"es"</span><span class="p">,</span><span class="w">
</span><span class="s2">"pl"</span><span class="p">,</span><span class="w">
</span><span class="s2">"pt"</span><span class="p">,</span><span class="w">
</span><span class="s2">"zh-tw"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"settingsSchema"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"object"</span><span class="p">,</span><span class="w">
</span><span class="nl">"additionalProperties"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"properties"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"fields"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"object"</span><span class="err">...</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>
<p>This file will need to be uploaded somewhere that is accessible via HTTP.</p>
<p>The CMS pulls that JSON object and knows about all the widgets and where to grab their assets. You’ll start seeing widgets appear in your editorial tools.</p>
<p><img src="/assets/images/progressive-decoupling/widget-list.png" alt="Widget list" /></p>
<h2>Ok…but where do I actually start?</h2>
<p>There are lots of existing tooling and examples at <a href="https://github.com/js-widgets" target="_blank">https://github.com/js-widgets</a>. It includes a registry boilerplate and catalog, widget examples, and CI/CD integration.</p>
<p>If you fork it, you’ll get a lot of nice things out of the box.</p>
<h3>Stakeholder-ready catalog</h3>
<p>The same registry that provides information to the CMS can provide information to a single-page application that is browsable and searchable. This requires zero effort. Everyone involved can see what is available: editors, developers, stakeholders, etc.</p>
<p>The catalog can also render a widget as a live sample, even if the widget requires editorial inputs. Examples utilize the “examples” key as shown in the widget definition above.</p>
<p><img src="/assets/images/progressive-decoupling/widget-catalog.png" alt="Widget catalog" /></p>
<h3>Governance like you need it</h3>
<p>All of this might seem like a governance nightmare. Do you really want JavaScript updated in a remote location and immediately deployed to your live site?</p>
<p>Of course not.</p>
<p><em>You</em> decide what registries to accept into your CMS. <em>You</em> decide what widgets and updates go into your registry. You decide who has access to change those widgets and registries.</p>
<h3>Production-ready dependencies</h3>
<p>We want these widgets as light as possible. What if there was a way not to bundle big dependencies in every single JS app? We don’t want to download React for every widget, for example.</p>
<p>Shared dependencies are possible with this paradigm. Widgets can be configured to pull certain dependencies from the parent container. This requires some Webpack configuration and telling the CMS where to find the excluded libraries. <a href="https://github.com/js-widgets/example-widget#external-dependencies" target="_blank">Read the documentation for external dependencies here</a>.</p>
<h2>Conclusion</h2>
<p>We hope this makes you excited to start taking advantage of widgets and progressive decoupling. For more videos on the specifics of setting this up, take a look at these additional videos:</p>
<ul>
<li><a href="https://video.mateuaguilo.com/w/d1ycUz5yCj9CdEGnqKkb9X" target="_blank">Any JS app can be embedded</a></li>
<li><a href="https://video.mateuaguilo.com/w/g2jKiesKjYSreig8abrKFh" target="_blank">The registry and the app catalog</a></li>
<li><a href="https://video.mateuaguilo.com/w/61aG9Y8xW7m5anwupKCL84" target="_blank">Set up Progressive Decoupled Drupal</a></li>
</ul>
<p><small>Photo by <a href="https://unsplash.com/@polarmermaid">Anne Nygård</a> on <a href="https://unsplash.com/@polarmermaid">Unsplash</a></small></p>Mateu Aguiló BoschDecoupling separates the system that stores the content from how that content is displayed on other independent systems. This can come with many benefits but also some downsides and tradeoffs. With progressive decoupling, you can get some of the benefits of decoupling while avoiding some of the downsides.Write better code with Typed Entity2021-05-05T00:00:00+00:002021-05-05T00:00:00+00:00https://mateuaguilo.com/web-development/drupal/2021/05/05/write-better-code-typed-entity<p>I proposed this session to DrupalCon, but it was not selected. I think that is good. I have had my fair share of stage
time in DrupalCons in the past, new contributors should take the lead. However, I still did the work of creating
the presentation, then recorded myself giving the talk.</p>
<!-- more -->
<p><em>This is a re-post of the article on the <a href="https://www.lullabot.com/articles/write-better-code-typed-entity">Lullabot blog</a>.</em></p>
<div class="video-wrapper"><iframe sandbox="allow-same-origin allow-scripts allow-popups" src="https://video.mateuaguilo.com/videos/embed/6da46367-2ac7-40d1-b561-095b991f7dd2" frameborder="0" allowfullscreen=""></iframe></div>
<p><a href="https://mateuaguilo.com/assets/documents/write-better-code-with-typed-entity.pdf">Slides available here</a>.</p>
<p>Drupal projects can be challenging. You need to have a lot of framework-specific knowledge or Drupalisms. Content types, plugins, services, tagged services, hook implementations, service subscribers, and the list goes on. You need to know when to use one and not the other, and that differs from context to context.</p>
<p>It is this flexibility and complexity that allows us to build complex projects with complex needs. Because of this flexibility, it is easy to write code that is hard to maintain.</p>
<p>How do we avoid this? How do we better organize our code to manage this complexity?</p>
<h2 id="framework-logic-vs-business-logic">Framework logic vs. business logic</h2>
<p>To start, we want to keep our framework logic separate from our business logic. What is the difference?</p>
<ul>
<li><strong>Framework logic</strong> - this is everything that comes in Drupal Core and Drupal contrib. It remains the same for every project.</li>
<li><strong>Business logic</strong> - this is what is unique to every project—for example, the process for checking out a book from a library.</li>
</ul>
<p>The goal is to easily demarcate where the framework logic ends and the business logic begins, and vice-versa. The better we can do this, the more maintainable our code will be. We will be able to reason better about the code and more easily write tests for the code.</p>
<h2 id="containing-complexity-with-typed-entity">Containing complexity with Typed Entity</h2>
<p>Complexity is a feature. We need to be able to translate complex business needs to code, and Drupal is very good at allowing us to do that. But that complexity needs to be contained.</p>
<p><a href="https://www.drupal.org/project/typed_entity">Typed Entity</a> is a module that allows you to do this. We want to keep logic close to the entity that logic affects and not scattered around in hooks. You might be altering a form related to the node or doing with access or operate on something related to an entity with a service.</p>
<p>In this example, Book is not precisely a node, but it contains a node of type <em>Book</em> in its $entity property. All the business logic related to <em>Book</em> node types will be contained in this class.</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">final</span> <span class="kd">class</span> <span class="nc">Book</span> <span class="kd">implements</span> <span class="nc">LoanableInterface</span> <span class="p">{</span>
<span class="k">private</span> <span class="k">const</span> <span class="no">FIELD_BOOK_TITLE</span> <span class="o">=</span> <span class="s1">'field_full_title'</span><span class="p">;</span>
<span class="k">private</span> <span class="nv">$entity</span><span class="p">;</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">label</span><span class="p">():</span> <span class="kt">TranslatableMarkup</span> <span class="p">{</span>
<span class="k">return</span> <span class="nv">$this</span><span class="o">-></span><span class="n">entity</span>
<span class="o">-></span><span class="p">{</span><span class="k">static</span><span class="o">::</span><span class="no">FIELD_BOOK_TITLE</span><span class="p">}</span>
<span class="o">-></span><span class="n">value</span> <span class="o">??</span> <span class="nf">t</span><span class="p">(</span><span class="s1">'Title not available'</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">author</span><span class="p">():</span> <span class="kt">Person</span> <span class="p">{</span><span class="mf">...</span><span class="p">}</span>
<span class="k">public</span> <span class="k">function</span> <span class="n">checkAvailability</span><span class="p">():</span> <span class="kt">bool</span> <span class="p">{</span><span class="mf">...</span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Then, in your hooks, services, and plugins, you call those methods. The result: cleaner code.</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// This uses the 'title' base field.</span>
<span class="nv">$title</span> <span class="o">=</span> <span class="nv">$book</span><span class="o">-></span><span class="nf">label</span><span class="p">();</span>
<span class="c1">// An object of type Author.</span>
<span class="nv">$author</span> <span class="o">=</span> <span class="nv">$book</span><span class="o">-></span><span class="nf">owner</span><span class="p">();</span>
<span class="c1">// This uses custom fields on the User entity type.</span>
<span class="nv">$author_name</span> <span class="o">=</span> <span class="nv">$author</span><span class="o">-></span><span class="nf">fullName</span><span class="p">();</span>
<span class="c1">//Some books have additional abilities and relationships</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$book</span> <span class="k">instanceof</span> <span class="nc">LoanableInterface</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$available</span> <span class="o">=</span> <span class="nv">$book</span><span class="o">-></span><span class="nf">checkAvailability</span><span class="p">()</span> <span class="o">===</span> <span class="nc">LoanableInterface</span><span class="o">::</span><span class="no">AVAILABLE</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Business logic for books goes in the <em>Book</em> class. Business logic for your service goes in your service class. And on it goes.</p>
<p>If you are directly accessing field data in various places ($entity->field_foo->value), this is a big clue you need an entity wrapper like Typed Entity.</p>
<h2 id="focusing-on-entity-types">Focusing on entity types</h2>
<p>Wrapping your entities does not provide organization for all of your custom code. In Drupal, however, entity types are the primary integration point for custom business logic. Intentionally organizing them will get you 80% of the way there.</p>
<p>Entities have a lot of responsibilities.</p>
<ul>
<li>They are rendered as content on the screen</li>
<li>They are used for navigation purposes</li>
<li>They hold SEO metadata</li>
<li>They have decorative hints added to them</li>
<li>Their fields are used to group content, like in Views</li>
<li>They can be embedded</li>
</ul>
<h3 id="similar-solutions">Similar solutions</h3>
<p>This concept of keeping business logic close to the entity is not unique. There is a <a href="https://www.drupal.org/project/drupal/issues/2570593">core patch to allow having custom classes for entity bundles</a>.</p>
<p>When you call Node::load(), the method will currently return an instance of the Node class, no matter what type the node is. The patch will allow you to get a different class based on the node type. Node::load(12) will return you an instance of the Book class, for example. This is also what the <a href="https://www.drupal.org/project/bundle_override">Bundle Override</a> module was doing.</p>
<p>There are some drawbacks to this approach.</p>
<ul>
<li><strong>It increments the API surface of entity objects</strong>. You will be able to get an instance of the Book class, but that class will still extend from the <em>Node</em> class. Your <em>Book</em> class will have all of the methods of the <em>Node</em> class, plus your custom methods. These methods could clash when Drupal is updated in the future. Unit testing remains challenging because it must carry over all the storage complexity of the <em>Node</em> class.</li>
<li><strong>It solves the solution only partially.</strong> What about methods that apply to many books? Or different types of books, like <em>SciFiBook</em> or <em>HistoryBook</em>. An <em>AudioBook</em>, for example, would share many methods of <em>Book</em> but be composed differently.</li>
<li><strong>It perpetuates inheritance, even into the application space.</strong> Framework logic bleeds into the application and business logic. This breaks the <a href="https://en.wikipedia.org/wiki/Separation_of_concerns">separation of concerns</a>. You don’t want to own the complexity of framework logic, but this inheritance forces you to deal with it. This makes your code less maintainable. We should favor composition over inheritance.</li>
</ul>
<h3 id="typed-entitys-approach">Typed Entity’s approach</h3>
<p>You create a plugin and associate it to an Entity Type and Bundle. These are called Typed Repositories. Repositories operate at the entity type level, so they are great for methods like findTaggedWith(). Methods that don’t belong to a specific book would go into the book repository. Bulk operations are another good example.</p>
<drupal-entity data-embed-button="media_entity_embed" data-entity-embed-display="view_mode:media.wide" data-entity-embed-display-settings="" data-entity-type="media" data-entity-uuid="913492df-31ca-4066-90c0-6e6dccf23180" data-langcode="en"></drupal-entity>
<p>Typed Entity is meant to help organize your project’s custom code while improving maintainability. It also seeks to optimize the developer experience while they are working on your business logic.</p>
<p>To maximize these goals, some tradeoffs have been made. These tradeoffs are consequences of how Drupal works and a desire to be pragmatic. While theory can help, we want to make sure things work well when the rubber meets the road. We want to make sure it is easy to use.</p>
<h2 id="typed-entity-examples">Typed Entity examples</h2>
<p>Your stakeholder comes in and gives you a new requirement: “Books located in Area 51 are considered off-limits.”</p>
<p>You have started using Typed Entity, and this is what your first approach looks like:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cd">/**
* Implements hook_node_access().
*/</span>
<span class="k">function</span> <span class="n">physical_media_node_access</span><span class="p">(</span><span class="kt">NodeInterface</span> <span class="nv">$node</span><span class="p">,</span> <span class="nv">$op</span><span class="p">,</span> <span class="kt">AccountInterface</span> <span class="nv">$account</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$node</span><span class="o">-></span><span class="nb">getType</span><span class="p">()</span> <span class="o">!==</span> <span class="s1">'book'</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nv">$book</span> <span class="o">=</span> <span class="err">\</span><span class="nc">Drupal</span><span class="o">::</span><span class="nf">service</span><span class="p">(</span><span class="nc">RepositoryManager</span><span class="o">::</span><span class="n">class</span><span class="p">)</span><span class="o">-></span><span class="nf">wrap</span><span class="p">(</span><span class="nv">$node</span><span class="p">);</span>
<span class="nb">assert</span><span class="p">(</span><span class="nv">$book</span> <span class="k">instanceof</span> <span class="nc">FindableInterface</span><span class="p">);</span>
<span class="nv">$location</span> <span class="o">=</span> <span class="nv">$book</span><span class="o">-></span><span class="nf">getLocation</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="nv">$location</span><span class="o">-></span><span class="nf">getBuilding</span><span class="p">()</span> <span class="o">===</span> <span class="s1">'area51'</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nc">AccessResult</span><span class="o">::</span><span class="nf">forbidden</span><span class="p">(</span><span class="s1">'Nothing to see.'</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nc">AccessResult</span><span class="o">::</span><span class="nf">neutral</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>
<p>You already have a physical_media module, so you implement an access hook. You are using the global repository manager that comes with Typed Entity to wrap the incoming $node and then call some methods on that Wrapped Entity to determine its location.</p>
<p>This is a good start. But there are some improvements we can make.</p>
<p>We want the entity logic closer to the entity. Right now, we have logic about “book” in a hook inside physical_media.module. We want that logic inside the <em>Book</em> class.</p>
<p>This way, our access hook can check on any Wrapped Entity and not care about any internal logic. It should care about physical media and not books specifically. It certainly shouldn’t care about something as specific as an “area51” string.</p>
<ul>
<li>Does this entity support access checks?</li>
<li>If so, check it.</li>
<li>If not, carry on.</li>
</ul>
<p>Here is a more refined approach:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span> <span class="n">physical_media_node_access</span><span class="p">(</span><span class="kt">NodeInterface</span> <span class="nv">$node</span><span class="p">,</span> <span class="nv">$op</span><span class="p">,</span> <span class="kt">AccountInterface</span> <span class="nv">$account</span><span class="p">)</span> <span class="p">{</span>
<span class="k">try</span> <span class="p">{</span>
<span class="nv">$wrapped_node</span> <span class="o">=</span> <span class="nf">typed_entity_repository_manager</span><span class="p">()</span><span class="o">-></span><span class="nf">wrap</span><span class="p">(</span><span class="nv">$node</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">catch</span> <span class="p">(</span><span class="nc">RepositoryNotFoundException</span> <span class="nv">$exception</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nc">AccessResult</span><span class="o">::</span><span class="nf">neutral</span><span class="p">();</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nv">$wrapped_node</span> <span class="k">instanceof</span> <span class="nc">AccessibleInterface</span>
<span class="o">?</span> <span class="nv">$wrapped_node</span><span class="o">-></span><span class="nf">access</span><span class="p">(</span><span class="nv">$op</span><span class="p">,</span> <span class="nv">$account</span><span class="p">,</span> <span class="kc">TRUE</span><span class="p">)</span>
<span class="o">:</span> <span class="nc">AccessResult</span><span class="o">::</span><span class="nf">neutral</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>
<p>If there is a repository for the $node, wrap the entity. If that $wrapped_entity has an access() method, call it. Now, this hook works for all Wrapped Entities that implement the AccessibleInterface.</p>
<p>This refinement leads to better:</p>
<ul>
<li>Code organization</li>
<li>Readability</li>
<li>Code authoring/discovery (which objects implement AccessibleInterface)</li>
<li>Class testability</li>
<li>Static analysis</li>
<li>Code reuse</li>
</ul>
<h2 id="how-does-typed-entity-work">How does Typed Entity work?</h2>
<p>So far, we’ve only shown typed_entity_repository_manager()->wrap($node). This is intentional. If you are only working on the layer of an access hook, you don’t <em>need</em> to know how it works. You don’t have to care about the details. This information hiding is part of what helps create maintainable code.</p>
<p>But you want to write better code, and to understand the concept, you want to understand how Typed Entity is built.</p>
<p>So how does it work under the hood?</p>
<p>This is a declaration of a Typed Repository for our <em>Book</em> entities:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cd">/**
* The repository for books.
*
* @TypedRepository(
* entity_type_id = "node",
* bundle = "book",
* wrappers = @ClassWithVariants(
* fallback = "Drupal\my_module\WrappedEntities\Book",
* variants = {
* "Drupal\my_module\WrappedEntities\SciFiBook",
* }
* ),
* description = @Translation("Repository that holds business logic")
* )
*/</span>
<span class="k">final</span> <span class="kd">class</span> <span class="nc">BookRepository</span> <span class="kd">extends</span> <span class="nc">TypedRepositoryBase</span> <span class="p">{</span><span class="mf">...</span><span class="p">}</span>
</code></pre></div></div>
<p>The “wrappers” key defines which classes will wrap your Node Type. There are different types of books, so we use ClassWithVariants, which has a fallback that refers to our main <em>Book</em> class. The repository manager will now return the <em>Book</em> class or one of the variants when we pass a book node to the ::wrap() method.</p>
<p>More on variants. We often attach special behavior to entities with specific data, and that can be data that we cannot include statically. It might be data entered by an editor or pulled in from an API. Variants are different types of books that need some shared business logic (contained in <em>Book</em>) but also need business logic unique to them.</p>
<p>We might fill out the variants key like this:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">variants</span> <span class="o">=</span> <span class="p">{</span>
<span class="s2">"Drupal\my_module\WrappedEntities\SciFiBook"</span><span class="p">,</span>
<span class="s2">"Drupal\my_module\WrappedEntities\BestsellerBook"</span><span class="p">,</span>
<span class="s2">"Drupal\my_module\WrappedEntities\AudioBook"</span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div></div>
<p>How does Typed Entity know which variant to use? Via an ::applies() method. Each variant must implement a specific interface that will force the class to implement ::applies(). This method gets a $context which contains the entity object, and you can check on any data or field to see if the class applies to that context. An ::applies() method returns TRUE or FALSE.</p>
<p>For example, you might have a Taxonomy field for Genre, and one of the terms is “Science Fiction.”</p>
<drupal-entity data-embed-button="media_entity_embed" data-entity-embed-display="view_mode:media.wide" data-entity-embed-display-settings="" data-entity-type="media" data-entity-uuid="14fcdbd6-d623-413f-b9e1-47a228e69213" data-langcode="en"></drupal-entity>
<h3 id="implementing-hooks">Implementing hooks</h3>
<p>We can take this organization even further. There are many entity hooks, and Typed Entity can implement these hooks and delegate the logic to interfaces. The logic remains close to the Wrapped Entity that implements the appropriate interface.</p>
<p>The following example uses a hypothetical hook_entity_foo().</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cd">/**
* Implements hook_entity_foo().
*/</span>
<span class="k">function</span> <span class="n">typed_entity_entity_foo</span><span class="p">(</span><span class="nv">$entity</span><span class="p">,</span> <span class="nv">$data</span><span class="p">)</span> <span class="p">{</span>
<span class="nv">$wrapped</span> <span class="o">=</span> <span class="nf">typed_entity_repository_manager</span><span class="p">()</span><span class="o">-></span><span class="nf">wrap</span><span class="p">(</span><span class="nv">$entity</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$wrapped</span> <span class="k">instanceof</span> <span class="err">\</span><span class="nc">Drupal\typed_entity\Fooable</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// if the entity not fooable, then we can't foo it</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nv">$wrapped</span><span class="o">-></span><span class="nf">fooTheBar</span><span class="p">(</span><span class="nv">$data</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This type of implementation could be done for any entity hook.</p>
<p>Is this a good idea? Yes and no.</p>
<p><strong>No</strong>, because Typed Entity doesn’t want to replace the hook system. Typed Entity wants to help you write better code that is more efficient to maintain. Reimplementing all of the hooks (thousands of them?) as interfaces doesn’t further this goal.</p>
<p><strong>Yes</strong>, because you could do this for your own codebase where it makes sense, keeping it simple and contained. And yes, because Typed Entity does make an exception for hooks related to rendering entities.</p>
<h3 id="rendering-entities">Rendering entities</h3>
<p>The most common thing we do with entities is to render them. When rendering entities, we already have variants called “<a href="https://www.drupal.org/docs/8/api/entity-api/display-modes-view-modes-and-form-modes">view modes</a>” that apply in specific contexts.</p>
<p>This is starting to sound familiar. It sounds like a different type of wrapped object could overlay this system and allow us to organize our code further. This would let us put everything related to rendering an entity type (preprocess logic, view alters, etc.) into its own wrapped object, called a <strong>renderer</strong>. We don’t have to stuff all of our rendering logic into one Wrapped Entity class.</p>
<p>Typed Entity currently supports three of these hooks:</p>
<ul>
<li>hook_entity_view_alter()</li>
<li>hook_preprocess()</li>
<li>hook_entity_display_build_alter()</li>
</ul>
<p>Renderers are declared in the repositories. Taking our repository example from above, we add a “renderers” key:</p>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cd">/**
* The repository for books.
*
* @TypedRepository(
* entity_type_id = "node",
* bundle = "book",
* wrappers = @ClassWithVariants(
* fallback = "Drupal\my_module\WrappedEntities\Book",
* variants = {
* "Drupal\my_module\WrappedEntities\SciFiBook",
* }
* ),
* renderers = @ClassWithVariants(
* fallback = "Drupal\my_module\Renderers\Base",
* variants = {
* "Drupal\my_module\Renderers\Teaser",
* }
* ),
* description = @Translation("Repository that holds business logic")
* )
*/</span>
<span class="k">final</span> <span class="kd">class</span> <span class="nc">BookRepository</span> <span class="kd">extends</span> <span class="nc">TypedRepositoryBase</span> <span class="p">{</span><span class="mf">...</span><span class="p">}</span>
</code></pre></div></div>
<p>If you understand wrappers, you understand renderers.</p>
<p>The <em>TypedEntityRendererBase</em> has a default ::applies() method to check the view mode being rendered and select the proper variant. See below:</p>
<drupal-entity data-embed-button="media_entity_embed" data-entity-embed-display="view_mode:media.wide" data-entity-embed-display-settings="" data-entity-type="media" data-entity-uuid="80de445f-9a98-42cc-ae3c-0ced408188cb" data-langcode="en"></drupal-entity>
<p>These renderers are much easier to test than individual hook implementations, as you can mock any of the dependencies.</p>
<h2 id="summary">Summary</h2>
<p>Typed Entity can help you make your code more testable, discoverable, maintainable, and readable. Specifically, it can help you:</p>
<ul>
<li><strong>Encapsulate</strong> your business logic in wrappers</li>
<li>Add <strong>variants</strong> (if needed) for specialized business logic</li>
<li>Check for wrapper <strong>interfaces</strong> when implementing hooks/services</li>
<li>Use <strong>renderers</strong> instead of logic in rendering-specific hooks</li>
<li>Add variants per <strong>view mode</strong>.</li>
</ul>
<p>All of this leads to a codebase that is easier to expand and cheaper to maintain.</p>
<p><small>Photo by <a href="https://unsplash.com/@jstrippa">James Harrison</a> on <a href="https://unsplash.com/@jstrippa">Unsplash</a></small></p>Mateu Aguiló BoschI proposed this session to DrupalCon, but it was not selected. I think that is good. I have had my fair share of stage time in DrupalCons in the past, new contributors should take the lead. However, I still did the work of creating the presentation, then recorded myself giving the talk.How I fixed, “Qt apps are too small in GNOME”2021-02-22T00:00:00+00:002021-02-22T00:00:00+00:00https://mateuaguilo.com/notes/2021/02/22/fix-qt-apps-resolution-fedora<p>I am a GNOME user, but I still use fantastic apps written using the Qt framework. However in my HiDPI screen (retina screen for macOS people) the Qt apps were rendering too small. They were not applying the scaling factor I requested in my GNOME settings.</p>
<!-- more -->
<p><img src="/assets/images/gnome-scale-factor.png" alt="Scale factor in GNOME" /></p>
<p>In order for Qt apps to have the correct size I created a script <code class="language-plaintext highlighter-rouge">/etc/profile.d/hidpi.sh</code>. I created it by executing:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo echo 'export QT_AUTO_SCREEN_SCALE_FACTOR=1' >> /etc/profile.d/hidpi.sh
</code></pre></div></div>
<p>After a computer restart, my Qt applications were showing correctly as usual. I did this in my Fedora 33 Workstation.</p>Mateu Aguiló BoschI am a GNOME user, but I still use fantastic apps written using the Qt framework. However in my HiDPI screen (retina screen for macOS people) the Qt apps were rendering too small. They were not applying the scaling factor I requested in my GNOME settings.Notify me if no VPN is active2021-02-10T00:00:00+00:002021-02-10T00:00:00+00:00https://mateuaguilo.com/privacy/2021/02/10/notify-me-if-no-vpn-active<p>I have <a href="/privacy/2019/12/27/network-editor/">blogged about ensuring you connect to a VPN</a> when
connecting to certain wi-fi networks. I have also <a href="/privacy/2020/10/14/connect-vpn-whent-computer-boots/">blogged about connecting to a VPN when the
computer boots</a>. Both of those methods
have merit, but also some inconveniences, like conflicting VPN profiles, or difficulty connecting
on boot (before the password keyring for the VPN password is ready), etc.</p>
<p>I simplified my solution quite a bit, and I made it more resilient.</p>
<!-- more -->
<p>One of the things I said in my previous post <a href="/notes/2020/03/12/gnome-vpn-notification/">I want a GNOME notification when not connected to VPN </a>:</p>
<blockquote>
<p>I would like to have a GNOME extension that pops a notification whenever I am not connected to a VPN.</p>
</blockquote>
<p>Today I made that myself, and it was quite simple.</p>
<p>I started making a script that checks if there is a VPN network interface active. If not, then it uses <code class="language-plaintext highlighter-rouge">notify-send</code> to
send a notification. Finally, I run that check every 5 minutes using cron.</p>
<p><img src="/assets/images/missing-vpn-notification.png" alt="The notification" /></p>
<h2 id="how-to-do-it">How to do it</h2>
<p>This is the script <code class="language-plaintext highlighter-rouge">check-vpn-status.sh</code>:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="k">if</span> <span class="o">[</span> <span class="si">$(</span>/usr/sbin/ip address|grep <span class="s2">"[0-9][0-9]*: tun[0-9][0-9]*: "</span>|wc <span class="nt">-l</span><span class="si">)</span> <span class="nt">-eq</span> 0 <span class="o">]</span><span class="p">;</span> <span class="k">then
</span>notify-send <span class="nt">-u</span> critical <span class="s2">"VPN connection not detected."</span> <span class="nt">-a</span> OpenVPN<span class="p">;</span>
<span class="k">fi</span>
</code></pre></div></div>
<p>I have seen that <code class="language-plaintext highlighter-rouge">notify-send</code> sometimes does not show notifications when running from cron. I have seen that in
Ubuntu. If that is your case, try adding <code class="language-plaintext highlighter-rouge">XDG_RUNTIME_DIR=/run/user/$(id -u)</code> at the end.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="k">if</span> <span class="o">[</span> <span class="si">$(</span>/usr/sbin/ip address|grep <span class="s2">"[0-9][0-9]*: tun[0-9][0-9]*: "</span>|wc <span class="nt">-l</span><span class="si">)</span> <span class="nt">-eq</span> 0 <span class="o">]</span><span class="p">;</span> <span class="k">then
</span><span class="nv">XDG_RUNTIME_DIR</span><span class="o">=</span>/run/user/<span class="si">$(</span><span class="nb">id</span> <span class="nt">-u</span><span class="si">)</span> notify-send <span class="nt">-u</span> critical <span class="s2">"VPN connection not detected."</span> <span class="nt">-a</span> OpenVPN<span class="p">;</span>
<span class="k">fi</span>
</code></pre></div></div>
<p>Then type <code class="language-plaintext highlighter-rouge">crontab -e</code> to edit your cron jobs and add the following line:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">*</span>/5 <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> /bin/bash /home/e0ipso/.config/check-vpn-status.sh
</code></pre></div></div>
<p>Of course, you will need to change <code class="language-plaintext highlighter-rouge">/home/e0ipso/.config</code> to the actual location of your script.</p>
<p>That’s it!</p>
<p><small>Photo by <a href="https://unsplash.com/@extaf_ms?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Misha Feshchak</a> on <a href="https://unsplash.com/s/photos/cybersecurity?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></small></p>Mateu Aguiló BoschI have blogged about ensuring you connect to a VPN when connecting to certain wi-fi networks. I have also blogged about connecting to a VPN when the computer boots. Both of those methods have merit, but also some inconveniences, like conflicting VPN profiles, or difficulty connecting on boot (before the password keyring for the VPN password is ready), etc. I simplified my solution quite a bit, and I made it more resilient.Practical Progressive Decoupling Explained2021-01-27T00:00:00+00:002021-01-27T00:00:00+00:00https://mateuaguilo.com/web-development/drupal/2021/01/27/practical-progressive-decoupling-explained-video<p>I recently recorded a video series tutorial about progressive Drupal decoupling. In this series I
take two of the official React app examples and turn them into <em>widgets</em>. Your Drupal editorial team
can then embed those React applications (a calculator, and an emoji selector) as blocks in a page, as
a field in a content type, as an embedded entity in the body field using the WYSIWYG, …</p>
<!-- more -->
<h2 id="1-embed-any-javascript-application">#1 Embed Any JavaScript Application</h2>
<p>In this first video in the series we will take one of the offical examples from react and we will turn it into a widget ready to be embedded in Drupal (or anywhere else).</p>
<p>Steps</p>
<ol>
<li>Create new repository from template.</li>
<li>Migrate source to new repo.
<ul>
<li>Copy new source files.</li>
<li>Adapt index.js (including render function).</li>
<li>Combine package.json.</li>
<li>Find & replace «widget-example».</li>
<li>Remove / add specific features.</li>
</ul>
</li>
<li>Reformat and execute tests.</li>
<li>Execute locally.</li>
<li>Deploy application.</li>
</ol>
<div class="video-wrapper"><iframe sandbox="allow-same-origin allow-scripts allow-popups" src="https://video.mateuaguilo.com/videos/embed/614182d9-7107-4dbd-b024-c9798d8d8457" frameborder="0" allowfullscreen=""></iframe></div>
<h2 id="2-the-registry--the-app-catalog">#2 The Registry & the App Catalog</h2>
<p>The widget registry is the place where you aggregate your widgets (and other people’s widgets you want to use) to make them discoverable to Drupal and other CMS integrations.</p>
<p>This piece plays a fundamental role in the governance of your project(s). You can choose to have a single registry for all your Drupal installations, or one registry per project. You can use the pull requests to gatekeep what versions are added to the registry and who can publish them. The idea is that the owner of the widget-registry project has the authority of accepting PRs to add/update widgets so they are available in the registry (and therefore in Drupal).</p>
<div class="video-wrapper"><iframe sandbox="allow-same-origin allow-scripts allow-popups" src="https://video.mateuaguilo.com/videos/embed/79a86069-a5ea-4daf-9b0e-67dfc442915a" frameborder="0" allowfullscreen=""></iframe></div>
<h2 id="3-set-up-progressive-decoupled-drupal">#3 Set up Progressive Decoupled Drupal</h2>
<p>In this video we will learn how to connect Drupal and the widget registry to let editors embed JS applications all over Drupal (that includes support for i18n!).</p>
<p>You can, for instance, embed JS applications as blocks, as a field for a content type, in the body field as an entity embed, …</p>
<div class="video-wrapper"><iframe sandbox="allow-same-origin allow-scripts allow-popups" src="https://video.mateuaguilo.com/videos/embed/2883aa99-8438-48c5-8cd8-ccbc844f0d79" frameborder="0" allowfullscreen=""></iframe></div>
<p><small>Photo by <a href="https://unsplash.com/@sotti?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Shifaaz shamoon</a> on <a href="https://unsplash.com/collections/4687121/increment-collection?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></small></p>Mateu Aguiló BoschI recently recorded a video series tutorial about progressive Drupal decoupling. In this series I take two of the official React app examples and turn them into widgets. Your Drupal editorial team can then embed those React applications (a calculator, and an emoji selector) as blocks in a page, as a field in a content type, as an embedded entity in the body field using the WYSIWYG, …Let’s not choose sides on Decoupled Drupal2021-01-23T18:12:00+00:002021-01-23T18:12:00+00:00https://mateuaguilo.com/notes/2021/01/23/let-s-not-choose-sides-on-decoupled-drupal<p>Yesterday I had an interesting conversation about the principles of decoupled Drupal with Gabe Sullice in the context of menus. We agree that decoupled Drupal should not be only about websites, there are plenty solutions for that already. Not picking sides with the web prepares Drupal for the future. However, we hold slightly different visions on how to go about this.</p>
<p>What is your view?</p>
<p>I pasted the Slack conversation here in case anyone is interested in it.</p>
<!-- more -->
<hr />
<p><strong>gabesullice</strong></p>
<p>I’m writing an issue to add authenticate and logout links to JSON:API responses. But I can already anticipate a bikeshed over which URL to put in there.</p>
<p>https://drupal.slack.com/archives/C016N3HTHRD/p1611328043014700?thread_ts=1611262827.003000&cid=C016N3HTHRD</p>
<blockquote>
<p>e0ipso</p>
<p>We should care beyond web developers in this initiative.
From a thread in #decoupled-menus-initiative</p>
</blockquote>
<p>If you have any idea about how to make it flexible without making it complicated, LMK</p>
<p><strong>e0ipso</strong></p>
<p>I think that we should be explicit where Drupal is:</p>
<ul>
<li>Web only</li>
<li>Web favoring</li>
<li>Consumer agnostic</li>
</ul>
<p>Ideally we should be consumer agnostic, but I fear that’s a lost battle already
I find myself constantly wanting to post a reminder about it and I sense only annoyance at that fact. Probably rightly so 😛</p>
<p><strong>gabesullice</strong></p>
<p>I’m not annoyed and I don’t think anyone is annoyed 🙂</p>
<p><strong>e0ipso</strong></p>
<p>I think the echo chamber effect is huge in this case we are a community of people that make a living building websites, and our mental model will keep falling back to that.</p>
<p><strong>gabesullice</strong></p>
<p>It would help me understand/share your concern if you could give some examples.</p>
<p><strong>e0ipso</strong></p>
<p>My concerns are that when we start favoring the web there is no coming back.</p>
<p><strong>gabesullice</strong></p>
<p>Is there a difference between favoring HTTP and favoring the web?</p>
<p><strong>e0ipso</strong></p>
<p>Yes.</p>
<p>The web is HTTP + HTML + a Browser. To remain consumer agnostic, we should stay at HTTP IMO.</p>
<p>Do you see it differently?</p>
<p><strong>gabesullice</strong></p>
<p>I don’t think there’s a difference. Browsers are just scriptable HTTP clients, and I think it’s okay for us to provide extra goodies for the most widely used HTTP client in the way that many companies provide “official” libraries and “community” libraries in less-used languages.</p>
<p><strong>e0ipso</strong></p>
<p>thinks</p>
<p><strong>gabesullice</strong></p>
<p>It’s why I’m so bullish on cookie auth, for example. It’s an HTTP spec. Not a browser spec, even though that’s where they’re most widely used.</p>
<p><strong>e0ipso</strong></p>
<p>I don’t think I agree with Browsers are just scriptable HTTP clients. My main objection is with the keyword just.</p>
<p><strong>gabesullice</strong></p>
<p>nods</p>
<p><strong>e0ipso</strong></p>
<blockquote>
<p>It’s why I’m so bullish on cookie auth, for example. It’s an HTTP spec. Not a browser spec, even though that’s where they’re most widely used.</p>
</blockquote>
<p>Again, this is a good example. All browsers have specific guardrails against the perils of cookies and the assumptions devs make around them. You cannot guarantee that in other technologies.</p>
<p><strong>gabesullice</strong></p>
<p>But the other mechanisms are no safer, AFAIK.</p>
<p>They all boil down to storing a secret on the client, somewhere.</p>
<p><strong>e0ipso</strong></p>
<p>Not arguing against that, but you’ll have to admit that cookie auth is unique in having all HTTP requests originating from a client -to a host-
attaching the credentials without the user, or the developer, doing anything. Even [when doing] <code class="language-plaintext highlighter-rouge"><img src="http://bankofamerica.com/donate-to-gabe"></code>. Not saying there isn’t a good reason but this seems to be almost unique to browsers</p>
<p>➕1</p>
<p><strong>gabesullice</strong></p>
<p>connect the dots for me.</p>
<p><strong>e0ipso</strong></p>
<p><code class="language-plaintext highlighter-rouge">.........</code></p>
<p><code class="language-plaintext highlighter-rouge">---------</code></p>
<p><strong>gabesullice</strong></p>
<p>lol</p>
<p>I totally agree with what you just said, that the security concern w/ attaching the secret to every request on non-origin domains is unwise and that it’s limited to browsers.
I don’t see what implications that has though.</p>
<p>Perhaps you’re saying that we should not use cookies and instead use Authorization headers to work around that leaky architecture, but that seems like favoring browsers.
Letting browsers influence the design by making auth more difficult for non-browsers (because cookies are easier IMO)</p>
<p><strong>e0ipso</strong></p>
<p>I don’t see that way 🤔</p>
<p>I see it as not taking a position in something that is not standard across technologies, therefore not favoring anything. Suffice to say that I am not saying we should not do it, rather ensure that we own the decision with a big billboard.</p>
<p>I think this is one of two things we never managed to agree upon. That’s a good thing, that means that when we reach agreement in other things, that is genuine agreement.</p>
<p>🙂</p>
<p>Also, is it OK if I publish this conversation in my blog?
[yes]</p>
<p><strong>gabesullice</strong></p>
<p>I don’t think we disagree 😛
I began by saying:</p>
<blockquote>
<p>If you have any idea about how to make it flexible without making it complicated, LMK</p>
</blockquote>
<p>I want to make the authentication/logout links not favor cookies (but I think cookies should be the default) maybe that’s a contradiction 😛</p>
<p><strong>e0ipso</strong></p>
<p>ha!</p>
<p>https://en.wikipedia.org/wiki/Nudge_%28book%29</p>
<blockquote>
<p>Wikipedia Nudge (book)
Nudge: Improving Decisions about Health, Wealth, and Happiness is a book written by University of Chicago economist Richard H. Thaler and Harvard Law School Professor Cass R. Sunstein, first published in 2008.
The book draws on research in psychology and behavioral economics to defend libertarian paternalism and active engineering of choice architecture.The book received largely positive reviews. The Guardian described it as “never intimidating, always amusing and elucidating: a jolly economic romp but with serious lessons within.” It was named one of the best books of 2008 by The Economist.</p>
</blockquote>
<p>According to this book, as a choice architect, there is nothing more powerful than defaults to favor an outcome.
FWIW we’ve been exercising libertarian paternalism for a long time now in our little corner of influence, and that is OK. No choice is a choice.</p>
<p><strong>gabesullice</strong></p>
<p>I just realized that the User account menu already has log in/out links. If we can attach menus to JSON:API responses, we don’t need to have special code to attach authentication links.</p>
<p><a href="https://www.drupal.org/project/simple_oauth">OAuth module</a> could override these links or provide its own.</p>
<p>https://git.drupalcode.org/project/drupal/-/blob/9.2.x/core/modules/user/user.links.menu.yml#L6</p>
<p><strong>e0ipso</strong></p>
<p>See! It was easy to start with. We just didn’t know yet. 😛</p>Mateu Aguiló BoschYesterday I had an interesting conversation about the principles of decoupled Drupal with Gabe Sullice in the context of menus. We agree that decoupled Drupal should not be only about websites, there are plenty solutions for that already. Not picking sides with the web prepares Drupal for the future. However, we hold slightly different visions on how to go about this. What is your view? I pasted the Slack conversation here in case anyone is interested in it.Contenta CMS is now on Drupal 92021-01-06T00:00:00+00:002021-01-06T00:00:00+00:00https://mateuaguilo.com/notes/drupal/2021/01/06/contenta-cms-drupal-9.md<p>You may know <a href="https://www.contentacms.org">Contenta CMS</a>. It is a decoupled Drupal starter kit.
It is aimed to teach developers, and site builders, about the tools available in the decoupled
landscape. It also serves as an example for both Drupal and client side best practices.</p>
<p>It took a while, but <a href="https://github.com/contentacms/contenta_jsonapi">the distribution</a> and all
the associated projects are now running on Drupal 9.</p>
<!-- more -->
<p><img src="/assets/images/contentacms-d9.png" alt="Status report screenshot" /></p>Mateu Aguiló BoschYou may know Contenta CMS. It is a decoupled Drupal starter kit. It is aimed to teach developers, and site builders, about the tools available in the decoupled landscape. It also serves as an example for both Drupal and client side best practices. It took a while, but the distribution and all the associated projects are now running on Drupal 9.