Blog https://agaric.coop/ en Always run the desired PHP version when your hosting solution has multiple versions available https://agaric.coop/ <section data-headerbg="transparent" class="hero is-cinnamon hero-project"> <div class="container"> <div class="hero-image"> <img loading="lazy" src="/sites/default/files/styles/max_650x650/public/title-image/2023-02/elephant-1_0.jpg?itok=GM8PU5D7" width="433" height="650" alt="A black and white photograph of an elephant&#039;s head." typeof="foaf:Image" /> </div> <div class="hero-body"> <h1 class="title">Always run the desired PHP version when your hosting solution has multiple versions available </h1> <h2 class="subtitle">"Oh, you wanted *that* PHP?" </h2> </div> </div> </section> , <div class="flow_middle"> <p>At Agaric, we perform a lot of Drupal upgrades. These very often involve transitioning away from older versions of PHP. Even when your hosting service provides multiple versions of PHP, you can still run into issues activating the appropriate one for each site: whether that's within the web server, at the command line, or via Drush (or other tools). In this blog post, we'll be providing remedies for all of these cases. Most content applies to any PHP application.</p> </div> , <div class="flow_middle"> <p>Only Section 3 is particularly Drupal/Drush specific, but still might give you a hint about running remote commands that use ssh from behind their shiny CLI.</p> <h2>TLDR: just give me what works, NOW (please?)</h2> <p>Since you asked so politely: if you are already familiar with hosting options, bash shell configuration, and Drush, this somewhat lengthy article can be summed up quite quickly, really. For those that might not be pro's in any one of these, you can take this as the quick intro, and keep reading to learn why these situations exist, as well as the detailed fix.</p> <ol><li>If you are on a hosting provider, they normally will have a UI to select the desired PHP version. If you are doing your own hosting, install Ondrej Sury's PHP PPA/Apt repo, and install PHP-FPM (not mod_php). Adjust your SetHandler (Apache), or fastcgi_pass (NGINX) in each site configuration to the socket for the pool of the version of PHP you want.</li> <li>Configure a local ~/bin folder and place a link to the php version you want (e.g. ln -s /usr/bin/php8.1 ~/bin/php) there. Be certain ~/bin is also in your PATH.</li> <li>When using Drush aliases to run commands on remote instances, this still won't work until you add an env-vars: key to your site.yml file and place a PATH in there.</li> </ol><p>Jump to the "Part" below if you only need help with one or more of these.</p> <h2>Part 1: All things hosting (well, at least Apache or NGINX, plus FPM)</h2> <p>Let's start where most people do: setting up the hosting itself. In most cases, a managed hosting provider will have some way to select the appropriate version of PHP for your individual site. If that's the case - pick your target, and you should be good to go (move on to Part 2, you lucky dog)! If you do your own hosting, there will be some additional steps to take, which I'll give an overview of, and some additional resources to get you over this first hurdle.</p> <p>If you are running "<a href="http://xahlee.info/UnixResource_dir/open_source_rewrite_history.html">A-PAtCHy</a>" web server (<a href="https://arstechnica.com/gadgets/2023/01/indigenous-tech-group-asks-apache-foundation-to-change-its-name/">possible name change coming?</a>) you will not be able to use <code>mod_php</code> for your PHP duties, as this method does not allow the web server to directly serve content of different virtual hosts using different versions of PHP. Instead, I recommend using PHP's "<a href="https://www.php.net/manual/en/install.fpm.php">FastCGI Process Manager</a>" service - aka FPM. This is a stand-alone service that Apache and NGINX will speak to using new-age technology from 1996 called <a href="https://fastcgi-archives.github.io/">FastCGI</a>. It's still technically CGI, only, like, <em>Fast</em> (seriously, it works <em>really</em> well). Your web server hands off to this service with it's related FastCGI/proxy module.</p> <p>The process is quite similar for both web servers, and an article <a href="https://www.linode.com/docs/guides/install-php-8-for-apache-and-nginx-on-ubuntu/">over at Linode</a> covers the basics of this method for each<em>, but wait!</em> Finish reading at least this paragraph before you jump over there for both a caveat emptor and then some Debian specific derivations, if you need those (the article is Ubuntu-specific). In the article, they utilize the excellent PHP resources offered by <a href="https://github.com/oerdnj">Ondřej Surý</a>. From this PPA/Debian APT resource, you can run concurrent installations of any of the following PHP versions (listed as of this writing): 5.6, 7.0, 7.1, 7.2, 7.3, 7.4, 8.0, 8.1, and 8.2. Do keep in mind, however, that versions prior to 8.0 are <a href="https://www.php.net/supported-versions.php">[per php.net]</a> now past their supported lifetime and no longer actively developed (see also <a href="https://github.com/oerdnj/deb.sury.org/wiki/Frequently-Asked-Questions#supported-distributions">this FAQ</a> for more details). Debian-specific instructions for setting up this repository in apt (as opposed to Ubuntu PPA support) are also found within <a href="https://github.com/oerdnj/deb.sury.org/wiki/Frequently-Asked-Questions#how-to-enable-the-debsuryorg-repository">Ondřej's instructions</a>. The remainder of the Linode process should still apply with that one change. OK, run along and get the PHP basics talking to your web server. If you'll be running Drupal (why wouldn't you?) then you'll want to ensure you have the version-specific php modules that <a href="https://www.drupal.org/docs/system-requirements/php-requirements">it requires</a> (this is for Drupal 9+, but links to earlier revisions also).  I'll wait...</p> <p>Assuming you have now progressed as far as having both versions of PHP you need installed, and followed the article from Linode above or whatever your favorite substitute source was, you likely noticed the special sauce that connects the web server to a particular PHP FPM service. In Apache, we have: <code>SetHandler "proxy:unix:/var/run/php/php8.0-fpm.sock|fcgi://localhost"</code>, and in NGINX flavor, it's: <code>fastcgi_pass unix:/var/run/php/php8.0-fpm.sock;</code> These directives point to the Unix socket as defined for in the default PHP "pool" for the particular version. For reference, these are defined in <code>/etc/php/{version}/fpm/pool.d/www.conf</code> and therein will look like: <code>listen = /var/run/php/php8.1-fpm.sock</code>. So - all that's necessary to select your PHP version for your web server is to point to whichever socket location for the version of PHP you want.</p> <p>The Linode article does not go into handling multiple host names, and I won't go too deep here either as I've already navigated headlong into a bit of a scope-creep iceberg. The quick-and-dirty: for Apache, add another site configuration (as in, add another your_site.conf in /etc/apache/sites-available, and link to it from sites-enabled) repeating the entire <code>VirtualHost</code> and everything inside it, however, use a different listen port, or the same port, and add the <code>ServerName</code> directive to specify the unique DNS name. Likewise, with NGINX, except you here you repeat the full <code>server</code> block in another configuration, and changing the <code>listen</code> and/or <code>server_name</code> bits. Oh yeah - you'll probably be changing the folder location of the Drupal installation in there too, that should definitely help reduce some confusion.</p> <p>Phew - we should have the web server out of the way now!</p> <h2>Part 2: BASH-ing the `php` command</h2> <p>Next up: PHP on your command line. Here, I'm referring to what you get when you type <code>php</code> on your command line once logged in (via ssh) as the user that manages the web site. In this section, I'm assuming that per best-practices, there are different users for each site. This method does not help much if you have but one user...though I guess it can help if they're both using the same non-default version of php, in contrast to, say, other users on the server.</p> <h3>Which PHP is which?</h3> <p>When a server has multiple versions of PHP, only one of them at a given time will ever live at the path <code>/usr/bin/php</code>. Ordinarily, this is what you get when you type just <code>php</code> at the command line. This also is what you'll get whenever you run a file with a <a href="https://en.wikipedia.org/wiki/Shebang_(Unix)">shebang</a> of <code>#!/usr/bin/env php</code>, meaning if you run drush (or wp-cli, for our Wordpress friends), you'll get whatever PHP is found there as well. You can run <code>which php</code> if you'd like to see where php is found.</p> <p>At this point, definitely check <code>php --version</code>. If you are getting the version you want, you're are done with this article! Well, maybe not - you just want to switch to the account where you do require a different version of PHP than this gives you.</p> <p>So, <code>php --version</code> gives one version, but there should be multiple PHP executables on the system now, right? (You should have installed multiple, or else have multiple versions available at this point). So where are those? These can be directly executed by running them using their versions in their name. For example <code>php8.1</code> or <code>php7.4</code>. So, what is happening here that we just get "one proper php"? Well, a couple things.</p> <h3>Environmental conditions...</h3> <p>What, the smog? No...well, yes, that's a problem, but <a href="https://portside.org/2022-12-31/ten-surprisingly-good-things-happened-2022">some good things happened in 2022</a>. In this instance, our first issue comes from an environment variable. In particular: the venerable <code>PATH</code>, which contains a list of locations that the shell will use to look for a given executable as you enter a command name (or, again, as specified by a shebang). The PATH variable is a colon-delimited list, typically looking about like this: <code>/usr/local/bin:/usr/bin:/bin:</code>. The shell simply looks for the first occurrence of your command as it peeks in each directory in sequence (left-to-right). Is there a <code>/usr/local/bin/php</code>? That's what you'll get. If not, how about <code>/usr/bin/php</code>? And so on, until it finds one, or else you've mistyped <code>pph</code> and end up with <code>command not found</code> instead. You can see your path with <code>echo $PATH</code> (or try <code>which agaric</code>. I'm guessing you won't have an <code>agaric</code> program, this will tell you where it looked for it when it fail to find one).</p> <h3>What's the 'alternative': links all the way down</h3> <p>The second part of this equation is what is going on with the <code>/usr/bin/php</code> that was found. This alleged "The PHP" is actually a soft link to the current system-level default version of PHP that's installed. You can see how this situation is resolved with a command such as <code>readlink -f /usr/bin/php</code>. This command basically says "read the symbolic link (recursively, due to the <code>-f</code>, try it again without the <code>-f</code>!) and show what it's [ultimately] pointing to". This link (and those it links to) come from an "alternatives" system used by Debian-like systems that connects such things as the canonical name of an executable to a specific installed version. You can learn more about how how this is set up (for PHP, anyway) from...you guessed it: <a href="https://github.com/oerdnj/deb.sury.org/wiki/Managing-Multiple-Versions#setting-global-defaults">Ondřej's FAQ</a>.</p> <h3>So...the fix then?</h3> <p>Now, where we have multiple versions of PHP installed, it's generally impractical to change all the shebang lines to something else, but that is technically one way to do things. We're also assuming you want to use multiple versions simultaneously here - so updating via the alternatives system isn't a great option either - if you can even do that. There is a simple method to make this work, even as an unprivileged user: make your own link called <code>php</code> and make sure the shell can find it in the PATH.</p> <p>What we will do, is create our own <code>~/bin</code> folder, and make our link there. Then, we just make sure that <code>~/bin</code> is in our path, and comes <em>before</em> other locations that have a <code>php</code> file. There's no shortage of places for customizing the PATH (and bash, generally), and quite frankly, since I'm not positive what the canonical location is, I'll happily follow the <a href="https://www.debian.org/doc/manuals/debian-reference/ch01.en.html#_customizing_bash">Debian manual</a> which says <code>~/.bashrc</code>. The particular file you'll want to use can be influenced by the type of shell you request (a login vs non-login, and interactive vs non-interactive). In the manual, part of their example looks like this:</p> <pre> <code> # set PATH so it includes user's private bin if it exists if [ -d ~/bin ] ; then PATH="~/bin${PATH+:$PATH}" fi export PATH </code></pre><blockquote> <p>Curious what that odd-looking <code>${PATH+:$PATH}</code> syntax is about? Most people just refer to PATH as <code>$PATH</code> when they want it, like this <code>PATH=~/bin:$PATH</code>, right? Well, yes, and that will probably work just fine, but that weird reference does have some smarts. These are something called parameter expansions. Note that in proper bash parlance, the thing we've been calling a variable this whole time is referred to as a parameter. Go figure...that certainly didn't help me find this reference documentation. If you are interested in shell programming (which I clearly think everyone is, or should be) these can be very helpful to know. You'll bump into these when performing various checks and substitutions on varia-er, parameters. Check out the <a href="https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html">bash documentation</a> to figure this one out.</p> </blockquote> <p>OK, this looks good! Let's go ahead and add that to the <code>~/.bashrc</code> file in your ssh user's home folder. Now, you either need to source the updated file (with <code>. ~/.bashrc</code>), or just reconnect. Sourcing (abbreviated with that previous <code>.</code>, otherwise spelled out as <code>source ~/.bashrc</code>) essentially executes the referenced file in a way that can it can modify the current shell's context. If you were to just run <code>~/.bashrc</code> without sourcing it, it happily sets up an environment, but all that gets wiped out when the script ends.</p> <p>Now, let's get moving with this plan again. Make a bin folder in our ssh user's home folder: <code>mkdir ~/bin</code>. Finally, link the php version you want in there. Here, I'll do php8.1: <code>ln -s /usr/bin/php8.1 ~/bin/php</code>. Voila! Now when you run <code>php --version</code>, you'll get PHP 8.1!</p> <h4>Another diversion, really?</h4> <p>For the impatient: reconnect and skip to the paragraph. For the curious (or when you have time to come back): it turns out our shell has a bit of memory we never expected it to. If you've typed <code>php</code> sometime earlier in your session, and bash last found that it was the one in /usr/bin/php that came up first, it's now remembered that so it doesn't have to find it again. While you <em>can</em> just re-login - again! - you might also wan to take the reins of your system and try typing <code>hash -d php</code> (see <code>help hash</code> to learn what that does - the help command covers shell built-in functionality, like hash). At last, <code>php</code> really works the way we wanted! No more odd little shell corners hiding dusty references on us.</p> <h3>OMG, that works?</h3> <p>Finally...despite my droning on, we're making progress! At this point, when you call upon your <code>drush status</code>, it should actually run without errors (some things under php7.x just don't work now - as expected) and show that it's using the correct php version.</p> <h2>Part 3: Drush does speak SSH, but not YOUR SSH.</h2> <p>The Drush boffins graced us with doodads called site aliases that allow us to readily send commands to various sites we control. If you don't know about those, you'll have to <a href="https://www.drush.org/latest/site-aliases/">read up on them first</a>. The prolific <a href="http://weitzman.github.io/">Moshe Weitzman</a> (along with some 310 contributors, and counting) didn't give us these because they were getting bored <a href="https://www.tag1consulting.com/blog/interview-moshe-weitzman">after 20 years of Drupal</a>; They're pretty essential to working with Drupal effectively.</p> <p>Assuming you have a grasp of aliases under your belt, let's try a Drush command from our local environment to the remote system: <code>drush @test status</code>. OK - something is wrong here. My Drush just said I'm still using php7.4 and not my beloved 8.1...again! Does Drush have a memory too? No, it just doesn't care how you want your ssh sessions to work; those are left to you and Drush does it's own thing. The ssh connection that Drush opens has it's own context set-up, unlike the one we have when we've ssh'd in interactively. Thankfully, there's a quick fix for this - they key Drush needs to know is how to set the PATH up so it also gets our targeted PHP version. Let's add our modified PATH to the site aliases configuration, so Drush also knows what the cool kids are doing.</p> <p><code>live:<br />   host: example.com<br />   paths:<br />     drush-script: /home/example-live/vendor/bin/drush<br />   env-vars:<br />     PATH: /home/example-live/bin:/usr/bin:/bin<br />   root: /home/example-live/web<br />   uri: 'https://example.com/'<br />   user: example-live</code></p> <p>Note the specified PATH of <code>/home/example-live/bin:/bin:/usr/bin</code> places the bin directory in home at the beginning again. At last, when we run <code>drush @test status</code>, it's telling us the PHP it uses is the one that we use. We can share, Drush. We made it!</p> <h2>Thanks for stopping by!</h2> <p>That wraps it up for this one. Hopefully you now feel a little more confident you have some tools helping you master your environment. The version of PHP you need is now at your command. Now you can go get busy with your composer updates and other TODO's. By the way, if all this configuration headache is just not for you - check out our hosted <a href="https://drutopia.org">Drutopia</a> platform. We run an instance of this distribution, and provide all these niceties as part of our hosting setup, so <a href="https://agaric.coop/contact">reach out if interested</a>. Either way, thanks for coming by!</p> </div> , <div class="paragraph paragraph--type-update paragraph--view-mode-default is-desktop columns clearfix"> <div class="is-12-tablet is-2-desktop column"> <time datetime="2023-02-03T12:00:00Z">February 3, 2023</time> </div> <div class="column"> <h2>Oh yeah!</h2> <p>I'm back! Because I knew this blog post wasn't quite long enough already...certainly not because it occurred to me that I left off another alternative I've used in a pinch.</p> <h3>The one-off</h3> <p>If you want to run a command using a different version of php, rather than rely on the shebang, you can always just call the desired script as an argument to php itself. When you run, e.g. <code>drush status</code>, your shell sees the <code>#!</code> on the first line, and uses that as the <a href="https://linuxhandbook.com/shebang/">default interpreter for the script</a>. In essence, your command is translated into <code>/usr/bin/env php drush status</code>.</p> <p>Rather than accept this default, you can specify the specific version on your command line, such as <code>php8.1 drush status</code>. In this case the <code>#!</code> is just ignored. Unfortunately, this method can only get you so far - particularly with drush. Drush often figures out what your command requires and in turn will spawn other php scripts to do the dirty work. These spawned scripts are unaware of the interpreter that drush itself was started with.</p> </div> </div> 2023 February 03 Chris Thompson https://agaric.coop/Always%20run%20the%20desired%20PHP%20version%20when%20your%20hosting%20solution%20has%20multiple%20versions%20available How to filter a view by content that references the current node in modern Drupal https://agaric.coop/ <section data-headerbg="transparent" class="hero is-berry hero-project"> <div class="container"> <div class="hero-image"> <img loading="lazy" src="/sites/default/files/styles/max_650x650/public/title-image/2023-01/web.jpg?itok=gweCfeT4" width="520" height="650" alt="A spiderweb covered in dew drops." typeof="foaf:Image" /> </div> <div class="hero-body"> <h1 class="title">How to filter a view by content that references the current node in modern Drupal </h1> <h2 class="subtitle">References and contextual filters </h2> </div> </div> </section> , <div class="flow_middle"> <p>It is often enough throughout the journey of building websites that you will desire to create very specific lists of content. In my current endeavor, I am working in Drupal 9 with 2 content types:<span> </span><em>Office location</em><span> </span>and<span> </span><em>Event</em><span> </span>. Each<span> </span><em>Event</em><span> </span>occurs at an<span> </span><em>Office location</em>, which is indicated by an<span> </span><em>Office location</em><span> </span>entity reference field on the<span> </span><em>Event</em>. Note that any info in this blog will also be relevant to Drupal 8, Drupal 10, and beyond.</p> <blockquote><p><strong>Here is the user story that we will be working through:</strong><br /> As a site visitor, when I view an<span> </span><em>Office location</em><span> </span>node, I should see a list of<span> </span><em>Events</em><span> </span>that are occurring at the location represented by the current node, so that I have an easy way to learn about events at locations relevant to me.</p> </blockquote> <p>To keep this short and sweet, I am going to assume that you have already created a view. However, all views are not created equal, so if you are not seeing the options that you are looking for while editing your view, maybe try creating a new one. We will be working with a<span> </span><code>block</code><span> </span>display—other options are possible, but this is the standard "Drupal approach".</p> <p>Once you are editing a block display for your view, open the<span> </span><code>Advanced dropdown</code><span> </span>on the right side. Here is what you will see:</p> <p><img alt="The advanced configuration options for a Drupal view." data-entity-type="file" data-entity-uuid="1f3a0259-fc27-4ba7-843b-171dbb704ac6" src="/sites/default/files/inline-images/Screenshot_48.png" width="668" height="753" /></p> </div> , <div class="flow_middle"> <h2 id="steps">Steps</h2> <h3 id="1-add-a-relationship-to-content-referenced-by-your-entity-reference-field">1, Add a relationship to content referenced by your entity reference field</h3> <p>Go ahead and click the<span> </span><code>Add</code><span> </span>button next to<span> </span><code>Relationships</code>.</p> <p><img alt="The relationship selection screen." data-entity-type="file" data-entity-uuid="94d52086-2699-49ee-9d05-3bf36a3f198d" src="/sites/default/files/inline-images/Screenshot_54_0.png" width="1643" height="660" /></p> </div> , <div class="flow_middle"> <p>In my case I am looking for<span> </span><code>Content referenced from field_office_location</code>. This is the entity reference field on my<span> </span><code>Event</code><span> </span>content type, and is the field that my view will use for comparison while querying data.</p> <p><code>Click add and configure relationships</code><span> </span>and then<span> </span><code>Apply</code><span> </span>on the next screen.</p> <h3 id="2-filter-your-view-results-by-content-that-references-the-current-node-id">2. Filter your view results by content that references the current node id</h3> <p>Click the<span> </span><code>Add</code><span> </span>button next to<span> </span><code>Contextual Filters</code></p> <p><code><img alt="The contextual filter selection screen." data-entity-type="file" data-entity-uuid="4c438481-1f12-4295-9546-2506ef929d85" src="/sites/default/files/inline-images/Screenshot_50.png" width="1650" height="1109" /></code></p> </div> , <div class="flow_middle"> <p>Select<span> </span><code>ID</code><span> </span>and click<span> </span><code>Add and configure contextual filters</code>.</p> <p><img alt="The contextual filter configuration screen." data-entity-type="file" data-entity-uuid="d5359eb4-4d3b-4809-99e6-422b7a8c392f" src="/sites/default/files/inline-images/Screenshot_51.png" width="2469" height="1634" /></p> </div> , <div class="flow_middle"> <p>The red boxes above indicate what needs to be done. Be sure to select the relationship you added to the<span> </span><code>Relationship</code><span> </span>field. This will allow the contextual filter to use the value of the<span> </span><em>Event</em>'s entity reference field for comparison.</p> <p>Click<span> </span><code>Provide default value</code><span> </span>and select<span> </span><code>Content ID from URL</code><span> </span>in the dropdown that appears. Note that if your nodes have URL aliases that do not include the content ID, Drupal will still provide the content ID to your contextual filter. Now your view will know which content to display by comparing the current node ID with the the values in the entity reference field of each listing.</p> <p>Lastly, Click<span> </span><code>Apply</code><span> </span>and do not forget to save your view.</p> <h2 id="the-end">The End</h2> <p>Congratulations! Now we know how to filter our view by content that references the current node. You probably still need to place your block and configure which nodes it should or should not display on, but I think that is a "How to" for another day. Have fun Drupaling!</p> </div> 2023 January 26 Keegan Rankin https://agaric.coop/How%20to%20filter%20a%20view%20by%20content%20that%20references%20the%20current%20node%20in%20modern%20Drupal The development of Therapy Fidelity https://agaric.coop/ <section data-headerbg="transparent" class="hero is-dark hero-project"> <div class="container"> <div class="hero-image"> <img loading="lazy" src="/sites/default/files/styles/max_650x650/public/title-image/2023-01/onion.jpg?itok=q4YZnufF" width="640" height="427" alt="A red onion sitting on a plate." typeof="foaf:Image" /> </div> <div class="hero-body"> <h1 class="title">The development of Therapy Fidelity </h1> <h2 class="subtitle">In support of evidence-based therapy </h2> </div> </div> </section> , <div class="flow_middle"> <p>Therapy Fidelity is a mobile interface that displays 132 sequential steps of cognitive behavioral therapy (CBT) for treating posttraumatic stress disorder (PTSD) to improve the fidelity of following the treatment protocol.</p> <h2>The Challenge</h2> <p>Leaders in the field of psychotherapy have been encouraging psychotherapists to adopt evidence-based treatments such as CBT for 20 years but it has been largely unsuccessful. Therapists have been unwilling and unable to rigorously follow CBT protocols with fidelity. Therapists cannot memorize all 132 steps, and patients have very limited access to the steps. As a result, treatment of patients suffers.</p> <p>Child psychiatrist Michael Scheeringa, MD identified a solution for this problem—a mobile interface that displays a CBT treatment protocol and that can be viewed by therapists and patients simultaneously. Through the guidance of Dr. Scheeringa and in partnership with Tulane University, Agaric developed the Therapy Fidelity web app.</p> <h2>Our Approach</h2> <h3>Designed For Use During a Therapy Session</h3> <p>When a patient attends their therapy session, it is far more likely that they arrive with a mobile phone than with a laptop. The need for a seamless mobile experience was one major reason we decided to build Therapy Fidelity as a single page application using React.js. Single page applications are great for reducing the number of network calls made to the host server since they do not need to request markup templates every time an end-user clicks to a different part of the site.</p> <p>Consequently, most of the network calls made by the app are made to read or write data to one of several third party APIs. We worked to mitigate these, too, by using the React Query library, a powerful and configurable tool for fetching, caching, and updating data in React. The result is an app that clearly indicates when data is loading, allows the user to continue navigating the app while update requests are being processed, indicates the results of those requests once they resolve, and prevents redundant fetching of data that has already been cached—in other words, a fast, smooth, and intuitive end-user experience, even on a limited-bandwidth device. One additional benefit of the data caching—many third party APIs will bill per API call after a limited number of requests; without caching that data in the browser, these API services could get expensive fast.</p> <h3>Simple, Consistent, and Flexible Design Elements with Tailwind UI/CSS</h3> <p>Throughout the building process, we looked to Tailwind UI for example components. By doing so, we made sure that the app has a consistent look and feel and is responsive so that it looks good on devices of all screen sizes. As this app mostly functions as a utility dashboard, the pre-designed components sufficed for the apps aesthetic needs, meaning that Tailwind UI saved us a lot of time on design work. Often we had to make minor adjustments to the markup so that it would suit our specific needs, and very seldom did we need to add any custom classes to our CSS stylesheet, which incidentally results in removing the often expensive overhead of building and maintaining our CSS architecture. Again, this is great for an app with simple aesthetic needs but which also needs re-usable and flexible responsive components.</p> <h3>HIPAA Certified Storage with TrueVault</h3> <p>In order to facilitate a quickest time-to-market, we opted to utilize an existing HIPAA certified provider for the storage, retrieval, and access controls/monitoring of protected data. TrueVault provides a JavaScript library for communicating with their RESTful backend. Their backend includes provisions for dealing with all aspects of the data, such as authentication, authorization, logging, and so-on. We were able to use their Vault-based data model to separate patient records into individual silo's per-therapist, and use their group-based security to set nearly all relevant permissions for the app's use. TrueVault certainly comes with its limitations and challenges, but it sure would have taken quite a bit more time and resources to build and certify a custom storage system.</p> <h3>Automated Quality Assurance using Typescript and React Testing Library</h3> <p><span>Additionally, we needed to ensure that each client's data is stored and retrieved accurately. TypeScript, which provides a type system for JavaScript, helped to ensure that as our application passed data around, it was of the expected type. Using a typed language also reduces the possibility for software errors generally. Beyond using good tools to write the application, checks must be in place to ensure that as it evolves, things do not break. Here, we leveraged React Testing framework and other tools to walk through the functionality of the application and ensure that everything continued to function correctly, and even stored the correct data; </span><span>By</span><span> mock</span><span>ing</span><span> (simulat</span><span>ing</span><span>) the TrueVault service we could inspect what the application would be storing there directly.</span></p> <h3>The Delicate Balance of Computing Large Sets of Sensitive Data</h3> <p>While the majority of permissions available to a logged in client were sufficient, there were cases where privileged tasks needed to be performed. Therapy Fidelity's <em>People Like Me</em> system is a great example. <em>People Like Me</em> involves a multi-step form which upon completion shows how many people with similar conditions were able to significantly improve their condition following the successful completion of a 3 month adherence to therapy protocol:</p> <p>First, a patient selects for comparison the results of a single survey they have completed, consisting of numerical indicators  for several psychological conditions (anxiety, depression, PTSD, etc.)</p> <p>Second, they configure which parameters they would like to match on. For example, a patient could choose to only see how many people who had shared a similar pre-therapy anxiety score were able to improve their condition following therapy.</p> <p>Finally, they view the results.</p> <p>Now it must be acknowledged that this system is parsing through and accessing patient data in real time. Of course, it would be a breach of patient privacy law for the patient to have such access to all of this other patient data, so we made sure that all data was scrubbed before reaching storage to reduce any personally identifiable information, and that computations occurred securely on a server before serving back results to the client.</p> <p>Rather than utilize something like a full VPS server that performed these functions for us, we were able to leverage functions-as-a-service to provide the specific privileged function access, specifically AWS Lambda functions. This helped us keep the surface area of the application as low as possible for security purposes, while providing server-side style functionality. The language we chose for these was Go, for its performance, excellent built-in framework, and because we were able to build for our needs with very minimal additional libraries, which again helps mitigate security concerns.</p> <p>While these functions-as-a-service were used several times throughout the site, it will be hard to forget Dr. Scheeringa's excitement about the realization of the <em>People Like Me</em> tool.</p> <h2>What's Next?</h2> <p>Therapy Fidelity was an excellent development experience—both for the thrill of developing a single page app and for carrying on Agaric's history of building <a href="https://agaric.coop/health">tools for medical and scientific communities</a>. As always, we are looking forward to our next opportunity to engage in these communities and build something wonderful.</p> <p>For any clinicians who happen to be reading this and are interested in using the Therapy Fidelity app for their own practice, <a href="http://www.scheeringamind.com">Scheeringa Mind</a> offers it as a service and provides a 30-day free trial. There are also plenty of <a href="http://www.scheeringamind.com/tutorials.html">video tutorials</a> on how to use the app, which might help to preview just what you might be getting into.</p> <p>As Scheeringa Mind Company grows and Therapy Fidelity sees more use, we will be sure to publish any updates or major achievements. We are anticipating great strides in research on CBT protocol and look forward to seeing the results.</p> </div> 2023 January 24 Keegan Rankin, Chris Thompson https://agaric.coop/The%20development%20of%20Therapy%20Fidelity Recommendations for social media as Twitter melts down: Host your own community https://agaric.coop/ <section data-headerbg="transparent" class="hero is-blue hero-project"> <div class="container"> <div class="hero-image"> <img loading="lazy" src="/sites/default/files/styles/max_650x650/public/title-image/2022-11/Tmp_32458-El_Pavo_Real_en_la_Vyshyvanka_protege_al_hombre22109534.jpg?itok=tHZyyuHX" width="512" height="607" alt="Painting of a fancy peacock." typeof="foaf:Image" /> </div> <div class="hero-body"> <h1 class="title">Recommendations for social media as Twitter melts down </h1> <h2 class="subtitle">Host your own community! </h2> </div> </div> </section> , <div class="flow_middle"> <p>As Elon Musk destroys Twitter, a lot of clients have asked about alternative social media, especially 'Mastodon'— meaning the federated network that includes thousands of servers, running that software <em><strong>and</strong></em> many other <abbr title="Free/Libre Open Source Software">FLOSS</abbr> applications, all providing interconnecting hubs for distributed social media. Agaric has some experience in those parts, so we are sharing our thoughts on the opportunity in this crisis.</p> <p>In short: For not-for-profit organizations and news outlets especially, this is a chance to host your own communities by providing people a natural home on the federated social web.</p> <p>Every not-for-profit organization lives or dies, ultimately, based on its relationship with its supporters. Every news organization, it's readers and viewers.</p> <p>For years now, a significant portion of the (potential) audience relationship of most organizations has been mediated by a handful of giant corporations through Google search, Facebook and Twitter social media.</p> <p>A federated approach based on a protocol called <a href="https://activitypub.rocks/">ActivityPub</a> has proven durable and viable over the past five years. Federated means different servers run by different people or organizations can host people's accounts, and people can see, reply to, and boost the posts of people on the other servers. The most widely known software doing this is <a href="https://joinmastodon.org/">Mastodon</a> but it is far from alone. <a href="https://akkoma.social/">Akkoma</a>, <a href="https://pleroma.social/">Pleroma</a>, <a href="https://friendi.ca/">Friendica</a>, <a href="https://pixelfed.social/">Pixelfed</a> (image-focused), <a href="https://joinpeertube.org/">PeerTube</a> (video-focused), <a href="https://mobilizon.org/en/">Mobilizon</a> (event-focused), and more all implement the ActivityPub protocol. You can be viewing and interacting with someone using different software and not know it— similar to how you can call someone on the phone and not know their cellular network nor their phone model.</p> <p>The goal of building a social media following of people interested in (and ideally actively supporting) your organization might be best met by setting up your own social media.</p> <p>This is very doable with the 'fediverse' and Mastodon in particular. In particular, because the number of people on this ActivityPub-based federated social web has already grown by a couple million in the past few weeks— and that's with Twitter not yet having serious technical problems that are sure to come with most of its staff laid off. With the likely implosion of Twitter, giving people a home that makes sense for them is a huge service in helping people get started— the hardest part is choosing a site!</p> <p>People fleeing Twitter as it breaks down socially and technically would benefit from your help in getting on this federated social network. So would people who have never joined, or long since left, Twitter or other social media, but are willing to join a network that is less toxic and is not engineered to be addictive and harmful.</p> <p>Your organization would benefit by having a relationship with readers that is not mediated by proprietary algorithms nor for-profit monopolies. It makes your access on this social network more like e-mail lists— it is harder for another entity to come in between you and your audience and take access away.</p> <p>But the mutual benefits for the organization and its audience go beyond all of this.</p> <p>When people discuss among one another what the organization has done and published, a little bit of genuine community forms.</p> <p>Starting a Mastodon server could be the start of your organization seeing itself as not only doing good works or publishing media, but building a better place for people to connect and create content online.</p> <p>The safety and stability of hosting a home on this federated social network gives people a place to build community.</p> <p>But organizations have been slow to adopt, even now with the Twitter meltdown. This opens up tho opportunity for extra attention and acquiring new followers.</p> <p>Hosting the server could cost between $50 to $450 a month, but this is definitely an opportunity to provide a pure community benefit (it is an ad-free culture) and seek donations, grants, or memberships.</p> <p>The true cost is in moderation time; if volunteers can start to fill that you are in good shape. A comprehensive writeup on everything to consider is here courtesy the <a href="https://social.coop/@agaric">cooperatively-managed Mastodon server that Agaric Technology Collective chose</a> to join at <a href="https://wiki.social.coop/How-to-make-the-fediverse-your-own.html">social.coop's how to make the fediverse your own</a>.</p> <p>You would be about the first for not-for-profit or news organizations.</p> <p>You would be:</p> <ul><li>giving people a social media home right when they need it</li> <li>literally owning the platform much of your community is on</li> </ul><p>And it all works because of the federation aspect— your organization does not have to provide a Twitter, TikTok, or Facebook replacement <em>yourselves</em>, you instead join the leading contender for all that.</p> <p>By being bold and early, you will also get media attention and perhaps donations and grants.</p> <p>The real question is if it would divert scarce resources from your core work, or if the community-managing aspects of this could bring new volunteer (or better, paid) talent to handle this.</p> <p>Even one person willing to take on the moderator role for a half-hour a day to start should be enough to remove any person who harasses people on other servers or otherwise posts racist, transphobic, or other hateful remarks.</p> <p>Above all, your organization would be furthering your purpose, through means other than its core activities or publishing, to inform and educate and give people more capacity to build with you.</p> <p>Not surprisingly, <a href="https://drupal.community/auth/sign_up">Drupal has already figured this out</a>!</p> </div> , <div class="flow_middle"> </div> , <div class="flow_middle"> <p> <small>Image credit: <a href="https://commons.wikimedia.org/wiki/File:Tmp_32458-El_Pavo_Real_en_la_Vyshyvanka_protege_al_hombre22109534.jpg">Karlheinrichpasch</a>, <a href="https://creativecommons.org/licenses/by-sa/4.0">CC BY-SA 4.0</a>, via Wikimedia Commons.</small></p> </div> 2022 November 18 Benjamin Melançon https://agaric.coop/Recommendations%20for%20social%20media%20as%20Twitter%20melts%20down%3A%20Host%20your%20own%20community How to add an audio player to a content node in Drupal 9 https://agaric.coop/ <section data-headerbg="transparent" class="hero hero-project"> <div class="container"> <div class="hero-image"> <img loading="lazy" src="/sites/default/files/styles/max_650x650/public/title-image/2022-11/tape-on-grass-crop.jpg?itok=un3pNFOz" width="473" height="427" alt="A white cassette tape laying in the grass." typeof="foaf:Image" /> </div> <div class="hero-body"> <h1 class="title">How to add an audio player to a content node in Drupal 9 </h1> </div> </div> </section> , <div class="flow_middle"> <p>Getting an audio player to show on your content nodes only takes a few steps. However, like many things in Drupal, knowing what those few steps are can be a bit of a challenge. This short guide will provide a basic introduction for anyone looking to use this core functionality on their site.</p> <h2>Step 1: Enable the Media module</h2> <p>The media module is available in Drupal core, and just needs to be enabled.</p> <h2>Step 2: Create a new <em>Media type</em></h2> <p>In the admin toolbar, navigate to <em>Structure &gt; Media types &gt; Add media type</em><br /> You may or may not have specific needs that determine how you name your media type. I simply named mine <em>Audio file</em>, and then selected <em>Audio file</em> for the <em>Media source</em> selection field. Don't forget to click save.</p> <h2>Step 3: Create an entity reference field on the entity type that should contain your audio player</h2> <p>In the most basic case, you will add a field to the content type where you want an audio player. When you get to the field type drop-down, select<em> Media</em> (under <em>Reference</em>). Of course, label your field according to your needs.</p> <p><strong>note:</strong> If you are using the paragraphs module, you might choose to create a new paragraph type and add a field there instead.</p> <p>Click <em>Save and continue</em></p> <p>Click <em>Save field settings</em></p> <p>Now you will be directed to the <em>Edit</em> tab for your new field. Select your desired settings. The important bit here is that you scroll to the <em>REFERENCE TYPE</em> field set, and click the checkbox next to your new media type (<em>Audio file</em> in my case).</p> <p>Scroll to the bottom and click <em>Save settings</em></p> <p>If you added this field directly to your content type, then you should be all set.<br /> If you are using paragraphs, don't forget to enable your new paragraph type on the paragraph field of your content type.</p> </div> 2022 September 20 Keegan Rankin https://agaric.coop/How%20to%20add%20an%20audio%20player%20to%20a%20content%20node%20in%20Drupal%209 Drupal 9 Component Plugin ContextException "not a valid context" after CTools, Symfony update https://agaric.coop/ <section data-headerbg="transparent" class="hero hero-project"> <div class="container"> <div class="hero-image"> <img loading="lazy" src="/sites/default/files/styles/max_650x650/public/title-image/2022-07/old-fireworks.jpg?itok=rWsDoayW" width="440" height="500" alt="Old-fashioned half-used colorful red and blue fireworks box, labeled &#039;Thunder Bomb&#039; with the warning Caution-Explosive in large capitalized letters." typeof="foaf:Image" /> </div> <div class="hero-body"> <h1 class="title">Drupal 9 Component Plugin ContextException "not a valid context" </h1> <h2 class="subtitle">after CTools, Symfony update </h2> </div> </div> </section> , <div class="flow_middle"> <p>Upgrading to CTools 3.10.0 and/or various Symfony upgrades can break the simple task of adding content on your Drupal site.</p> <blockquote><p>Drupal\Component\Plugin\Exception\ContextException: The context is not a valid context. in Drupal\Core\Executable\ExecutablePluginBase-&gt;getContextDefinition() (line 184 …</p> </blockquote> <p><strong><em>Update:</em></strong>  Seems confirmed this is CTools, <a href="https://www.drupal.org/project/ctools/issues/3300682">see critical bug issue</a>.</p> <p>And this fixed it:</p> <pre> git revert 0fbbbaf [main 7be1cd2] Revert fatal error standard code update "Update composer lock" 1 file changed, 163 insertions(+), 220 deletions(-) saharareporters [main]$ dco install # that is, composer install - dco is my alias for ddev composer Gathering patches for root package. Installing dependencies from lock file (including require-dev) Verifying lock file contents can be installed on current platform. Warning: The lock file is not up to date with the latest changes in composer.json. You may be getting outdated dependencies. It is recommended that you run `composer update` or `composer update &lt;package name&gt;`. Package operations: 0 installs, 27 updates, 1 removal - Removing drupal/entityqueue (1.2.0) Deleting web/modules/contrib/entityqueue - deleted Gathering patches for root package. Gathering patches for dependencies. This might take a minute. - Downgrading drupal/core-composer-scaffold (9.4.4 =&gt; 9.4.3): Extracting archive - Downgrading symfony/yaml (v4.4.44 =&gt; v4.4.43): Extracting archive - Downgrading symfony/translation (v4.4.44 =&gt; v4.4.41): Extracting archive - Downgrading symfony/event-dispatcher (v4.4.44 =&gt; v4.4.42): Extracting archive - Downgrading symfony/dependency-injection (v4.4.44 =&gt; v4.4.43): Extracting archive - Downgrading symfony/console (v4.4.44 =&gt; v4.4.43): Extracting archive - Downgrading symfony/filesystem (v5.4.11 =&gt; v5.4.9): Extracting archive - Downgrading symfony/config (v4.4.44 =&gt; v4.4.42): Extracting archive - Downgrading symfony/css-selector (v5.4.11 =&gt; v5.4.3): Extracting archive - Downgrading symfony/dom-crawler (v4.4.44 =&gt; v4.4.42): Extracting archive - Downgrading symfony/browser-kit (v4.4.44 =&gt; v4.4.37): Extracting archive - Downgrading symfony/finder (v6.1.3 =&gt; v6.1.0): Extracting archive - Downgrading symfony/validator (v4.4.44 =&gt; v4.4.43): Extracting archive - Downgrading symfony/serializer (v4.4.44 =&gt; v4.4.43): Extracting archive - Downgrading symfony/routing (v4.4.44 =&gt; v4.4.41): Extracting archive - Downgrading symfony/mime (v5.4.11 =&gt; v5.4.10): Extracting archive - Downgrading symfony/http-foundation (v4.4.44 =&gt; v4.4.43): Extracting archive - Downgrading symfony/process (v4.4.44 =&gt; v4.4.41): Extracting archive - Downgrading symfony/var-dumper (v5.4.11 =&gt; v5.4.9): Extracting archive - Downgrading symfony/debug (v4.4.44 =&gt; v4.4.41): Extracting archive - Downgrading symfony/error-handler (v4.4.44 =&gt; v4.4.41): Extracting archive - Downgrading symfony/http-kernel (v4.4.44 =&gt; v4.4.43): Extracting archive - Downgrading drupal/migmag (1.8.1 =&gt; 1.8.0): Extracting archive - Downgrading symfony/string (v6.1.3 =&gt; v6.1.2): Extracting archive - Downgrading drupal/noreferrer (1.14.0 =&gt; 1.13.0): Extracting archive - Downgrading drupal/ctools (3.10.0 =&gt; 3.9.0): Extracting archive - Downgrading symfony/mailer (v5.4.11 =&gt; v5.4.10): Extracting archive</pre><p>And a drush cache-reload, of course.</p> <p>Full error, for search engines:</p> <p>The website encountered an unexpected error. Please try again later.</p> <p><em>Drupal\Component\Plugin\Exception\ContextException</em>: The context is not a valid context. in <em>Drupal\Core\Executable\ExecutablePluginBase-&gt;getContextDefinition()</em> (line <em>184</em> of <em>core/lib/Drupal/Core/Plugin/ContextAwarePluginTrait.php</em>).</p> <pre> Drupal\Core\Executable\ExecutablePluginBase-&gt;getContext(NULL) (Line: 116) Drupal\Core\Executable\ExecutablePluginBase-&gt;getContextValue(NULL) (Line: 91) Drupal\Core\Entity\Plugin\Condition\EntityBundle-&gt;evaluate() (Line: 77) Drupal\Core\Condition\ConditionManager-&gt;execute(Object) (Line: 84) Drupal\Core\Condition\ConditionPluginBase-&gt;execute() (Line: 376) Drupal\pathauto\Entity\PathautoPattern-&gt;applies(Object) (Line: 320) Drupal\pathauto\PathautoGenerator-&gt;getPatternByEntity(Object) (Line: 41) Drupal\pathauto\PathautoWidget-&gt;formElement(Object, 0, Array, Array, Object) (Line: 353) Drupal\Core\Field\WidgetBase-&gt;formSingleElement(Object, 0, Array, Array, Object) (Line: 220) Drupal\Core\Field\WidgetBase-&gt;formMultipleElements(Object, Array, Object) (Line: 111) Drupal\Core\Field\WidgetBase-&gt;form(Object, Array, Object) (Line: 181) Drupal\Core\Entity\Entity\EntityFormDisplay-&gt;buildForm(Object, Array, Object) (Line: 121) Drupal\Core\Entity\ContentEntityForm-&gt;form(Array, Object) (Line: 127) Drupal\node\NodeForm-&gt;form(Array, Object) (Line: 106) Drupal\Core\Entity\EntityForm-&gt;buildForm(Array, Object) call_user_func_array(Array, Array) (Line: 531) Drupal\Core\Form\FormBuilder-&gt;retrieveForm('node_article_form', Object) (Line: 278) Drupal\Core\Form\FormBuilder-&gt;buildForm(Object, Object) (Line: 97) Drupal\autosave_form\Form\AutosaveFormBuilder-&gt;buildForm(Object, Object) (Line: 73) Drupal\Core\Controller\FormController-&gt;getContentResult(Object, Object) call_user_func_array(Array, Array) (Line: 123) Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber-&gt;Drupal\Core\EventSubscriber\{closure}() (Line: 564) Drupal\Core\Render\Renderer-&gt;executeInRenderContext(Object, Object) (Line: 124) Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber-&gt;wrapControllerExecutionInRenderContext(Array, Array) (Line: 97) Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber-&gt;Drupal\Core\EventSubscriber\{closure}() (Line: 158) Symfony\Component\HttpKernel\HttpKernel-&gt;handleRaw(Object, 1) (Line: 80) Symfony\Component\HttpKernel\HttpKernel-&gt;handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session-&gt;handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle-&gt;handle(Object, 1, 1) (Line: 106) Drupal\page_cache\StackMiddleware\PageCache-&gt;pass(Object, 1, 1) (Line: 85) Drupal\page_cache\StackMiddleware\PageCache-&gt;handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware-&gt;handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware-&gt;handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel-&gt;handle(Object, 1, 1) (Line: 709) Drupal\Core\DrupalKernel-&gt;handle(Object) (Line: 19) </pre> </div> 2022 July 29 Benjamin Melançon https://agaric.coop/Drupal%209%20Component%20Plugin%20ContextException%20%26quot%3Bnot%20a%20valid%20context%26quot%3B%20after%20CTools%2C%20Symfony%20update Drupal 9.4 installation with existing configuration fails because "unable to uninstall the MySQL module"!? https://agaric.coop/ <div class="flow_middle"> <p>Here is how to deal with the surprising-to-impossible-seeming error "Unable to uninstall the <em class="placeholder">MySQL</em> module because: The module 'MySQL' is providing the database driver 'mysql'.."</p> <p>Like, why is it trying to uninstall anything when you are installing? Well, it is because you are installing <em>with existing configuration</em>— and your configuration is out-of-date. This same problem will happen on configuration import on a Drupal website, too. (See update below for those steps!)</p> <p>Really this error message is a strong reminder to <em>always</em> run database updates and then commit any resulting configuration changes after updating Drupal core or module code.</p> <p>And so the solution is to roll back the code to Drupal 9.3, do your installation from configuration, and then run the database updates, export configuration, and commit the result.</p> <p>For example:</p> <pre><code class="language-bash">git checkout &lt;commit-hash-of-earlier-composer-lock&gt; composer install drush -y site:install drutopia --existing-config git checkout main composer install drush -y updb drush -y cex git status # Review what is here; git add -p can also help git add config/ git commit -m "Apply configuration updates from Drupal 9.4 upgrade" </code></pre> <p>The system update <code>enable_provider_database_driver</code> is the <code>post-update</code> hook that is doing the work here to "Enable the modules that are providing the listed database drivers." Pretty cool <a href="https://www.drupal.org/node/3129492">feature</a> and a strong reminder to always, always run database updates and commit any configuration changes immediately after any code updates!</p> <h3>Update: Steps on for resolving the unable to uninstall MySQL module error on configuration import</h3> <p>This is what you probably already did, before the <code>drush -y cim</code> failed (luckily, luckily it failed).</p> <pre><code class="language-bash">composer update drush -y updb </code></pre> <p>All that is great! Now continue, <em>not</em> with a config import, but with a config export:</p> <pre><code class="language-bash">drush -y cex git status # Review what is here; git add -p can also help git add config/ git commit -m "Apply configuration updates from Drupal 9.4 upgrade" </code></pre> <p>Remember, after every composer update and database update, you need to do a configuration export and commit the results— database updates can change configuration, and if you do not commit those, you will undo these intentional and potentially important changes on a configuration import. If you ran into this problem on a configuration import, it is a sign of breakdown in discipline in following these steps!</p> <p><strong>Every</strong> time you bring in code changes with composer update all this must be part and parcel:</p> <pre><code class="language-bash">composer update drush -y updb drush -y cex git status # Review what is here; git add -p can also help git add config/ git commit -m "Apply configuration from database updates" </code></pre> </div> , <div class="flow_middle"> <img loading="lazy" src="/sites/default/files/styles/large/public/2022-06/firefox-cannot-connect-dinorabbit_0.png?itok=9yqpTY4r" width="480" height="395" alt="A cartoon dinosaur trying to plug two cords together but their arms are too short." typeof="foaf:Image" /> </div> 2022 June 17 Benjamin Melançon https://agaric.coop/Drupal%209.4%20installation%20with%20existing%20configuration%20fails%20because%20%26quot%3Bunable%20to%20uninstall%20the%20MySQL%20module%26quot%3B%21 Uniting Visions: Kicking off Thursday 3pm ET planning & building sessions for democratic conversation scaling platform, Visions Unite https://agaric.coop/ <section data-headerbg="transparent" class="hero hero-project"> <div class="container"> <div class="hero-image"> <img loading="lazy" src="/sites/default/files/styles/max_650x650/public/title-image/2022-06/stand-deliver-dueling-free-speech.jpg?itok=R3X3mqEi" width="500" height="413" alt="Silhouettes of a people on soapboxes shouting through bullhorns at a crowd, demonstrating stand / deliver, a portable dueling free speech unit, a mobile box equipped with two platforms and megaphones." typeof="foaf:Image" /> </div> <div class="hero-body"> <h1 class="title">Uniting Visions </h1> <h2 class="subtitle">Kicking off Thursday 3pm ET planning &amp; building sessions for democratic conversation scaling platform, Visions Unite </h2> </div> </div> </section> , <div class="flow_middle"> <p>Hi friends and collaborators, <a href="https://communitybridge.com/bbb-room/show-and-tell/">join us</a> today at 3pm ET (or any subsequent Thursday at 3) as we kick off a series of research, planning, discussion, and building sessions for Visions Unite.</p> <p>As our primary pro bono project, Agaric is working on Visions Unite, "where people seeking to make the world more whole can share ideas and information and gather the commitment and resources to build power to be the change we need", which a dozen projects have tried to do—what makes this different is sharing power via democratic mass communication.</p> <p>Here are some <a href="https://gitlab.com/pwgd/visionsunite/-/issues">initial user stories for Visions Unite</a>.</p> <p>Help plan and build the interface and underlying technology! (Drupal friends, we have been leaning against Drupal but might do it for the <abbr title="Minimum Viable Product">MVP</abbr>— would love to hear your thoughts for or against.)</p> <p>Connection info will always be up-to-date at <a href="https://agaric.coop/show-and-tell">agaric.coop/show</a> (for these sessions we are taking over most of our Show &amp; Tell hour, which is weekly on Thursdays 3pm Eastern).</p> </div> 2022 June 16 Benjamin Melançon, Keegan Rankin https://agaric.coop/Uniting%20Visions%3A%20Kicking%20off%20Thursday%203pm%20ET%20planning%20%26amp%3B%20building%20sessions%20for%20democratic%20conversation%20scaling%20platform%2C%20Visions%20Unite Good discussion here with a great takeaway. Meet your fellow workers! https://portside.org/2022-05-12/revolutionary-grounds https://agaric.coop/ <div class="flow_middle"> </div> 2022 May 13 https://agaric.coop/Good%20discussion%20here%20with%20a%20great%20takeaway.%20Meet%20your%20fellow%20workers%21%20https%3A//portside.org/2022-05-12/revolutionary-grounds Here is a social media microblog-sized post just to say we are syndicating to social.coop Mastodon and Twitter thanks to the Indieweb module! https://agaric.coop/ <div class="flow_middle"> <p>It's great to be here, and <a href="https://social.coop/@agaric">there</a>, and <a href="https://twitter.com/agaric">there</a>.</p> <p>Thanks <a href="https://www.drupal.org/project/indieweb">Indieweb module for Drupal</a>!</p> </div> 2022 March 08 Benjamin Melançon, Michele Metts https://agaric.coop/Here%20is%20a%20social%20media%20microblog-sized%20post%20just%20to%20say%20we%20are%20syndicating%20to%20social.coop%20Mastodon%20and%20Twitter%20thanks%20to%20the%20Indieweb%20module%21 Create and use a custom permission in your module https://agaric.coop/ <section class="hero is-white hero-project"> <div class="container"> <div class="hero-image"> <img loading="lazy" src="/sites/default/files/styles/max_650x650/public/title-image/2021-03/undraw_secure_login_pdn4.png?itok=9woH369n" width="650" height="448" alt="Laptop with lock icon." typeof="foaf:Image" /> </div> <div class="hero-body"> <h1 class="title">Create and use a custom permission in your Drupal module </h1> <h2 class="subtitle">Define a permission in Drupal code and check access programmatically </h2> </div> </div> </section> , <div class="flow_middle"> <p>You can define your own permissions for the Drupal permissions page (<em>/admin/people/permissions</em> in modern Drupal, Drupal 8, 9, 10, and beyond) and then add conditional options to your code to do different things based on the role of the user and the permissions configured by a site administrator.</p> <p>Here's how.</p> <h3>Create a modulename.permissions.yml file</h3> <p>This simple file has the permission machine name (lower case with spaces) and a title (Sentence case) with an optional description.</p> <p>For our module, which has a particularly long name, that file is <code>drutopia_findit_site_management.permissions.yml</code> and its contents are like so:</p> <pre><code class="language-yaml">access meta tab: title: 'Access meta tab' description: 'Access meta information (author, creation date, boost information) in Meta vertical tab.' </code></pre> <p>You can repeat lines like these in the same file for as many permissions as you wish to define.</p> <h3>Check for that permission in your code</h3> <p>The process for checking permissions is simply to use a user object if that's handed into your code, or to <a href="https://api.drupal.org/api/drupal/core%21lib%21Drupal.php/function/Drupal%3A%3AcurrentUser/9.1.x">load the current user</a> if it's not, and use the <a href="https://api.drupal.org/api/drupal/core%21modules%21user%21src%21Entity%21User.php/function/User%3A%3AhasPermission/9.1.x">hasPermission()</a> method which returns TRUE if that user has permission and FALSE if not.</p> <p>For example, in a form alter in our <code>drutopia_findit_site_management.module</code> file:</p> <pre><code class="language-php">/** * Implements hook_form_BASE_FORM_ID_alter() for node_form. * * Completely hide the Meta vertical tab (field group) from people without permission. * */ function drutopia_findit_site_management_form_node_form_alter(&amp;$form, FormStateInterface $form_state, $form_id) { // If the current user has the permission, do not hide the Meta vertical tab. if (\Drupal::currentUser()-&gt;hasPermission('access meta tab')) { return; } // Code to hide the meta tab goes here, and is only reached if the user lacks the permission. // ... } </code></pre> <p>See all this code in context in the <a href="https://gitlab.com/find-it-program-locator/drutopia_findit_site_management/">Find It Site Management module</a>.</p> <p>To learn more about defining permissions in modern Drupal, including dynamic permissions, you can see the <a href="https://www.drupal.org/node/2311427">change record for when the new approach replaced hook_permission()</a>.</p> </div> 2021 March 10 Benjamin Melançon https://agaric.coop/Create%20and%20use%20a%20custom%20permission%20in%20your%20module Why we resist Zoom and choose BigBlueButton for video chat https://agaric.coop/ <div class="flow_middle"> <p><img alt="BigBlueButton screenshot of Agaric's Show and Tell" data-entity-type="file" data-entity-uuid="23ad61ea-a642-421c-91f8-b8ffaeb73f75" src="/sites/default/files/inline-images/bbbinsideroomS%2BTscreenshot.png" width="1322" height="643" /></p> <p>We can look at the recent popularity of some widely used platforms like Zoom and ask ourselves some questions as to why we still use them when we know a lot of terrible things about them. Agaric prefers to use a free/libre video chat software called BigBlueButton for many reasons, the first one being the licensing, but there are many reasons.</p> <p>Zoom has had some major technology failures, which the corporation is not liable to disclose. At one point, a vulnerability was discovered in the desktop Zoom client for MacOS that allowed hackers to start your webcam remotely and launch you into a meeting without your permission. The company posted a note saying that they fixed the issue. Unfortunately, the Zoom source code is proprietary and we are not even allowed to look at it. There is no way for the community to see how the code works or to verify that the fix was comprehensive.</p> <p>The Zoom Corporation stated early on that the software was encrypted end-to-end (E2EE) from your device to the recipient's device. This was untrue at the time, but the company states that it has been corrected for users on their client app. While it is no longer true that E2EE is unsupported, it does require that you use the proprietary Zoom client for E2EE to work. Without E2EE, any data that is retrieved on its way from your computer to a server can be accessed! The only real security is knowing the operators of your server. This is why Agaric uses trusted sources like <a href="https://mayfirst.org">MayFirst.org</a> for most of our projects and we have a relationship with our BigBlueButton host. The <a href="https://theintercept.com/2020/03/31/zoom-meeting-encryption/">Intercept also revealed</a> that Zoom users that dial in on their phone are NOT encrypted at all</p> <p>BigBlueButton does not have a client app and works in your browser, so there is no E2EE. The idea for E2EE is that with it,  you "do not have to trust the server operator and you can rely on E2EE" because the model implies that every client has keys that are protecting the transferred data. However: you MUST still use a proprietary client in order to get the benefits of E2EE support, so once again you MUST trust Zoom as you have no permission to examine the app to determine that the keys are not being shared with Zoom.</p> <p>Of course there is always the fact that hackers work day and night to corrupt E2EE and a Corporation is not obligated to tell you the customer every time there has been a security breach, and this information is usually buried in the terms of service they post - sometimes with a note saying the terms are subject to change and updates. A Corporation is not obligated to tell you, the customer when there has been a security breach" unless any personal information is exposed. There are now mandatory timely disclosure requirements for all states: <a href="https://www.ncsl.org/research/telecommunications-and-information-technology/security-breach-notification-laws.aspx">https://www.ncsl.org/research/telecommunications-and-information-technology/security-breach-notification-laws.aspx</a> ...Can Zoom really be trusted? As with some laws, the fine that is applied is low and affordable and subject to the interpretation of the courts and the status of knowledge your lawyer is privvy to - meaning most Corporations normally have a battery of lawyers to interpret the law and drag the case out until you are... broke.</p> <p>In the case of BigBlueButton encryption, E2EE would only make sense if there are separate clients using an API to connect to the BBB server so a user does not have to trust the BBB server operator. If the user trusts the server operator, then there would be no need for E2EE." Lesson learned: It is always best practice to know and trust your server hosts as they are the ones that have the keys to your kingdom. </p> <p>Some technology analysts consider <a href="https://www.theguardian.com/technology/2020/apr/02/zoom-technology-security-coronavirus-video-conferencing">Zoom software to be malware</a>. Within companies that use Zoom, employers are even able to monitor whether or not you are <a href="https://www.huffpost.com/entry/zoom-tracks-not-paying-attention-video-call_l_5e7b96b5c5b6b7d80959ea96">focusing on the computer screen during meetings</a> which seems excessively intrusive. Speaking of intrusive, the <a href="https://9to5mac.com/2020/03/27/zoom-ios-app/">Zoom Corporation also shares your data with FaceBook</a>, even if you do not have a FB account - that could be a whole blog in itself, but just being aware of some of the <a href="https://www.tenable.com/blog/unauthorized-call-and-webcam-access-vulnerability-in-zoom-mac-client-cve-2019-13450">vulnerabilities</a> is a good thing to pass on. Some of the <a href="https://www.schneier.com/blog/archives/2019/07/zoom_vulnerabil.html">bad stuff remains</a> even if you uninstall the Zoom app from your device! Even though <a href="https://www.cnbc.com/2020/04/08/zoom-faces-investor-lawsuit-over-privacy-and-security-flaws.html">a class action suit</a> was filed over privacy issues, the company stock still continued to rise.</p> <p>Those are many reasons why we do not support Zoom. But there are also many reasons why we prefer BBB over Zoom. Besides, BBB has many great features that Zoom lacks:</p> <p>1. Easily see who is speaking when their name appears above the presentation.<br /> 2. Chat messages will remain if you lose your connection or reload and rejoin the room.<br /> 3. Video is HD quality and you can easily focus on a persons webcam image.<br /> 4. Collaborative document writing on a shared Etherpad.<br /> 5. Easily share the presenter/admin role with others in the room.<br /> 6. Write closed captions in many languages, as well as change the language of the interface.<br /> 7. An interactive whiteboard for collaborative art with friends!</p> <p><img alt="Collaborative artwork on BBB whiteboard." data-entity-type="file" data-entity-uuid="b1d8e0eb-2946-4399-90bb-fa0ab9b9c2ea" src="/sites/default/files/inline-images/2021catBBBwhiteboard.png" width="702" height="381" /></p> <p>One huge advantage of free software, like BBB, is that you can usually find their issue queue where you can engage with the actual developers to report bugs and request feature enhancements. Here is a link to the <a href="https://github.com/bigbluebutton/bigbluebutton/issues">BigBlueButton issue queue</a>.</p> <p>So, why do people keep using a platform like Zoom, even though there are many features in BigBlueButton that are much better? </p> <p>There is very little publicity for free software and not many know it exists and that there are <a href="https://www.fsf.org/campaigns/priority-projects/voicevideochat">alternative solutions</a>. You can find some great suggestions of software and switch to it by using this site called <a href="https://switching.software">switching.software</a>. The marketing budget for Zoom is large and leads you to believe it has everything you will need. Sadly their budget grows larger everyday with the money people pay for subscriptions to the platform. As a result, many people go with it as it is already used by their friends and colleagues, even though there are reports of irresponsible behavior by the Zoom Corporation. This is why the New York school system does not use Zoom and many organizations are following suit. The company gives people <a href="https://techcrunch.com/2020/03/31/zoom-at-your-own-risk/?guccounter=1">a false sense of security</a> as it is widely used and very popular.</p> <p>Of course, there are reasons to <a href="https://blog.grobox.de/2009/ten-reasons-why-you-should-boycott-skype/">avoid other proprietary chat platforms</a> too...</p> <p>Agaric offers BigBlueButton for events and meetings. Check out our fun BBB website at <a href="http://communitybridge.com">CommunityBridge</a> and test drive the video chat yourself!</p> <p>If this discussion interests you, please share your thoughts with us in the comments.</p> <p>Looking to learn more about problems with Zoom? There are a lot of articles about Zoom scandals.</p> <ul><li>If you need <a href="https://theintercept.com/2021/01/18/leak-zoom-meeting/">to leak a zoom video without being identified</a>, you need to be very careful!</li> <li><a href="https://theintercept.com/2020/11/14/zoom-censorship-leila-khaled-palestine/">Zoom censorship of Palestine seminars sparks fight over academic freedom</a></li> <li><a href="https://theintercept.com/2020/04/03/zooms-encryption-is-not-suited-for-secrets-and-has-surprising-links-to-china-researchers-discover/">Zoom encryption is not suited for secrets</a></li> <li><a href="https://theintercept.com/2020/03/31/zoom-meeting-encryption/">Zoom is unclear about whether or not the app is actually end-to-end encrypted</a>, which it isn't.</li> <li>And, yes - <a href="https://en.wikipedia.org/wiki/Zoom_(software)#Reception">the wikipedia page</a> has a good summary of some issues</li> <li>Stallman provides resources on <a href="https://stallman.org/zoom.html">what is bad about Zoom</a> also</li> </ul><p>Looking to learn more about protecting your privacy online? These links have some helpful information and videos for tech-savvy people and organic folks alike!</p> <ul><li><a href="https://odysee.com/$/search?q=the%20hated%20one">The Hated One</a></li> <li><a href="https://odysee.com/@RobBraxmanTech">Rob Braxman Tech</a></li> <li><a href="https://odysee.com/@the-rev:5/2018-04-27-Safe-Services:9">Safe Services</a></li> </ul><p class="is-6 subtitle">2021 could be the year we all begin to STOP supporting the Corporations that oppress us. </p> <p class="is-6 subtitle">Special thanks to Keegan Rankin for edits!</p> <p> </p> </div> 2021 January 23 Michele Metts, Chris Thompson https://agaric.coop/Why%20we%20resist%20Zoom%20and%20choose%20BigBlueButton%20for%20video%20chat Drupal toolbar not working in dev environments in Firefox? Here's why. https://agaric.coop/ <div class="flow_middle"> <p>Drupal's toolbar second level of menu options and dropdown not showing? Look for "Uncaught DOMException: The quota has been exceeded." errors, as viewable in the <a href="https://developer.mozilla.org/en-US/docs/Tools/Web_Console">Firefox web console</a>. If you see them, the problem is likely due to sites sharing a top-level domain—which is likely if you are using a local development environment like <a href="https://www.ddev.com/ddev-local/">DDEV</a>, and you working on way too many sites at once—combined with a pretty bad <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1683299">Firefox bug that will be fixed in the next release</a>.</p> <p>To quote <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1683299#c7">Nathan Monfils</a>:</p> <blockquote> <ol><li>Everything from your public domain (abc.tld) counts against your quota, even if it is in a seemingly unrelated subdomain (e.g. my-app.example.com and intranet.example.com).</li> <li>The quota is not recomputed properly, requiring a firefox restart after clearing your data on other subdomains</li> </ol></blockquote> <p>Note this may affect all sorts of applications, not just Drupal, when you have them running on multiple subdomains of the same top-level domain. So this isn't just about local development environments (and i dislike that DDEV shares their own top-level domain across all the instances you are working on, and while it <a href="https://ddev.readthedocs.io/en/stable/users/cli-usage/#using-ddev-offline-and-top-level-domain-options">can be changed</a> i've accepted its way of doing things so i'm on the same page with other developers by default).</p> <p>Sure, closing more tabs and restarting Firefox could (predictably) have fixed this—and a lot else that's wrong with me, according to everyone i know—but why do that when i can open <em>more</em> tabs and learn precisely how broken everything around me really is?</p> </div> , <div class="flow_middle"> <img loading="lazy" src="/sites/default/files/styles/large/public/2021-01/toomanytabs_0.jpeg?itok=tmNHTYpV" width="480" height="360" alt="A drawing of a laptop with open tabs extending outside of the laptop as horizontally tiled windows." typeof="foaf:Image" /> </div> , <div class="flow_middle"> <p>I am very happy the bug is fixed and this blog post will be obsolete in mere days! Usually this sort of technical noodlings get relegated to our <a href="https://agaric.gitlab.io/raw-notes/">raw notes, currently hosted through GitLab</a>, but figured at least a few other Drupal developers would want to know what has been going on with their toolbars.</p> <p>Image credit: "Too Many Tabs" by <a href="https://www.flickr.com/photos/38765959@N00">John Markos O'Neill</a> is licensed with <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC BY-SA 2.0</a>.</p> </div> 2021 January 19 Benjamin Melançon https://agaric.coop/Drupal%20toolbar%20not%20working%20in%20dev%20environments%20in%20Firefox?Here=#039;s why. Upgrade from Drupal 6 or 7 when it is right for you: In the era of modern Drupal, release cycles of major versions have only minor importance https://agaric.coop/ <section class="hero is-white hero-project"> <div class="container"> <div class="hero-image"> <img loading="lazy" src="/sites/default/files/styles/max_650x650/public/title-image/2021-01/undraw_update_uxn2.png?itok=SmY6kzuE" width="650" height="404" alt="&quot;&quot;" typeof="foaf:Image" /> </div> <div class="hero-body"> <h1 class="title">Upgrade from Drupal 6 or 7 when it is right for you </h1> <h2 class="subtitle">In the era of modern Drupal, release cycles of major versions have only minor importance </h2> </div> </div> </section> , <div class="flow_middle"> <p><abbr title="Too Long Didn't Read">TLDR</abbr>: Drupal 7 has a much longer lifespan than the (already pushed back) official date, and Drupal 8 has an essentially infinite lifespan because it updates in-place to Drupal 9 easily (and the same will be true of Drupal 10, 11, ∞.). There's no reason to rush an <a href="https://agaric.coop/drupal-upgrades-migrating-content-your-new-site">upgrade</a>— but there's no reason to wait either.</p> <p>That's the short version.</p> <p>A client recently wrote to Agaric about Drupal 7 / Drupal 8 / Drupal 9 project planning questions:</p> <blockquote> <p>With the EOL for Drupal 7 in Nov of 2022, and the EOL for Drupal 8 in Nov 2021, is there a reason we should move a D7 site to D8 at all this year? Seems like we might want to move directly to D9? We don’t want to feel pushed up against a wall with a “new” site build in Drupal 8, if we can limp along in D7 for a couple more years while we develop a D9 site with a longer lifespan. I’m wondering if you might have time to discuss pros and cons briefly so we can get a good plan together for moving forward.</p> </blockquote> <p>I started typing and almost did not stop:</p> <ol><li> <p>No one believes me when i say this, but i repeat my assurance that Drupal 7 will be well-supported commercially until 2030 or later (Drupal 6, released in 2008, still has semi-official long term support until <a href="https://www.mydropwizard.com/drupal-6-lts">at least February 24th, 2022</a>— and Drupal 7 has a larger install base than Drupal 6 ever did, and <a href="https://www.drupal.org/project/usage/drupal">currently has the largest install base of any version of Drupal by far</a>, with more than half a million tracked installs.</p> <p>Drupal 7 will be supported by the community for a long time. You do not have to feel <em>pushed</em> to a new version, like, ever.</p> </li> </ol> </div> , <div class="flow_middle"> <img loading="lazy" src="/sites/default/files/styles/large/public/2021-01/usage-statistics-for-Drupal-core.png?itok=JouW1YhU" width="480" height="261" alt="Stacked area chart showing Drupal 7 with more than half of all currently tracked Drupal core installs, which is more than half a million." typeof="foaf:Image" /> </div> , <div class="flow_middle"> <ol start="2"><li> <p>We do recommend moving directly to Drupal 9 (which was released on June 3rd of 2020), however:</p> </li> <li> <p>Moving to Drupal 8 or to Drupal 9 is much the same. Drupal 8 starts what i call the "modern Drupal" era. Whereas for going from Drupal 5 to 6 or 6 to 7 or 7 to 8 broke backward compatibility and might as well be a full rebuild (so we would often recommend hopping a version, say, stay on Drupal 6 and wait for Drupal 8 to be ready) going from Drupal 8 to 9 is closer to going from Drupal 8.8 to 8.9— an in-place upgrade from 8.9 to 9.0. Going from 9 to 10 will work the same, and that's the plan and promise for Drupal 8 on out.</p> </li> <li> <p>All that said, if anything significant needs fixing on your current Drupal 7 site, or you are looking to make any improvements, you'll want to do that on Drupal 8+ or Drupal 8/9 as we phrased it back when Drupal 9 was still a pretty recent release, but now we can just say Drupal 9— or, as i call it to emphasize the decreased importance of major version numbers, <strong>modern Drupal</strong>.</p> </li> </ol><p>Agaric is always happy to <a href="https://agaric.coop/ask">discuss more</a>! Mostly what i'm saying here is the useful things to talk about are the specific goals for the sites—when you want to accomplish what—because the official support cycles are a distraction in the current context of Drupal. So make sure your current site is <a href="/maintenance-and-support">maintained</a>, but take your time to get clear on your objectives, and contact Agaric or the Drupal professionals of your choice when you think it might make sense to <a href="https://agaric.coop/drupal-upgrades-migrating-content-your-new-site">upgrade your site into the era of modern Drupal</a>.</p> </div> 2021 January 13 Benjamin Melançon https://agaric.coop/Upgrade%20from%20Drupal%206%20or%207%20when%20it%20is%20right%20for%20you%3A%20In%20the%20era%20of%20modern%20Drupal%2C%20release%20cycles%20of%20major%20versions%20have%20only%20minor%20importance Community-managed categories https://agaric.coop/ <div class="flow_middle"> <p>Community-managed categories is an idea from just about the beginning of my time as a web developer.  As "Community-managed taxonomy" it was my submission to the 2007 Summer of Code, barely a couple years into my time as a Drupal developer.</p> <blockquote><p>The Drupal module Community Managed Taxonomy, or CMT, variously known as Community Managed Categories, seeks to bring the possibility of mass participation to categorization (taxonomy) and therefore potentially site structure.</p> </blockquote> <p>As the <a href="https://www.drupal.org/project/cmt">project page</a> put it:</p> <blockquote><p>Community-managed taxonomy (CMT) opens categorization of content to the site's community. Users can influence both what terms nodes are tagged with and how these terms are themselves organized.</p> <p>It can also be used to make structured tags on the fly. Users do not need to be logged in to make or propose terms for content.</p> </blockquote> <p>I very much hope to get back to this work.  I never did get the module working—Agaric was a new company and my father became ill and was killed by the hospital that summer, and my mentor and i were not the best match—and after failing to complete the module and get the second stipend, the time and circumstance has not yet returned.  Please <a href="https://agaric.coop/ask">contact us</a> if you may be able to help make the time right in 2021!</p> <p>This capability to allow a large community to coordinate in categorizing may be more salient when democratically-moderated mass communication is made possible.  That's a goal i have pursued even longer, and i think it is more important and possibly logically prior to community-managed taxonomy.  First we need community-managed communication, so that the resources we build together can reach, be distributed to, the people who should know.  But building community-managed communication may bring up the need for community-managed categories directly, too— how do we decide what the groups are that we can manage communication <em>within</em>?  That's a job for community-managed taxonomy, probably.</p> <p>It is all tied up to what I have been aiming for for decades.  This description is from 2008:</p> <blockquote><p>People have always needed something better than mailing lists— or other communication tools as they exist now. We need something that can reach millions of people (or billions– everyone) and still be open to everyone on an equal basis. Reaching everyone means filtering to reduce quantity and increase quality. Staying open to everyone means that the filtering must not be controlled by any group, must in some true sense belong to everyone.</p> <p>The Internet has this potential. (The issue of access remains crucial, but is separate from helping the Internet reach closer to its potential for people who do have access.)</p> <p>People Who Give a Damn is incorporated as a nonprofit organization to connect, without interference and without wasting anyone's time, everyone who gives a damn.</p> <p>As the first technique to achieve this, anyone signed up to receive messages in a network can submit a message to be sent. The message will be publicly available immediately, but it will be moderated by a random sample of other people in the network pulled to serve jury duty. If they decide it is important enough to send to everyone in the network, it is sent to everyone. If not, the message will have more limited distribution to the sender's personal contacts and possibly to groups within the overall network to which the sender belongs.</p> <p>This is simple. Yet it will make possible horizontal communication, not top-down few-to-many broadcasts, that is also mass communication. We need horizontal mass communication because we need mass cooperation and collaboration. It is possible with current technology, and necessary for the well-being of ourselves, our friends and family, our fellow humans, our Earth. Anyone interested in updates on progress or how they can help, please contact me.</p> </blockquote> </div> 2021 January 01 Benjamin Melançon https://agaric.coop/Community-managed%20categories Double-edged Raiser: Past time to ditch Blackbaud https://agaric.coop/ <section class="hero is-white hero-project"> <div class="container"> <div class="hero-image"> <img loading="lazy" src="/sites/default/files/styles/max_650x650/public/title-image/2020-07/double-edged-razor.jpg?itok=pl8XyuaS" width="650" height="650" alt="A plain two-edged razor blade (for a pun on double-edged razor)." typeof="foaf:Image" /> </div> <div class="hero-body"> <h1 class="title">Double-edged Raiser </h1> <h2 class="subtitle">Past time to ditch Blackbaud </h2> </div> </div> </section> , <div class="flow_middle"> <p>I have watched in sadness and sometimes anger as large non-profit after large non-profit collectively poured enough money into Raiser's Edge and other Blackbaud licenses and consulting services to fund many feature enhancements for the main <abbr title="Free/Libre Open Source Software">FLOSS</abbr> alternative, <a href="https://civicrm.org/">CiviCRM</a>— improvements which would then be free for everyone, forever.</p> <p>I have never met anyone who actually likes Blackbaud products and services. However, many organizations felt they were the only safe option, in the sense of claiming to have everything an enterprise needs.</p> <p>Now, Blackbaud failed to secure its servers sufficiently and large amounts of its clients' donor data, including personally identifying information, was obtained in a ransomware attack. This was back in May. Blackbaud ultimately paid the ransomer to allegedly destroy the data they obtained— and only late in July finally told their customers what happened.</p> <p>As the American Civil Liberties Union wrote to all its supporters, current and past (including myself), this is a rotten situation:</p> <blockquote> <p>In all candor, we are frustrated with the lack of information we've received from Blackbaud about this incident thus far. The ACLU is doing everything in our power to ascertain the full nature of the breach, and we are actively investigating the nature of the data that was involved, details of the incident, and Blackbaud's remediation plans.</p> <p>We are also exploring all options to ensure this does not happen again, including revisiting our relationship with Blackbaud.</p> </blockquote> <p>Fortunately, none of Agaric's clients are affected. But we hope everyone using or considering using Blackbaud and other proprietary services for their most important data will look at free/libre open source solutions. Code you (or your technology partner) can see and contribute to means you truly can do anything. And if you put aside the money that would be gouged out of your organization by the <a href="https://www.thenonprofittimes.com/npt_articles/blackbaud-s-buying-spree-hits-431-million-with-convio-deal/">eTapestry, Kintera, and Convio-swallowing</a> monopolist Blackbaud, you probably can afford to.</p> <p>At <a href="https://agaric.coop">Agaric,</a> we have recently been working with CiviCRM more recently (building on experience dating back fifteen years!) and we know our friends at <a href="https://palantetech.coop/">Palante Technology Cooperative</a> and <a href="https://www.mydropwizard.com/">myDropWizard</a> are well-versed in CiviCRM, as are <a href="https://civicrm.org/partners-contributors">many others</a>. Please consider this when weighing your options for maintaining a strong, ethical relationship with your supporters, and <a href="https://agaric.coop/ask">let us know</a> if you have any thoughts or questions!</p> </div> 2020 August 03 Benjamin Melançon https://agaric.coop/Double-edged%20Raiser%3A%20Past%20time%20to%20ditch%20Blackbaud Are you building communities of practice for community health in this time of public recognition of injustice? https://agaric.coop/ <div class="paragraph paragraph--type-update paragraph--view-mode-default is-desktop columns clearfix"> <div class="is-12-tablet is-2-desktop column"> <time datetime="2021-01-25T12:00:00Z">January 25, 2021</time> </div> <div class="column"> <h3>Blog Update: Keep this conversation alive!</h3> <p>In this post, we call out for "Birds of a feather" to join us at DrupalCon, which has come and gone. However, this conversation remains relevant to our political condition and relevant to our work! Our scientific and government entities must continue to increasingly acknowledge racism as a public health threat. We believe that harnessing the power of data within our own communities is a path to the change that we want to see. Please help us keep this post and the discussion it provokes alive and circulating!</p> </div> </div> , <div class="flow_middle"> <h3>Original Post</h3> <p>The struggle to take seriously the <a href="https://www.usnews.com/news/healthiest-communities/articles/2020-07-08/racism-resonates-as-public-health-crisis-amid-pandemic-protests">impact of racism on public health during the covid-19 crisis</a> spotlights the importance of health communities of practice and their need to have high-quality, data-driven discussions.</p> <p>Environmental racism was <a href="https://portside.org/2016-01-23/environmental-racism-harms-americans-flint-and-beyond">forced into the national conversation by Flint, Michigan</a> years ago and <a href="https://www.propublica.org/article/on-the-minds-of-black-lives-matters-protestors-a-racist-health-system">COVID-19 has made long-standing problems of racism in healthcare provision</a> impossible to ignore.  The <a href="https://itsgoingdown.org/mapping-the-states-strategy-of-repression-against-the-rebellion/">rebellion against police repression</a>, which started in Minneapolis, is itself a reaction to the public health issues caused by policing. This reaction was predictable, and Minneapolis resident <span><span><a href="https://twitter.com/BullyCreative">D.A. Bullock</a>, indeed, <a href="https://www.tcdailyplanet.net/entrenched-sexism-racism-police/">predicted it</a>.</span></span></p> <p>So, moving forward...</p> <p>How do we have data-informed conversations effectively within our communities?</p> <p>How do we expand them to include more health professionals and community members from outside of what is currently recognized as the healthcare industry?</p> <p>We need resolute answers to both of these questions.</p> <p>Thanks to <a href="https://www.nichq.org/insight/raising-bar-virtual-qi-collaboration">a client giving us a shout out</a>, Agaric has come to acknowledge <a href="https://agaric.coop/health">our experience building for health communities</a> and the role that we are able to take in this conversation. We would love to talk and learn with others more deeply about these issues.  We're hosting a "Birds of a Feather" (people interested in the same topic coming together) at DrupalCon Global today at 3:15 Eastern Time to talk about facilitating discussion among healthcare practitioners, researchers, and the public.</p> <p>Here are some more questions to get you thinking!</p> <p>What are the next steps for healthcare workers and researchers? What are the next steps for any person who cares about our communities? How do we move important conversations into the public realm sustainably? The stakes for both well-informed and broad-based discussion are clearer than ever.  We know <a href="https://www.yesmagazine.org/opinion/2020/07/08/history-protests-social-change/">pressure, policy, and practice are what make change</a>; what is our role?</p> <p>Please leave your comments even if you can't join us today!</p> </div> 2020 July 16 Benjamin Melançon, Michele Metts https://agaric.coop/Are%20you%20building%20communities%20of%20practice%20for%20community%20health%20in%20this%20time%20of%20public%20recognition%20of%20injustice Drupal migrations reference: List of configuration options for destination plugins https://agaric.coop/ <div class="flow_middle"> <p>In the previous article we provided a <a href="https://agaric.coop/blog/drupal-migrations-reference-list-configuration-options-source-plugins">reference of available configuration options for migrate source plugins</a>. In today’s article we are doing something similar for destination plugins. We will present a reference of available configuration options for migrate destination plugins provided by Drupal core and some contributed modules. Knowing which options are available might require some Drupal development knowledge. By providing this reference it should make the process of writing migrations easier.</p> <p><img alt="List of configuration options for destination plugins" data-entity-type="file" data-entity-uuid="08d46f6a-80ce-4aad-a850-11fb3fa199c9" src="/sites/default/files/inline-images/migrations-destination-plugin-configuration-options-list.jpg" width="960" height="540" /></p> <p>For each migrate destination plugin we will present: the module that provides it, the class that defines it, the class that the plugin extends, and any inherited options from the class hierarchy. For each plugin configuration option we will list its name, type, a description, and a note if it is optional.</p> <h2 id="DestinationBase">DestinationBase (abstract class)</h2> <p><strong>Module</strong>: Migrate (Drupal Core)<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21destination%21DestinationBase.php/class/DestinationBase/9.0.x">Drupal\migrate\Plugin\migrate\destination\DestinationBase</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Plugin%21PluginBase.php/class/PluginBase/9.0.x">Drupal\Core\Plugin\PluginBase</a></p> <p>This abstract class is extended by many migrate destination plugins. This means that the provided configuration keys apply to any destination plugin extending it.</p> <p>List of configuration keys:</p> <ol><li><strong>destination_module</strong>: An optional string value. Identifies the module handling the destination data. If not set, the Migrate API tries to read the value from the destination plugin definition.</li> </ol><h2 id="Entity">Entity (abstract class)</h2> <p><strong>Module</strong>: Migrate (Drupal Core) <strong>Plugin ID</strong>: entity:$entity_type See below.<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core!modules!migrate!src!Plugin!migrate!destination!Entity.php/class/Entity/9.0.x">Drupal\migrate\Plugin\migrate\destination\Entity</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21destination%21DestinationBase.php/class/DestinationBase/9.0.x">Drupal\migrate\Plugin\migrate\destination\DestinationBase</a><br /><strong>Inherited configuration options</strong>: destination_module.<br /><strong>Related article</strong>: <a href="https://agaric.coop/blog/drupal-migrations-reference-list-properties-content-entity">Drupal migrations reference: List of properties per content entity</a></p> <p>This abstract class is extended by migrate destination plugins that want to import entities. It uses the <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21Derivative%21MigrateEntity.php/class/MigrateEntity/9.0.x">MigrateEntity derivative</a> to handle both content and configuration entities. The derivative sets the plugin ID to entity:$entity_type where $entity_type is the machine name of the entity. For example, entity:node and entity:node_type.</p> <p>By default, content entities are handled by the <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21destination%21EntityContentBase.php/class/EntityContentBase/9.0.x">EntityContentBase</a> class while configuration entities use <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21destination%21EntityConfigBase.php/class/EntityConfigBase/9.0.x">EntityConfigBase</a>. Some entities like user (content) and node type (configuration) use specific classes for the import operation. The <a href="https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Plugin%21DefaultPluginManager.php/function/DefaultPluginManager%3A%3AgetDefinitions/9.0.x">DefaultPluginManager::getDefinitions</a> method triggers the search for classes that will override the default for a particular plugin ID. The override ultimately happens in the <a href="https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Component%21Plugin%21Discovery%21DerivativeDiscoveryDecorator.php/function/DerivativeDiscoveryDecorator%3A%3AgetDerivatives/9.0.x">DerivativeDiscoveryDecorator::getDerivatives</a> method.</p> <p>In addition to the keys provided in the parent class chain, this abstract class provides the following configuration keys, which will be available to its derivatives:</p> <ol><li><strong>default_bundle</strong>: An optional string value. Gets the bundle for the row taking into account the default. If not present, the bundle <a href="https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21EntityType.php/property/EntityType%3A%3Aentity_keys/9.0.x">entity key</a> should be set in the process section for entities that can have more than one bundle. For example, the type property for <a href="https://api.drupal.org/api/drupal/core%21modules%21node%21src%21Entity%21Node.php/class/Node/9.0.x">nodes</a>, the vid property for <a href="https://api.drupal.org/api/drupal/core%21modules%21taxonomy%21src%21Entity%21Term.php/class/Term/9.0.x">taxonomy terms</a>, and the bundle property for <a href="https://api.drupal.org/api/drupal/core%21modules%21media%21src%21Entity%21Media.php/class/Media/9.0.x">media entities</a>.</li> </ol><h2 id="EntityContentBase">EntityContentBase</h2> <p><strong>Module</strong>: Migrate (Drupal Core) <strong>Plugin ID</strong>: entity:$entity_type See below.<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21destination%21EntityContentBase.php/class/EntityContentBase/9.0.x">Drupal\migrate\Plugin\migrate\destination\EntityContentBase</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core!modules!migrate!src!Plugin!migrate!destination!Entity.php/class/Entity/9.0.x">Drupal\migrate\Plugin\migrate\destination\Entity</a><br /><strong>Inherited configuration options</strong>: destination_module and <strong>default_bundle</strong>.</p> <p>This class is used to handle import operations for all content entities, unless a specific class exists for the plugin ID being used. For example, nodes are handled by this class, but users leverage the <a href="https://api.drupal.org/api/drupal/core%21modules%21user%21src%21Plugin%21migrate%21destination%21EntityUser.php/class/EntityUser/9.0.x">EntityUser</a> class. The <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21Derivative%21MigrateEntity.php/class/MigrateEntity/9.0.x">MigrateEntity derivative</a> sets the plugin ID to entity:$entity_type where $entity_type is the machine name of the content entity. For example, entity:node, entity:user, entity:taxonomy_term, entity:file, entity:media, entity:comment, entity:block_content, and entity:contact_message.</p> <p>In addition to the keys provided in the parent class chain, this class provides the following configuration keys:</p> <ol><li><strong>translations</strong>: An optional boolean value. It indicates if the entity is translatable, defaults to FALSE. If set to TRUE, the corresponding langcode <a href="https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21EntityType.php/property/EntityType%3A%3Aentity_keys/9.0.x">entity key</a> will be added to the <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21destination%21EntityContentBase.php/function/EntityContentBase%3A%3AgetIds/9.0.x">list of destination IDs</a> that uniquely identifies each record. If the migration itself does not provide a value for the langcode property, the site’s default language is used.</li> <li><strong>overwrite_properties</strong>: An optional array of string values. It is a list of properties or fields in the destination entity that will be overwritten if an entity with the same ID already exists. Any properties that are not listed will not be overwritten. Refer to the <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21destination%21EntityContentBase.php/class/EntityContentBase/9.0.x">class documentation</a> for an example.</li> <li><strong>validate</strong>: An optional boolean value. It indicates whether an entity should be validated, defaults to FALSE. This was <a href="https://www.drupal.org/node/3073707">introduced in Drupal 8.8</a> and can be used to prevent invalid entities from being migrated. For example, a node with an empty title would fail validation. A required field that is not set by the migration will trigger a validation error, unless the field is configured to have a default value. Similarly, an integer field with minimum and maximum values will trigger an error if a value outside that range tries to be imported. <a href="https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21FieldableEntityInterface.php/function/FieldableEntityInterface%3A%3Avalidate/9.0.x">Field API validations</a> are triggered when this configuration option is set to TRUE.</li> </ol><h2 id="EntityUser">EntityUser</h2> <p><strong>Module</strong>: User (Drupal Core) <strong>Plugin ID</strong>: entity:user See below.<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21user%21src%21Plugin%21migrate%21destination%21EntityUser.php/class/EntityUser/9.0.x">Drupal\user\Plugin\migrate\destination\EntityUser</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21destination%21EntityContentBase.php/class/EntityContentBase/9.0.x">Drupal\migrate\Plugin\migrate\destination\EntityContentBase</a><br /><strong>Inherited configuration options</strong>: destination_module, default_bundle, translations, overwrite_properties, and <strong>validate</strong>.</p> <p>This class provides a destination plugin for migrating user entities. It performs extra checks like preventing that the password for user 1 is overridden by the migration.</p> <p>In addition to the keys provided in the parent class chain, this abstract class provides the following configuration keys:</p> <ol><li><strong>md5_passwords</strong>: An optional boolean value. If set to TRUE, the pass (password) property of the user entity is assumed to contain an MD5 hash. The <a href="https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Password%21PhpassHashedPassword.php/function/PhpassHashedPassword%3A%3Ahash/9.0.x">passwords will be salted and re-hashed</a> before they are saved to the destination Drupal database.</li> </ol><h2 id="EntityRevision">EntityRevision</h2> <p><strong>Module</strong>: Migrate (Drupal Core) <strong>Plugin ID</strong>: entity_revision:$entity_type See below.<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21destination%21EntityRevision.php/class/EntityRevision/9.0.x">Drupal\migrate\Plugin\migrate\destination\EntityRevision</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21destination%21EntityContentBase.php/class/EntityContentBase/9.0.x">Drupal\migrate\Plugin\migrate\destination\EntityContentBase</a><br /><strong>Inherited configuration options</strong>: destination_module, <strong>default_bundle</strong>, translations, overwrite_properties, and validate.</p> <p>This class provides an entity revision destination plugin. Only revisionable entities, those that define a revision <a href="https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21EntityType.php/property/EntityType%3A%3Aentity_keys/9.0.x">entity key</a>, can use this destination plugin. It uses the <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21Derivative%21MigrateEntityRevision.php/class/MigrateEntityRevision/9.0.x">MigrateEntityRevision derivative</a> which sets the plugin ID to entity_revision:$entity_type where $entity_type is the machine name of the entity. For example, entity_revision:node whose revision key is vid and entity_revision:block_content whose revision key is revision_id.</p> <p>Entity revisions can only be migrated after the entity to which they belong has been migrated. For example, revisions of a given node (entity_revision:node destination migration) can be migrated only after the current version of that node (entity:node destination migration) has been imported.</p> <h2 id="EntityReferenceRevisions">EntityReferenceRevisions</h2> <p><strong>Module</strong>: <a href="https://www.drupal.org/project/entity_reference_revisions">Entity Reference Revisions module</a> <strong>Plugin ID</strong>: entity_reference_revisions:$entity_type See below.<br /><strong>Class</strong>: <a href="https://git.drupalcode.org/project/entity_reference_revisions/-/blob/8.x-1.x/src/Plugin/migrate/destination/EntityReferenceRevisions.php">Drupal\entity_reference_revisions\Plugin\migrate\destination\EntityReferenceRevisions</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21destination%21EntityRevision.php/class/EntityRevision/9.0.x">Drupal\migrate\Plugin\migrate\destination\EntityRevision</a><br /><strong>Inherited configuration options</strong>: destination_module, default_bundle, translations, overwrite_properties, and validate.<br /><strong>Related article</strong>: Introduction to paragraphs migrations in Drupal</p> <p>This class provides an entity revision revision destination plugin. It uses the <a href="https://git.drupalcode.org/project/entity_reference_revisions/-/tree/8.x-1.x/src/Plugin/Derivative">MigrateEntityReferenceRevisions derivative</a> which sets the plugin ID to entity_reference_revisions:$entity_type where $entity_type is the machine name of the entity. For example, entity_reference_revisions:node and entity_reference_revisions:paragraph. For example, entity_reference_revisions:node whose revision key is vid and entity_reference_revisions:paragraph whose revision key is revision_id.</p> <p>This is the destination plugin used for migrating <a href="https://www.drupal.org/project/paragraphs">Paragraphs</a>. Entity reference fields are <a href="https://git.drupalcode.org/project/paragraphs/-/commit/ab0011ddf98e98c314ff68cce055c588a8742cb0">no longer supported</a> to <a href="https://www.drupal.org/node/2668678">reference Paragraphs</a>. Instead, entity entity reference revisions must be used. Therefore, this class is used for paragraphs migrations with the entity_reference_revisions:paragraph plugin ID. See <a href="https://agaric.coop/blog/introduction-paragraphs-migrations-drupal">this article</a> for an example on how to migrate paragraphs.</p> <p>In addition to the keys provided in the parent class chain, this abstract class provides the following configuration keys:</p> <ol><li><strong>new_revisions</strong>: An optional boolean value. Flag to indicate if a new revision should be created instead of updating a previous default record. Only applicable when providing an entity id without a revision_id. Defaults to FALSE.</li> </ol><h2 id="EntityConfigBase">EntityConfigBase</h2> <p><strong>Module</strong>: Migrate (Drupal Core) <strong>Plugin ID</strong>: entity:$entity_id See below.<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21destination%21EntityConfigBase.php/class/EntityConfigBase/9.0.x">Drupal\migrate\Plugin\migrate\destination\EntityConfigBase</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core!modules!migrate!src!Plugin!migrate!destination!Entity.php/class/Entity/9.0.x">Drupal\migrate\Plugin\migrate\destination\Entity</a><br /><strong>Inherited configuration options</strong>: destination_module and default_bundle.</p> <p>This class is used to handle import operations for all configuration entities, unless a specific class exists for the plugin ID being used. For example, taxonomy vocabularies are handled by this class, but node types leverage the <a href="https://api.drupal.org/api/drupal/core%21modules%21node%21src%21Plugin%21migrate%21destination%21EntityNodeType.php/class/EntityNodeType/9.0.x">EntityNodeType</a> class. The <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21Derivative%21MigrateEntity.php/class/MigrateEntity/9.0.x">MigrateEntity derivative</a> sets the plugin ID to entity:$entity_type where $entity_type is the machine name of the content entity. For example, entity:node_type, entity:user_role, entity:taxonomy_vocabulary, entity:block, entity:comment_type, entity:block_content_type, entity:contact_form, entity:date_format.</p> <p>In addition to the keys provided in the parent class chain, this abstract class provides the following configuration keys:</p> <ol><li><strong>translations</strong>: An optional boolean value. if TRUE, the destination will be associated with the langcode provided by the source plugin. Defaults to FALSE. For example: en for English, es for Spanish, and fr for French.</li> </ol><h2 id="EntityNodeType">EntityNodeType</h2> <p><strong>Module</strong>: Node (Drupal Core) <strong>Plugin ID</strong>: entity:node_type<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21node%21src%21Plugin%21migrate%21destination%21EntityNodeType.php/class/EntityNodeType/9.0.x">Drupal\node\Plugin\migrate\destination\EntityNodeType</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21destination%21EntityConfigBase.php/class/EntityConfigBase/9.0.x">Drupal\migrate\Plugin\migrate\destination\EntityConfigBase</a><br /><strong>Inherited configuration options</strong>: destination_module, default_bundle, and translations.</p> <p>This class is used to import node types. It does not take extra configuration options. The plugin overrides the import method to attach the body field to the imported content type. This depends on the presence of certain destination properties in the imported row. That is, the following properties needs to be mapped in the process section of the migration:</p> <ol><li><strong>create_body</strong>: An optional boolean value. If TRUE, a body field will be added to the content type.</li> <li><strong>create_body_label</strong>: An optional string value. If set and create_body is TRUE, the value for this destination property will be used as the label of the body field.</li> </ol><h2 id="Config">Config</h2> <p><strong>Module</strong>: Migrate (Drupal Core) <strong>Plugin ID</strong>: config<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21destination%21Config.php/class/Config/9.0.x">Drupal\migrate\Plugin\migrate\destination\Config</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21destination%21DestinationBase.php/class/DestinationBase/9.0.x">Drupal\migrate\Plugin\migrate\destination\DestinationBase</a><br /><strong>Inherited configuration options</strong>: destination_module.</p> <p>This class persists data to the <a href="https://www.drupal.org/docs/8/configuration-management">configuration management system</a>. In addition to the keys provided in the parent class chain, this class provides the following configuration keys:</p> <ol><li><strong>store null</strong>: An optional boolean value. If TRUE, when a property is NULL, the NULL value is stored. Otherwise, the default value is used. Defaults to FALSE. Note that there is a <em>space character in the configuration name</em>.</li> <li><strong>translations</strong>: An optional boolean value. if TRUE, the destination will be associated with the langcode provided by the source plugin. Defaults to FALSE.</li> </ol><p>Additionally, the plugin expects certain destination properties in the imported row. That is, the following properties needs to be mapped in the process section of the migration:</p> <ol><li><strong>config_name</strong>: A string value. The machine name of the configuration. For example: node.settings, node.type.article, user.settings, system.site, and core.extension.</li> <li><strong>langcode</strong>: An optional string value. The language code of the configuration. For example: en for English, es for Spanish, and fr for French.</li> </ol><h2 id="Table">Table</h2> <p><strong>Module</strong>: Migrate Plus Plugin ID: table<br /><strong>Class</strong>: <a href="https://git.drupalcode.org/project/migrate_plus/-/blob/8.x-5.x/src/Plugin/migrate/destination/Table.php">Drupal\migrate_plus\Plugin\migrate\destination\Table</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21destination%21DestinationBase.php/class/DestinationBase/9.0.x">Drupal\migrate\Plugin\migrate\destination\DestinationBase</a><br /><strong>Inherited configuration options</strong>: destination_module.</p> <p>This class allows you to write directly to a table not registered with <a href="https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Database!database.api.php/group/schemaapi/9.0.x">Drupal Schema API</a>. <a href="https://git.drupalcode.org/project/migrate_plus/-/blob/8.x-5.x/tests/src/Kernel/MigrateTableTest.php">See this test</a> for an example on how to use this plugin.</p> <p>In addition to the keys provided in the parent class chain, this class provides the following configuration keys:</p> <ol><li><strong>database_key</strong>: An string value. Key for the database connection used for inserting records. See <a href="https://www.drupal.org/docs/8/api/database-api/database-configuration">this documentation page</a> for more information on database connection keys. We also covered the topic when explaining the <a href="https://agaric.coop/blog/drupal-migrations-reference-list-configuration-options-source-plugins#SqlBase">SqlBase source plugin</a>.</li> <li><strong>table_name</strong>: An string value. Name of the table where records will be imported.</li> <li><strong>batch_size</strong>: An optional integer value. Maximum number of rows to insert in one query. Defaults to 1.</li> <li><strong>id_fields</strong>: An associative array value. Fields used to uniquely identify table rows. At least one field is required. See the class’s docblock for an example of the expected structure.</li> <li><strong>fields</strong>: An optional associative array value. Mapping of column names to values set in the process section.</li> </ol><h2 id="other-plugins">Available configuration for other migrate destination plugins</h2> <p>In Drupal core itself there are around 50 migrate destination plugins. And many more are made available by contributed modules. It would be impractical to document them all here. To get a list by yourself, load the <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21MigrateDestinationPluginManager.php/class/MigrateDestinationPluginManager/9.0.x">plugin.manager.migrate.destination</a> service and call its getDefinitions() method. This will return all migrate destination plugins provided by the <strong>modules that are currently enabled</strong> on the site. This Drush command would get the list:</p> <p><code># List of migrate destination plugin definitions. $ drush php:eval "print_r(\Drupal::service('plugin.manager.migrate.destination')-&gt;getDefinitions());" # List of migrate destination plugin ids. $ drush php:eval "print_r(array_keys(\Drupal::service('plugin.manager.migrate.destination')-&gt;getDefinitions()));"</code></p> <p>To find out which configuration options are available for any destination plugin consider the following:</p> <ul><li>Find the class that defines the plugin and find out which configuration values are read. Some plugins even include the list in the docblock of the class. Search for a pattern similar to $this-&gt;configuration['option_name'] or $configuration['option_name']. The plugins can be found in the Drupal\module_name\Plugin\migrate\destination namespace. The class itself would be in a file under the /src/Plugin/migrate/destination/ directory of the module.</li> <li>Look up in the class hierarchy. The destination plugin can use any configuration set directly in its definition class and any parent class. There might be multiple layers of inheritance.</li> <li>Check if the plugin requires the presence of specific destination properties to be set in the process section. This is usually documented in the class’ docblock, but you can also look for code like $row-&gt;getDestinationProperty('property_name').</li> </ul><p>What did you learn in today’s article? Did you know that migrate destination plugins can inherit configuration keys from their class hierarchy? Were you aware that there are so many destination plugins? Other than the ones listed here, which destination plugins have you used? Please share your answers in the comments. Also, we would be grateful if you shared this article with your friends and colleagues.</p> </div> 2020 July 15 Mauricio Dinarte https://agaric.coop/Drupal%20migrations%20reference%3A%20List%20of%20configuration%20options%20for%20destination%20plugins Drupal migrations reference: List of configuration options for source plugins https://agaric.coop/ <div class="flow_middle"> <p>In a previous article we explained <a href="https://agaric.coop/blog/understanding-syntax-drupal-migrations">the syntax used to write Drupal migration</a>. We also provided references of <a href="https://agaric.coop/blog/drupal-migrations-reference-list-subfields-field-type">subfields</a> and <a href="https://agaric.coop/blog/drupal-migrations-reference-list-properties-content-entity">content entities' properties</a> including those provided by the <a href="https://agaric.coop/blog/drupal-migrations-reference-list-properties-commerce-content-entity">Commerce module</a>. This time we are going to list the configuration options of many migrate source plugins. For example, when importing from a JSON file you need to specify which data fetcher and parser to use. In the case of CSV migrations, the source plugin configuration changes depending on the presence of a headers row. Finding out which options are available might require some Drupal development knowledge. To make the process easier, in today’s article we are presenting a reference of available configuration options for migrate source plugins provided by Drupal core and some contributed modules.</p> <p><img alt="List of configuration options for source plugins" data-entity-type="file" data-entity-uuid="908400f4-5c34-45e4-b277-d934792319f5" src="/sites/default/files/inline-images/migrations-source-plugin-configuration-options-list.jpg" width="960" height="540" /></p> <p>For each migrate source plugin we will present: the module that provides it, the class that defines it, the class that the plugin extends, and any inherited options from the class hierarchy. For each plugin configuration option we will list its name, type, a description, and a note if it is optional.</p> <h2 id="SourcePluginBase">SourcePluginBase (abstract class)</h2> <p><strong>Module</strong>: Migrate (Drupal Core)<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21source%21SourcePluginBase.php/class/SourcePluginBase/9.0.x">Drupal\migrate\Plugin\migrate\source\SourcePluginBase</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Plugin%21PluginBase.php/class/PluginBase/9.0.x">Drupal\Core\Plugin\PluginBase</a></p> <p>This abstract class is extended by most migrate source plugins. This means that the provided configuration keys apply to any source plugin extending it.</p> <p>List of configuration keys:</p> <ol><li><strong>skip_count</strong>: An optional boolean value. If set, do not attempt to count the source. This includes status and import operations.</li> <li><strong>cache_counts</strong>: An optional boolean value. If set, cache the source count. This saves time calculating the number of available records in the source when a count operation is requested.</li> <li><strong>cache_key</strong>: An optional string value. Uniquely named cache key used for cache_counts.</li> <li><strong>track_changes</strong>: An optional boolean value. If set, the Migrate API will keep a hash of the source rows to determine whether the incoming data has changed. If the hash is different the record is re-imported.</li> <li><strong>high_water_property</strong>: An optional array value. If set, only content with a higher value will be imported. The value is usually a timestamp or serial ID indicating what was the last imported record. This key is configured as an associate array. The name key indicates the column in the source that will be used for the comparison. The alias key is optional and if set it serves as a table alias for the column name.</li> <li><strong>source_module</strong>: An optional string value. Identifies the system providing the data the source plugin will read. If not set, the Migrate API tries to read the value from the source plugin annotation. The source plugin itself determines how the value is used. For example, Migrate Drupal's source plugins expect source_module to be the name of a module that must be installed and enabled in the source database.</li> </ol><p>The high_water_property and track_changes are mutually exclusive. They are both designed to conditionally import new or updated records from the source. Hence, only one can be configured per migration definition file.</p> <h2 id="SqlBase">SqlBase (abstract class)</h2> <p><strong>Module</strong>: Migrate (Drupal Core)<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21source%21SqlBase.php/class/SqlBase/9.0.x">Drupal\migrate\Plugin\migrate\source\SqlBase</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21source%21SourcePluginBase.php/class/SourcePluginBase/9.0.x">Drupal\migrate\Plugin\migrate\source\SourcePluginBase</a><br /><strong>Inherited configuration options</strong>: skip_count, cache_counts, cache_key, track_changes, high_water_property, and source_module.</p> <p>This abstract class is extended by migrate source plugins whose data may be fetched via a database connection. This means that the provided configuration keys apply to any source plugin extending it.</p> <p>In addition to the keys provided in the parent class chain, this abstract class provides the following configuration keys:</p> <ol><li><strong>key</strong>: An optional string value. The database key name. Defaults to 'migrate'.</li> <li><strong>target</strong>: An optional string value. The database target name. Defaults to 'default'.</li> <li><strong>database_state_key</strong>: An optional string value. Name of the state key which contains an array with database connection information. The Migrate API will consult the <a href="https://www.drupal.org/docs/8/api/state-api/overview">States API</a> using the provided key. The returned value should be an associative array with at least two keys: key and target to determine which database connection to use. A third key database can also be included containing database connection information as seen in the snippet below.</li> <li><strong>batch_size</strong>: An optional integer value. Number of records to fetch from the database during each batch. If omitted, all records are fetched in a single query.</li> <li><strong>ignore_map</strong>: An optional boolean value. Source data is joined to the map table by default to improve performance. If set to TRUE, the map table will not be joined. Using expressions in the query may result in column aliases in the JOIN clause which would be invalid SQL. If you run into this, set ignore_map to TRUE.</li> </ol><p>To explain how these configuration keys are used, consider the following database connections:</p> <p><code>&lt;?php $databases['default']['default'] = [ 'database' =&gt; 'drupal-8-or-9-database-name', 'username' =&gt; 'drupal-8-or-9-database-username', 'password' =&gt; 'drupal-8-or-9-database-password', 'host' =&gt; 'drupal-8-or-9-database-server', 'port' =&gt; '3306', 'namespace' =&gt; 'Drupal\\Core\\Database\\Driver\\mysql', 'driver' =&gt; 'mysql', ]; $databases['migrate']['default'] = [ 'database' =&gt; 'drupal-6-or-7-database-name', 'username' =&gt; 'drupal-6-or-7-database-username', 'password' =&gt; 'drupal-6-or-7-database-password', 'host' =&gt; 'drupal-6-or-7-database-server', 'port' =&gt; '3306', 'namespace' =&gt; 'Drupal\\Core\\Database\\Driver\\mysql', 'driver' =&gt; 'mysql', ];</code></p> <p>This snippet can be added to settings.php or settings.local.php. The $databases array is a nested array of at least three levels. The first level defines the database keys: default and migrate in our example. The second level defines the database targets: default in both cases. The third level is an array with connection details for each key/target combination. This <a href="https://www.drupal.org/docs/8/api/database-api/database-configuration">documentation page</a> contains more information about database configuration.</p> <p>Based on the specified configuration values, this is how the Migrate API determines which database connection to use:</p> <ul><li>If the source plugin configuration contains database_state_key, its value is taken as the name of a States API key that specifies an array with the database configuration.</li> <li>Otherwise, if the source plugin configuration contains key, the database configuration with that name is used.</li> <li>Otherwise, load a fallback state key from the States API. The value that it tries to read is the global state key: migrate.fallback_state_key.</li> <li>Otherwise, the database connection named migrate is used by default.</li> <li>If all of the above steps fail, a RequirementsException is thrown.</li> </ul><p>Note that all values configuration keys are optional. If none is set, the plugin will default to use the connection specified under $databases['migrate']['default']. At least, set the key configuration even if the value is migrate. This would make it explicit which connection is being used.</p> <h2 id="DrupalSqlBase">DrupalSqlBase (abstract class)</h2> <p><strong>Module</strong>: Migrate Drupal (Drupal Core)<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate_drupal%21src%21Plugin%21migrate%21source%21DrupalSqlBase.php/class/DrupalSqlBase/9.0.x">Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21source%21SqlBase.php/class/SqlBase/9.0.x">Drupal\migrate\Plugin\migrate\source\SqlBase</a><br /><strong>Inherited configuration options</strong>: skip_count, cache_counts, cache_key, track_changes, high_water_property, source_module, key, target, database_state_key, batch_size, and ignore_map.</p> <p>This abstract class provides general purpose helper methods that are commonly needed when writing source plugins that use a Drupal database as a source. For example, check if the given module exists and read Drupal configuration variables. Check the linked class documentation for more available methods.</p> <p>In addition to the keys provided in the parent class chain, this abstract class provides the following configuration key:</p> <ol><li><strong>constants</strong>: An optional array value used to add module dependencies. The value is an associative array with two possible elements. The first uses the entity_type key to add a dependency on the module that provides the specified entity type. The second uses the module key to directly add a dependency on the specified module. In both cases, the <a href="https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21DependencyTrait.php/trait/DependencyTrait/9.0.x">DependencyTrait</a> is used for setting the dependencies.</li> </ol><p><strong>Warning</strong>: A plugin extending this abstract class might want to use this configuration key in the source definition to set module dependencies. If so, the expected keys might clash with other <a href="https://understanddrupal.com/articles/using-constants-and-pseudofields-data-placeholders-drupal-migration-process-pipeline#source-constants">source constants used in the process pipeline</a>. Arrays keys in PHP are case sensitive. Using uppercase in custom source constants might avoid this clash, but it is preferred to use a different name to avoid confusion.</p> <p>This abstract class is extended by dozens of <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate_drupal%21src%21Plugin%21migrate%21source%21DrupalSqlBase.php/class/uses/DrupalSqlBase/9.0.x">core classes that provide an upgrade path from Drupal 6 and 7</a>. It is also used by the <a href="https://www.drupal.org/project/commerce_migrate">Commerce Migrate module</a> to read <a href="https://git.drupalcode.org/project/commerce_migrate/-/blob/8.x-2.x/modules/commerce/src/Plugin/migrate/source/commerce1/ProductType.php">product types</a>, <a href="https://git.drupalcode.org/project/commerce_migrate/-/blob/8.x-2.x/modules/commerce/src/Plugin/migrate/source/commerce1/ProductDisplayType.php">product display types</a>, and <a href="https://git.drupalcode.org/project/commerce_migrate/-/blob/8.x-2.x/modules/commerce/src/Plugin/migrate/source/commerce1/ShippingFlatRate.php">shipping flat rates</a> from a Commerce 1 database. The same module follows a similar approach to <a href="https://git.drupalcode.org/project/commerce_migrate/-/tree/8.x-2.x/modules/ubercart/src/Plugin/migrate/source">read data from an Ubercart database</a>. The <a href="https://www.drupal.org/project/paragraphs">Paragraphs module</a> also <a href="https://git.drupalcode.org/project/paragraphs/-/blob/8.x-1.x/src/Plugin/migrate/source/DrupalSqlBase.php">extends it</a> to add and implement <a href="https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Component%21Plugin%21ConfigurableInterface.php/interface/ConfigurableInterface/9.0.x">Configurable Plugin interface</a> so it can import <a href="https://git.drupalcode.org/project/paragraphs/-/blob/8.x-1.x/src/Plugin/migrate/source/d7/FieldCollectionType.php">field collection types</a> and <a href="https://git.drupalcode.org/project/paragraphs/-/blob/8.x-1.x/src/Plugin/migrate/source/d7/ParagraphsType.php">paragraphs types</a> from Drupal 7.</p> <h2 id="d8_config">Config</h2> <p><strong>Module</strong>: Migrate Drupal (Drupal Core). <strong>Plugin ID</strong>: d8_config<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate_drupal%21src%21Plugin%21migrate%21source%21d8%21Config.php/class/Config/9.0.x">Drupal\migrate_drupal\Plugin\migrate\source\d8\Config</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate_drupal%21src%21Plugin%21migrate%21source%21DrupalSqlBase.php/class/DrupalSqlBase/9.0.x">Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase</a><br /><strong>Inherited configuration options</strong>: skip_count, cache_counts, cache_key, track_changes, high_water_property, source_module, key, target, database_state_key, batch_size, ignore_map, and constants.</p> <p>This plugin allows reading configuration values from a Drupal 8 site by reading its config table.</p> <p>In addition to the keys provided in the parent class chain, this plugin does not define extra configuration keys. And example configuration for this plugin would be:</p> <p><code>source: plugin: d8_config key: migrate skip_count: true</code></p> <p>In this case we are setting the key property from SqlBase to use the migrate default database connection. The skip_count from SourcePluginBase indicates that there is no need to count how many records exist in the source database before executing migration operations like importing them.</p> <p>This plugin is presented to show that Drupal core already offers a way to migrate data from Drupal 8. Remember that there are dozens of other plugins extending DrupalSqlBase. It would be impractical to list them all here. See this <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate_drupal%21src%21Plugin%21migrate%21source%21DrupalSqlBase.php/class/uses/DrupalSqlBase/9.0.x">API page</a> for a list of all of them.</p> <h2 id="csv">CSV</h2> <p><strong>Module</strong>: <a href="https://www.drupal.org/project/migrate_source_csv">Migrate Source CSV</a>. <strong>Plugin ID</strong>: csv<br /><strong>Class</strong>: <a href="https://git.drupalcode.org/project/migrate_source_csv/-/blob/8.x-3.x/src/Plugin/migrate/source/CSV.php">Drupal\migrate_source_csv\Plugin\migrate\source\CSV</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21source%21SourcePluginBase.php/class/SourcePluginBase/9.0.x">Drupal\migrate\Plugin\migrate\source\SourcePluginBase</a><br /><strong>Inherited configuration options</strong>: skip_count, cache_counts, cache_key, track_changes, high_water_property, and source_module.</p> <p>This plugin allows reading data from a CSV file. We used this plugin in the <a href="https://agaric.coop/blog/migrating-csv-files-drupal">CSV migration example</a> of the <a href="https://agaric.coop/31-days-drupal-migrations">31 days of migration series</a>.</p> <p>In addition to the keys provided in the parent class chain, this plugin provides the following configuration keys:</p> <ol><li><strong>path</strong>: A string value. It contains the path to the CSV file. Starting with the 8.x-3.x branch, <a href="https://api.drupal.org/api/drupal/namespace/Drupal!Core!StreamWrapper/9.0.x">stream wrappers</a> are supported. The original article contains more details on specifying the <a href="https://agaric.coop/blog/migrating-csv-files-drupal#file-location">location of the CSV file</a>.</li> <li><strong>ids</strong>: An array of string values. The column names listed are used to uniquely identify each record.</li> <li><strong>header_offset</strong>: An optional integer value. The index of record to be used as the CSV header and the thereby each record's field name. It defaults to zero (0) because the index is zero-based. For CSV files with no header row the value should be set to null.</li> <li><strong>fields</strong>: An optional associative array value. It contains a nested array of names and labels to use instead of a header row. If set, it will overwrite the column names obtained from header_offset.</li> <li><strong>delimiter</strong>: An optional string value. It contains a one character column delimiter. It defaults to a comma (,). For example, if your file uses tabs as delimiter, you set this configuration to \t.</li> <li><strong>enclosure</strong>: An optional string value. It contains one character used to enclose the column values. Defaults to double quotation marks (").</li> <li><strong>escape</strong>: An optional string value. It contains one character used for character escaping in the column values. It defaults to a backslash (\).</li> </ol><p><strong>Important</strong>: The configuration options changed significantly between the 8.x-3.x and 8.x-2.x branches. Refer to this <a href="https://www.drupal.org/node/3060246">change record</a> for a reference of how to configure the plugin for the 8.x-2.x.</p> <p>For reference, below is the source plugin configuration used in the <a href="https://agaric.coop/blog/migrating-csv-files-drupal">CSV migration example</a>:</p> <p><code>source: plugin: csv path: modules/custom/ud_migrations/ud_migrations_csv_source/sources/udm_photos.csv ids: [photo_id] header_offset: null fields: - name: photo_id label: 'Photo ID' - name: photo_url label: 'Photo URL'</code></p> <h2 id="spreadsheet">Spreadsheet</h2> <p><strong>Module</strong>: <a href="https://www.drupal.org/project/migrate_spreadsheet">Migrate Spreadsheet</a>. <strong>Plugin ID</strong>: spreadsheet<br /><strong>Class</strong>: <a href="https://git.drupalcode.org/project/migrate_spreadsheet/-/blob/8.x-1.x/src/Plugin/migrate/source/Spreadsheet.php">Drupal\migrate_spreadsheet\Plugin\migrate\source\Spreadsheet</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21source%21SourcePluginBase.php/class/SourcePluginBase/9.0.x">Drupal\migrate\Plugin\migrate\source\SourcePluginBase</a><br /><strong>Inherited configuration options</strong>: skip_count, cache_counts, cache_key, track_changes, high_water_property, and source_module.</p> <p>This plugin allows reading data from Microsoft Excel and LibreOffice Calc files. It requires the <a href="https://github.com/PHPOffice/PhpSpreadsheet">PhpOffice/PhpSpreadsheet</a> library <strong>and</strong> many PHP extensions including ext-zip. Check <a href="https://github.com/PHPOffice/PhpSpreadsheet/blob/master/composer.json#L41">this page</a> for a full list of dependencies. We used this plugin in the <a href="https://agaric.coop/blog/migrating-microsoft-excel-and-libreoffice-calc-files-drupal">spreadsheet migration examples</a> of the <a href="/31-days-of-migrations">31 days of migration series</a>.</p> <p>In addition to the keys provided in the parent class chain, this plugin provides the following configuration keys:</p> <ol><li><strong>file</strong>: A string value. It stores the path to the document to process. You can use a relative path from the Drupal root, an absolute path, or <a href="https://agaric.coop/blog/migrating-csv-files-drupal#file-location">stream wrappers</a>.</li> <li><strong>worksheet</strong>: A string value. It contains the name of the one worksheet to process.</li> <li><strong>header_row</strong>: An optional integer value. This number indicates which row contains the headers. Contrary to <a href="https://agaric.coop/blog/migrating-csv-files-drupal">CSV migrations</a>, the row number is not zero-based. So, set this value to 1 if headers are on the first row, 2 if they are on the second, and so on.</li> <li><strong>origin</strong>: An optional string value. It defaults to A2. It indicates which non-header cell contains the first value you want to import. It assumes a grid layout and you only need to indicate the position of the top-left cell value.</li> <li><strong>columns</strong>: An optional array value. It is the list of columns you want to make available for the migration. In case of files with a header row, use those header values in this list. Otherwise, use the default title for columns: A, B, C, etc. If this setting is missing, the plugin will return all columns. This is not ideal, especially for very large files containing more columns than needed for the migration.</li> <li><strong>row_index_column</strong>: An optional string value. This is a special column that contains the row number for each record. This can be used as a unique identifier for the records in case your dataset does not provide a suitable value. Exposing this special column in the migration is up to you. If so, you can come up with any name as long as it does not conflict with header row names set in the columns configuration. Important: this is an autogenerated column, not any of the columns that comes with your dataset.</li> <li><strong>keys</strong>: An optional associative array value. If not set, it defaults to the value of row_index_column. It contains the "columns" that uniquely identify each record. The keys are column names as defined in the data_rows key. The values is an array with a single member with key 'type' and value a column type such as 'integer'. For files with a header row, you can use the values set in the columns configuration. Otherwise, use default column titles like A, B, C, etc. In both cases, you can use the row_index_column column if it was set.</li> </ol><p>Note that nowhere in the plugin configuration you specify the file type. The same setup applies for both Microsoft Excel and LibreOffice Calc files. The library will take care of detecting and validating the proper type.</p> <p>For reference, below is the source plugin configuration used in the <a href="https://agaric.coop/blog/migrating-microsoft-excel-and-libreoffice-calc-files-drupal#header-row">LibreOffice Calc migration example</a>:</p> <p><code>source: plugin: spreadsheet file: modules/custom/ud_migrations/ud_migrations_sheets_sources/sources/udm_book_paragraph.ods worksheet: 'UD Example Sheet' header_row: 1 origin: A2 columns: - book_id - book_title - 'Book author' row_index_column: 'Document Row Index' keys: book_id: type: string</code></p> <h2 id="SourcePluginExtension">SourcePluginExtension (abstract class)</h2> <p><strong>Module</strong>: <a href="https://www.drupal.org/project/migrate_plus">Migrate Plus</a><br /><strong>Class</strong>: <a href="https://git.drupalcode.org/project/migrate_plus/-/blob/8.x-5.x/src/Plugin/migrate/source/SourcePluginExtension.php">Drupal\migrate_plus\Plugin\migrate\source\SourcePluginExtension</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21source%21SourcePluginBase.php/class/SourcePluginBase/9.0.x">Drupal\migrate\Plugin\migrate\source\SourcePluginBase</a><br /><strong>Inherited configuration options</strong>: skip_count, cache_counts, cache_key, track_changes, high_water_property, and source_module.</p> <p>This abstract class provides extra configuration keys. It is extended by the URL plugin (explained later) and by source plugins provided by other modules like <a href="https://www.drupal.org/project/feeds_migrate">Feeds Migrate</a>.</p> <p>In addition to the keys provided in the parent class chain, this abstract class provides the following configuration keys:</p> <ol><li><strong>fields</strong>: An associative array value. Each element represents a field that will be made available to the migration. The following options can be set:</li> <li><strong>name</strong>: A string value. This is how the field is going to be referenced in the migration. The name itself can be arbitrary. If it contains spaces, you need to put double quotation marks (") around it when referring to it in the migration.</li> <li><strong>label</strong>: An optional string value. This is a description used when presenting details about the migration. For example, in the user interface provided by the Migrate Tools module. When defined, you do not use the label to refer to the field. Keep using the name.</li> <li><strong>selector</strong>: A string value. This is another XPath-like string to find the field to import. The value must be relative to the location specified by the item_selector configuration. In the example, the fields are direct children of the records to migrate. Therefore, only the property name is specified (e.g., unique_id). If you had nested objects or arrays, you would use a slash (/) character to go deeper in the hierarchy. This will be demonstrated in the image and paragraph migrations.</li> <li><strong>ids</strong>: An associative array value. It contains the "columns" that uniquely identify each record. The keys are column names as defined in the data_rows key. The values is an array with a single member with key 'type' and value a column type such as 'integer'.</li> </ol><p>See the code snippet for the Url plugin in the next section for an example of how these configuration options are used.</p> <h2 id="url">Url (used for JSON, XML, SOAP, and Google Sheets migrations)</h2> <p><strong>Module</strong>: <a href="https://www.drupal.org/project/migrate_plus">Migrate Plus</a>. <strong>Plugin ID</strong>: url<br /><strong>Class</strong>: <a href="https://git.drupalcode.org/project/migrate_plus/-/blob/8.x-5.x/src/Plugin/migrate/source/Url.php">Drupal\migrate_plus\Plugin\migrate\source\Url</a><br /><strong>Extends</strong>: <a href="https://git.drupalcode.org/project/migrate_plus/-/blob/8.x-5.x/src/Plugin/migrate/source/SourcePluginExtension.php">Drupal\migrate_plus\Plugin\migrate\source\SourcePluginExtension</a><br /><strong>Inherited configuration options</strong>: skip_count, cache_counts, cache_key, track_changes, high_water_property, source_module, <strong>fields</strong>, and <strong>ids</strong>.</p> <p>This plugin allows reading data from URLs. Using data parser plugins it is possible to fetch data from <a href="https://agaric.coop/blog/migrating-json-files-drupal">JSON</a>, <a href="https://agaric.coop/blog/migrating-xml-files-drupal">XML</a>, SOAP, and <a href="https://agaric.coop/blog/migrating-google-sheets-drupal">Google Sheets</a>. Note that this source plugin uses other plugins provided by Migrate Plus that might require extra configuration keys in addition to the ones explicitly defined in the plugin class. Those will also be listed.</p> <p>In addition to the keys provided in the parent class chain, this plugin provides the following configuration keys:</p> <ol><li><strong>urls</strong>: An array of string values. It contains the source URLs to retrieve. If the file data fetcher plugin is used, the location can be a relative path from the Drupal root, an absolute path, a fully-qualified URL, or a stream wrapper. See this article for details on <a href="https://agaric.coop/blog/migrating-json-files-drupal#file-location">location of the JSON file</a>. If the HTTP data fetcher plugin is used, the value can use any protocol supported by curl. See this article for more details on importing <a href="https://agaric.coop/blog/migrating-json-files-drupal#remote-file">remote JSON files</a>.</li> <li><strong>data_parser_plugin</strong>: A string value. It indicates which data parser plugin to use. Possible values provided by the Migrate Plus module are json, xml, simple_xml, and soap. If the <a href="https://www.drupal.org/project/migrate_google_sheets">Migrate Google Sheets module</a> is installed, it is possible to set the value to google_sheets. Review the relevant articles in the <a href="/31-days-of-migrations">31 days of migration series</a> to know more about how each of them are used.</li> </ol><p>The <strong>data parser plugins</strong> provide the following configuration keys:</p> <ol><li><strong>item_selector</strong>: A string value. It indicates where in the source file lies the array of records to be migrated. Its value is an XPath-like string used to traverse the file hierarchy. Note that a slash (/) is used to separate each level in the hierarchy.</li> <li><strong>data_fetcher_plugin</strong>: A string value. It indicates which data parser plugin to use. Possible values provided by the Migrate Plus module are file and http.</li> </ol><p>The <strong>HTTP data fetcher plugins</strong> provide the following configuration keys:</p> <ol><li><strong>headers</strong>: An associative array value. The key/value pairs represent HTTP headers to be sent when the request is made.</li> <li><strong>authentication</strong>: An associative array value. One of the elements in the array should be the plugin key which indicates the authentication plugin to use. Possible values provided by the Migrate Plus module are basic, digest, and oauth2. Other elements in the array will depend on the selected authentication plugin.</li> </ol><p>The <strong>basic and digest authentication plugins</strong> provide the following configuration keys:</p> <ol><li><strong>username</strong>: A string value.</li> <li><strong>password</strong>: A string value.</li> </ol><p>The <strong>OAuth2 authentication plugin</strong> requires the <a href="https://github.com/Sainsburys/guzzle-oauth2-plugin">sainsburys/guzzle-oauth2-plugin</a> composer package to work. It provides the following configuration keys:</p> <ol><li><strong>base_uri</strong>: A string value.</li> <li><strong>grant_type</strong>: A string value. Possible values are authorization_code, client_credentials, urn:ietf:params:oauth:grant-type:jwt-bearer, password, and refresh_token. Each of these might require extra configuration values.</li> </ol><p>The <strong>client credentials grant type</strong> requires the following configuration keys:</p> <ol><li><strong>token_url</strong>: A string value.</li> <li><strong>client_id</strong>: A string value.</li> <li><strong>client_secret</strong>: A string value.</li> </ol><p>For configuration keys required by other grant types, refer to the <a href="https://github.com/Sainsburys/guzzle-oauth2-plugin/tree/master/src/GrantType">classes that implement them</a>. Read this <a href="https://agaric.coop/blog/adding-http-request-headers-and-authentication-remote-json-and-xml-drupal-migrations">article on adding HTTP request headers and authentication parameters</a> for example configurations.</p> <p>There are many combinations possible to configure this plugin. In the <a href="/31-days-of-migrations">31 days of migration series</a> there are many example configurations. For reference, below is the source plugin configuration used in the <a href="https://agaric.coop/blog/migrating-json-files-drupal#migrating-nodes">local JSON node migration example</a>:</p> <p><code>source: plugin: url data_fetcher_plugin: file data_parser_plugin: json urls: - modules/custom/ud_migrations/ud_migrations_json_source/sources/udm_data.json item_selector: /data/udm_people fields: - name: src_unique_id label: 'Unique ID' selector: unique_id - name: src_name label: 'Name' selector: name - name: src_photo_file label: 'Photo ID' selector: photo_file - name: src_book_ref label: 'Book paragraph ID' selector: book_ref ids: src_unique_id: type: integer</code></p> <h2 id="embedded_data">EmbeddedDataSource</h2> <p><strong>Module</strong>: Migrate (Drupal Core). <strong>Plugin ID</strong>: embedded_data<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21source%21EmbeddedDataSource.php/class/EmbeddedDataSource/9.0.x">Drupal\migrate\Plugin\migrate\source\EmbeddedDataSource</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21source%21SourcePluginBase.php/class/SourcePluginBase/9.0.x">Drupal\migrate\Plugin\migrate\source\SourcePluginBase</a><br /><strong>Inherited configuration options</strong>: skip_count, cache_counts, cache_key, track_changes, high_water_property, and source_module.</p> <p>This plugin allows the definition of data to be imported right inside the migration definition file. We used this plugin in many of the examples of the <a href="/31-days-of-migrations">31 days of migration series</a>. It is also used in many core tests for the Migrate API itself.</p> <p>In addition to the keys provided in the parent class chain, this abstract class provides the following configuration keys:</p> <ol><li><strong>data_rows</strong>: An array of all the records to be migrated. Each record might contain an arbitrary number of key-value pairs representing "columns" of data to be imported.</li> <li><strong>ids</strong>: An associative array. It contains the "columns" that uniquely identify each record. The keys are column names as defined in the data_rows key. The values is an array with a single member with key 'type' and value a column type such as 'integer'.</li> </ol><p>Many examples of <a href="/31-days-of-migrations">31 days of migration series</a> use this plugin. You can get the example modules from <a href="https://github.com/dinarcon/ud_migrations">this repository</a>. For reference, below is the source plugin configuration used in the <a href="https://agaric.coop/blog/writing-your-first-drupal-migration">first migration example</a>:</p> <p><code>source: plugin: embedded_data data_rows: - unique_id: 1 creative_title: 'The versatility of Drupal fields' engaging_content: 'Fields are Drupal''s atomic data storage mechanism...' - unique_id: 2 creative_title: 'What is a view in Drupal? How do they work?' engaging_content: 'In Drupal, a view is a listing of information. It can a list of nodes, users, comments, taxonomy terms, files, etc...' ids: unique_id: type: integer</code></p> <p>This plugin can also be used to create default content when the data is known in advance. We often present Drupal site building workshops. To save time, we use this plugin to create nodes which are later used when explaining how to create Views. Check this <a href="https://github.com/dinarcon/wdc_emprendimiento_parcial">repository</a> for an example of this. Note that it uses a different directory structure to store the migrations as explained in this <a href="https://agaric.coop/blog/defining-drupal-migrations-configuration-entities-migrate-plus-module">blog post</a>.</p> <h2 id="content_entity">ContentEntity</h2> <p><strong>Module</strong>: Migrate Drupal (Drupal Core). <strong>Plugin ID</strong>: content_entity<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate_drupal%21src%21Plugin%21migrate%21source%21ContentEntity.php/class/ContentEntity/9.0.x">Drupal\migrate_drupal\Plugin\migrate\source\ContentEntity</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21source%21SourcePluginBase.php/class/SourcePluginBase/9.0.x">Drupal\migrate\Plugin\migrate\source\SourcePluginBase</a><br /><strong>Inherited configuration options</strong>: skip_count, cache_counts, cache_key, track_changes, high_water_property, and source_module.</p> <p>This plugin returns content entities from a Drupal 8 or 9 installation. It uses the Entity API to get the data to migrate. If the source entity type has custom field storage fields or computed fields, this class will need to be extended and the new class will need to load/calculate the values for those fields.</p> <p>In addition to the keys provided in the parent class chain, this plugin provides the following configuration key:</p> <ol><li><strong>entity_type</strong>: A string value. The entity type ID of the entities being migrated. This is calculated dynamically by the deriver so it is only needed if the deriver is not utilized, i.e., a custom source plugin.</li> <li><strong>bundle</strong>: An optionals string value. If set and the entity type is bundleable, only return entities of this bundle.</li> <li><strong>include_translations</strong>: An optional boolean value. If set, entity translations are included in the returned data. It defaults to TRUE.</li> </ol><p>For reference, this is how this plugin is configured to get all nodes of type article in their default language only:</p> <p><code>source: plugin: content_entity:node bundle: article include_translations: false</code></p> <p>Note: this plugin was brought into core in <a href="https://www.drupal.org/project/drupal/issues/2935951">this issue</a> copied from the <a href="https://www.drupal.org/project/migrate_drupal_d8">Drupal 8 migration (source) module</a>. The latter can be used if the source database does not use the default connection.</p> <h2 id="table">Table</h2> <p><strong>Module</strong>: <a href="https://www.drupal.org/project/migrate_plus">Migrate Plus</a>. <strong>Plugin ID</strong>: table<br /><strong>Class</strong>: <a href="https://git.drupalcode.org/project/migrate_plus/-/blob/8.x-5.x/src/Plugin/migrate/source/Table.php">Drupal\migrate_plus\Plugin\migrate\source\Table</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21source%21SqlBase.php/class/SqlBase/9.0.x">Drupal\migrate\Plugin\migrate\source\SqlBase</a><br /><strong>Inherited configuration options</strong>: skip_count, cache_counts, cache_key, track_changes, high_water_property, source_module, key, target, database_state_key, batch_size, and ignore_map.</p> <p>This plugin allows reading data from a single database table. It uses one of the database connections for the site as defined by the options. <a href="https://git.drupalcode.org/project/migrate_plus/-/blob/8.x-5.x/tests/src/Kernel/MigrateTableTest.php">See this test</a> for an example on how to use this plugin.</p> <p>In addition to the keys provided in the parent class chain, this plugin provides the following configuration key:</p> <ol><li><strong>table_name</strong>: An string value. The table to read values from.</li> <li><strong>id_fields</strong>: An associative array value. IDMap compatible array of fields that uniquely identify each record.</li> <li><strong>fields</strong>: An array value. The elements are fields ("columns") present on the source table.</li> </ol><h2 id="empty">EmptySource (migrate module)</h2> <p><strong>Module</strong>: Migrate (Drupal Core). <strong>Plugin ID</strong>: empty<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21source%21EmptySource.php/class/EmptySource/9.0.x">Drupal\migrate\Plugin\migrate\source\EmptySource</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21source%21SourcePluginBase.php/class/SourcePluginBase/9.0.x">Drupal\migrate\Plugin\migrate\source\SourcePluginBase</a><br /><strong>Inherited configuration options</strong>: skip_count, cache_counts, cache_key, track_changes, high_water_property, and source_module.</p> <p>This plugin returns an empty row by default. It can be used as a placeholder to defer setting the source plugin to a deriver. An example of this can be seen in the migrations for <a href="https://api.drupal.org/api/drupal/core%21modules%21content_translation%21migrations%21d6_entity_reference_translation.yml/9.0.x">Drupal 6</a> and <a href="https://api.drupal.org/api/drupal/core%21modules%21content_translation%21migrations%21d7_entity_reference_translation.yml/9.0.x">Drupal 7 entity reference translations</a>. In both cases, the source plugin will be determined by the <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate_drupal%21src%21Plugin%21migrate%21EntityReferenceTranslationDeriver.php/class/EntityReferenceTranslationDeriver/9.0.x">EntityReferenceTranslationDeriver</a>.</p> <p>In addition to the keys provided in the parent class chain, this plugin does not define extra configuration keys. If the plugin is used with <a href="https://agaric.coop/blog/using-constants-and-pseudofields-data-placeholders-drupal-migration-process-pipeline#source-constants">source constants</a>, a single row containing the constant values will be returned. For example:</p> <p><code>source: plugin: empty constants: entity_type: node field_name: body</code></p> <p>The plugin will return a single row containing 'entity_type' and 'field_name' elements, with values of 'node' and 'body', respectively. This is not very useful. For the most part, the plugin is used to defer the definition to a deriver as mentioned before.</p> <h2 id="md_empty">EmptySource (migrate_drupal module)</h2> <p><strong>Module</strong>: Migrate Drupal (Drupal Core). <strong>Plugin ID</strong>: md_empty<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate_drupal%21src%21Plugin%21migrate%21source%21EmptySource.php/class/EmptySource/9.0.x">Drupal\migrate_drupal\Plugin\migrate\source\EmptySource</a><br /><strong>Extends</strong>: <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21migrate%21source%21EmptySource.php/class/EmptySource/9.0.x">Drupal\migrate\Plugin\migrate\source\EmptySource</a><br /><strong>Inherited configuration options</strong>: skip_count, cache_counts, cache_key, track_changes, high_water_property, and source_module.</p> <p>By default, this plugin returns an empty row with Drupal specific config dependencies. If the plugin is used with <a href="https://agaric.coop/blog/using-constants-and-pseudofields-data-placeholders-drupal-migration-process-pipeline#source-constants">source constants</a>, a single row containing the constant values will be returned. These can be seen in the <a href="https://api.drupal.org/api/drupal/core%21modules%21user%21migrations%21user_picture_field.yml/9.0.x">user_picture_field.yml</a> and <a href="https://api.drupal.org/api/drupal/core%21modules%21file%21migrations%21d6_upload_field.yml/9.0.x">d6_upload_field.yml</a> migrations.</p> <p>In addition to the keys provided in the parent class chain, this abstract class provides the following configuration keys:</p> <ol><li><strong>constants</strong>: An optional array value used to add module dependencies. The value is an associative array with one possible element. An entity_type key is used to add a dependency on the module that provides the specified entity type.</li> </ol><h2 id="other-plugins">Available configuration for other migrate source plugins</h2> <p>In Drupal core itself there are more than 100 migrate source plugins, most of which come from the Migrate Drupal module. And many more are made available by contributed modules. It would be impractical to document them all here. To get a list by yourself, load the <a href="https://api.drupal.org/api/drupal/core%21modules%21migrate%21src%21Plugin%21MigrateSourcePluginManager.php/class/MigrateSourcePluginManager/9.0.x">plugin.manager.migrate.source</a> service and call its getFieldStorageDefinitions() method. This will return all migrate source plugins provided by the <strong>modules that are currently enabled</strong> on the site. This Drush command would get the list:</p> <p><code># List of migrate source plugin definitions. $ drush php:eval "print_r(\Drupal::service('plugin.manager.migrate.source')-&gt;getDefinitions());" # List of migrate source plugin ids. $ drush php:eval "print_r(array_keys(\Drupal::service('plugin.manager.migrate.source')-&gt;getDefinitions()));"</code></p> <p>To find out which configuration options are available for any source plugin consider the following:</p> <ul><li>Find the class that defines the plugin and find out which configuration values are read. Some plugins even include the list in the docblock of the class. Search for a pattern similar to $this-&gt;configuration['option_name'] or $configuration['option_name']. The plugins can be found in the Drupal\module_name\Plugin\migrate\source namespace. The class itself would be in a file under the /src/Plugin/migrate/source/ directory of the module.</li> <li>Look up in the class hierarchy. The source plugin can use any configuration set directly in its definition class and any parent class. There might be multiple layers of inheritance.</li> <li>Check if the plugin offers some extension mechanism that would allow it to use other types of plugins. For example, the data fetcher, data parses, and authentication plugins provided by Migrate Plus. The Url migrate source plugin does this.</li> </ul><p>What did you learn in today’s article? Did you know that migrate source plugins can inherit configuration keys from their class hierarchy? Were you aware that there are so many source plugins? Other than the ones listed here, which source plugins have you used? Please share your answers in the comments. Also, we would be grateful if you shared this article with your friends and colleagues.</p> </div> 2020 July 13 Mauricio Dinarte https://agaric.coop/Drupal%20migrations%20reference%3A%20List%20of%20configuration%20options%20for%20source%20plugins Drupal migrations reference: List of properties per content entity https://agaric.coop/ <div class="flow_middle"> <p>In a previous article we explained the <a href="https://agaric.coop/blog/understanding-syntax-drupal-migrations">syntax used to write Drupal migrations</a>. When migrating into content entities, these define several properties that can be included in the process section to populate their values. For example, when importing nodes you can specify the title, publication status, creation date, etc. In the case of users, you can set the username, password, timezone, etc. Finding out which properties are available for an entity might require some Drupal development knowledge. To make the process easier, in today’s article we are presenting a reference of properties available in content entities provided by Drupal core and some contributed modules.</p> <p><img alt="Example migrations mapping of content entity properties" data-entity-type="file" data-entity-uuid="e61f4c86-fbed-40fb-ac5c-ab72ffd6456a" src="/sites/default/files/inline-images/migrations-properties-per-content-entity-list.jpg" width="960" height="540" /></p> <p>For each entity we will present: the module that provides it, the class that defines it, and the available properties. For each property we will list its name, field type, a description, and a note if the field allows unlimited values (i.e. it has an unlimited cardinality). The list of properties available for a content entity depend on many factors. For example, if the entity is revisionable (e.g. revision_default), translatable (e.g. langcode), or both (e.g. revision_translation_affected). The modules that are enabled on the site can also affect the available properties. For instance, if the “Workspaces” module is installed, it will add a workspace property to many content entities. This reference assumes that Drupal was installed using the standard installation profile and all modules that provide content entities are enabled.</p> <p>It is worth noting that entity properties are divided in two categories: base field definitions and field storage configurations. Base field configurations will always be available for the entity. On the other hand, the presence of field storage configurations will depend on various factors. For one, they can only be added to fieldable entities. Attaching the fields to the entity can be done manually by the user, by a module, or by an installation profile. Again, this reference assumes that Drupal was installed using the standard installation profile. Among other things, it adds a user_picture image field to the user entity and body, comment, field_image, and field_tags fields to the node entity. For entities that can have multiple bundles, not all properties provided by the field storage configurations will be available in all bundles. For example, with the standard installation profile all content types will have a body field associated with it, but only the article content type has the field_image, and field_tags fields. If <a href="https://agaric.coop/blog/drupal-migrations-reference-list-subfields-field-type">subfields are available for the field type</a>, you can migrate into them.</p> <h2 id="node">Content (Node) entity</h2> <p><strong>Module</strong>: Node (Drupal Core)<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core!modules!node!src!Entity!Node.php/class/Node/9.0.x">Drupal\node\Entity\Node</a><br /> Related article: <a href="https://agaric.coop/blog/writing-your-first-drupal-migration">Writing your first Drupal migration</a></p> <p>List of base field definitions:</p> <ol><li><strong>nid</strong>: (integer) The node ID.</li> <li><strong>uuid</strong>: (uuid) The node UUID.</li> <li><strong>vid</strong>: (integer) Revision ID.</li> <li><strong>langcode</strong>: (language) Language code (e.g. en).</li> <li><strong>type</strong>: (entity_reference to node_type) Content type machine name.</li> <li><strong>revision_timestamp</strong>: (created) The time that the current revision was created.</li> <li><strong>revision_uid</strong>: (entity_reference to user) The user ID of the author of the current revision.</li> <li><strong>revision_log</strong>: (string_long) Briefly describe the changes you have made.</li> <li><strong>status</strong>: (boolean) Node published when set to TRUE.</li> <li><strong>uid</strong>: (entity_reference to user) The user ID of the content author.</li> <li><strong>title</strong>: (string) Title.</li> <li><strong>created</strong>: (created) The time that the node was created.</li> <li><strong>changed</strong>: (changed) The time that the node was last edited.</li> <li><strong>promote</strong>: (boolean) Node promoted to front page when set to TRUE.</li> <li><strong>sticky</strong>: (boolean) Node sticky at top of lists when set to TRUE.</li> <li><strong>default_langcode</strong>: (boolean) A flag indicating whether this is the default translation.</li> <li><strong>revision_default</strong>: (boolean) A flag indicating whether this was a default revision when it was saved.</li> <li><strong>revision_translation_affected</strong>: (boolean) Indicates if the last edit of a translation belongs to current revision.</li> <li><strong>workspace</strong>: (entity_reference to workspace) Indicates the workspace that this revision belongs to.</li> </ol><p>List of field storage configurations:</p> <ol><li><strong>body</strong>: text_with_summary field.</li> <li><strong>comment</strong>: comment field.</li> <li><strong>field_image</strong>: image field.</li> <li><strong>field_tags</strong>: entity_reference field.</li> </ol><h2 id="user">User entity</h2> <p><strong>Module</strong>: User (Drupal Core)<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core!modules!user!src!Entity!User.php/class/User/9.0.x">Drupal\user\Entity\User</a><br /> Related articles: <a href="https://agaric.coop/blog/migrating-users-drupal-part-1">Migrating users into Drupal - Part 1</a> and <a href="https://agaric.coop/blog/migrating-users-drupal-part-2">Migrating users into Drupal - Part 2</a></p> <p>List of base field definitions:</p> <ol><li><strong>uid</strong>: (integer) The user ID.</li> <li><strong>uuid</strong>: (uuid) The user UUID.</li> <li><strong>langcode</strong>: (language) The user language code.</li> <li><strong>preferred_langcode</strong>: (language) The user's preferred language code for receiving emails and viewing the site.</li> <li><strong>preferred_admin_langcode</strong>: (language) The user's preferred language code for viewing administration pages.</li> <li><strong>name</strong>: (string) The name of this user.</li> <li><strong>pass</strong>: (password) The password of this user (hashed).</li> <li><strong>mail</strong>: (email) The email of this user.</li> <li><strong>timezone</strong>: (string) The timezone of this user.</li> <li><strong>status</strong>: (boolean) Whether the user is active or blocked.</li> <li><strong>created</strong>: (created) The time that the user was created.</li> <li><strong>changed</strong>: (changed) The time that the user was last edited.</li> <li><strong>access</strong>: (timestamp) The time that the user last accessed the site.</li> <li><strong>login</strong>: (timestamp) The time that the user last logged in.</li> <li><strong>init</strong>: (email) The email address used for initial account creation.</li> <li><strong>roles</strong>: (entity_reference to user_role) The roles the user has. Allows unlimited values.</li> <li><strong>default_langcode</strong>: (boolean) A flag indicating whether this is the default translation.</li> </ol><p>List of field storage configurations:</p> <ol><li><strong>user_picture</strong>: image field.</li> </ol><h2 id="taxonomy_term">Taxonomy term entity</h2> <p><strong>Module</strong>: Taxonomy (Drupal Core)<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core!modules!taxonomy!src!Entity!Term.php/class/Term/9.0.x">Drupal\taxonomy\Entity\Term</a><br /> Related article: <a href="https://agaric.coop/blog/migrating-taxonomy-terms-and-multivalue-fields-drupal">Migrating taxonomy terms and multivalue fields into Drupal</a></p> <p>List of base field definitions:</p> <ol><li><strong>tid</strong>: (integer) The term ID.</li> <li><strong>uuid</strong>: (uuid) The term UUID.</li> <li><strong>revision_id</strong>: (integer) Revision ID.</li> <li><strong>langcode</strong>: (language) The term language code.</li> <li><strong>vid</strong>: (entity_reference to taxonomy_vocabulary) The vocabulary to which the term is assigned.</li> <li><strong>revision_created</strong>: (created) The time that the current revision was created.</li> <li><strong>revision_user</strong>: (entity_reference to user) The user ID of the author of the current revision.</li> <li><strong>revision_log_message</strong>: (string_long) Briefly describe the changes you have made.</li> <li><strong>status</strong>: (boolean) Published.</li> <li><strong>name</strong>: (string) Name.</li> <li><strong>description</strong>: (text_long) Description.</li> <li><strong>weight</strong>: (integer) The weight of this term in relation to other terms.</li> <li><strong>parent</strong>: (entity_reference to taxonomy_term) The parents of this term. Allows unlimited values.</li> <li><strong>changed</strong>: (changed) The time that the term was last edited.</li> <li><strong>default_langcode</strong>: (boolean) A flag indicating whether this is the default translation.</li> <li><strong>revision_default</strong>: (boolean) A flag indicating whether this was a default revision when it was saved.</li> <li><strong>revision_translation_affected</strong>: (boolean) Indicates if the last edit of a translation belongs to current revision.</li> <li><strong>workspace</strong>: (entity_reference to workspace) Indicates the workspace that this revision belongs to.</li> </ol><h2 id="file">File entity</h2> <p><strong>Module</strong>: File (Drupal Core)<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core!modules!file!src!Entity!File.php/class/File/9.0.x">Drupal\file\Entity\File</a><br /> Related articles: <a href="https://agaric.coop/blog/migrating-files-and-images-drupal">Migrating files and images into Drupal</a>, <a href="https://agaric.coop/blog/migrating-data-drupal-subfields#images">Migrating images using the image_import plugin</a>, and <a href="https://agaric.coop/blog/using-constants-and-pseudofields-data-placeholders-drupal-migration-process-pipeline#migrating-images">Migrating images using the image_import plugin</a></p> <p>List of base field definitions:</p> <ol><li><strong>fid</strong>: (integer) The file ID.</li> <li><strong>uuid</strong>: (uuid) The file UUID.</li> <li><strong>langcode</strong>: (language) The file language code.</li> <li><strong>uid</strong>: (entity_reference to user) The user ID of the file.</li> <li><strong>filename</strong>: (string) Name of the file with no path components.</li> <li><strong>uri</strong>: (file_uri) The URI to access the file (either local or remote).</li> <li><strong>filemime</strong>: (string) The file's MIME type.</li> <li><strong>filesize</strong>: (integer) The size of the file in bytes.</li> <li><strong>status</strong>: (boolean) The status of the file, temporary (FALSE) and permanent (TRUE).</li> <li><strong>created</strong>: (created) The timestamp that the file was created.</li> <li><strong>changed</strong>: (changed) The timestamp that the file was last changed.</li> </ol><h2 id="media">Media entity</h2> <p><strong>Module</strong>: Media (Drupal Core)<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core!modules!media!src!Entity!Media.php/class/Media/9.0.x">Drupal\media\Entity\Media</a></p> <p>List of base field definitions:</p> <ol><li><strong>mid</strong>: (integer) The media ID.</li> <li><strong>uuid</strong>: (uuid) The media UUID.</li> <li><strong>vid</strong>: (integer) Revision ID.</li> <li><strong>langcode</strong>: (language) Language code (e.g. en).</li> <li><strong>bundle</strong>: (entity_reference to media_type) Media type.</li> <li><strong>revision_created</strong>: (created) The time that the current revision was created.</li> <li><strong>revision_user</strong>: (entity_reference to user) The user ID of the author of the current revision.</li> <li><strong>revision_log_message</strong>: (string_long) Briefly describe the changes you have made.</li> <li><strong>status</strong>: (boolean) Published.</li> <li><strong>uid</strong>: (entity_reference to user) The user ID of the author.</li> <li><strong>name</strong>: (string) Name.</li> <li><strong>thumbnail</strong>: (image) The thumbnail of the media item.</li> <li><strong>created</strong>: (created) The time the media item was created.</li> <li><strong>changed</strong>: (changed) The time the media item was last edited.</li> <li><strong>default_langcode</strong>: (boolean) A flag indicating whether this is the default translation.</li> <li><strong>revision_default</strong>: (boolean) A flag indicating whether this was a default revision when it was saved.</li> <li><strong>revision_translation_affected</strong>: (boolean) Indicates if the last edit of a translation belongs to current revision.</li> <li><strong>workspace</strong>: (entity_reference to workspace) Indicates the workspace that this revision belongs to.</li> </ol><p>List of field storage configurations:</p> <ol><li><strong>field_media_audio_file</strong>: file field.</li> <li><strong>field_media_document</strong>: file field.</li> <li><strong>field_media_image</strong>: image field.</li> <li><strong>field_media_oembed_video</strong>: string field.</li> <li><strong>field_media_video_file</strong>: file field.</li> </ol><h2 id="comment">Comment entity</h2> <p><strong>Module</strong>: Comment (Drupal Core)<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core!modules!comment!src!Entity!Comment.php/class/Comment/9.0.x">Drupal\comment\Entity\Comment</a></p> <p>List of base field definitions:</p> <ol><li><strong>cid</strong>: (integer) The comment ID.</li> <li><strong>uuid</strong>: (uuid) The comment UUID.</li> <li><strong>langcode</strong>: (language) The comment language code.</li> <li><strong>comment_type</strong>: (entity_reference to comment_type) The comment type.</li> <li><strong>status</strong>: (boolean) Published.</li> <li><strong>uid</strong>: (entity_reference to user) The user ID of the comment author.</li> <li><strong>pid</strong>: (entity_reference to comment) The parent comment ID if this is a reply to a comment.</li> <li><strong>entity_id</strong>: (entity_reference to node) The ID of the entity of which this comment is a reply.</li> <li><strong>subject</strong>: (string) Subject.</li> <li><strong>name</strong>: (string) The comment author's name.</li> <li><strong>mail</strong>: (email) The comment author's email address.</li> <li><strong>homepage</strong>: (uri) The comment author's home page address.</li> <li><strong>hostname</strong>: (string) The comment author's hostname.</li> <li><strong>created</strong>: (created) The time that the comment was created.</li> <li><strong>changed</strong>: (changed) The time that the comment was last edited.</li> <li><strong>thread</strong>: (string) The alphadecimal representation of the comment's place in a thread, consisting of a base 36 string prefixed by an integer indicating its length.</li> <li><strong>entity_type</strong>: (string) The entity type to which this comment is attached.</li> <li><strong>field_name</strong>: (string) The field name through which this comment was added.</li> <li><strong>default_langcode</strong>: (boolean) A flag indicating whether this is the default translation.</li> </ol><p>List of field storage configurations:</p> <ol><li><strong>comment_body</strong>: text_long field.</li> </ol><h2 id="aggregator_feed">Aggregator feed entity</h2> <p><strong>Module</strong>: Aggregator (Drupal Core)<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core!modules!aggregator!src!Entity!Feed.php/class/Feed/9.0.x">Drupal\aggregator\Entity\Feed</a></p> <p>List of base field definitions:</p> <ol><li><strong>fid</strong>: (integer) The ID of the aggregator feed.</li> <li><strong>uuid</strong>: (uuid) The aggregator feed UUID.</li> <li><strong>langcode</strong>: (language) The feed language code.</li> <li><strong>title</strong>: (string) The name of the feed (or the name of the website providing the feed).</li> <li><strong>url</strong>: (uri) The fully-qualified URL of the feed.</li> <li><strong>refresh</strong>: (list_integer) The length of time between feed updates. Requires a correctly configured cron maintenance task.</li> <li><strong>checked</strong>: (timestamp) Last time feed was checked for new items, as Unix timestamp.</li> <li><strong>queued</strong>: (timestamp) Time when this feed was queued for refresh, 0 if not queued.</li> <li><strong>link</strong>: (uri) The link of the feed.</li> <li><strong>description</strong>: (string_long) The parent website's description that comes from the &lt;description&gt; element in the feed.</li> <li><strong>image</strong>: (uri) An image representing the feed.</li> <li><strong>hash</strong>: (string) Calculated hash of the feed data, used for validating cache.</li> <li><strong>etag</strong>: (string) Entity tag HTTP response header, used for validating cache.</li> <li><strong>modified</strong>: (timestamp) When the feed was last modified, as a Unix timestamp.</li> </ol><h2 id="aggregator_item">Aggregator feed item entity</h2> <p><strong>Module</strong>: Aggregator (Drupal Core)<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core!modules!aggregator!src!Entity!Item.php/class/Item/9.0.x">Drupal\aggregator\Entity\Item</a></p> <p>List of base field definitions:</p> <ol><li><strong>iid</strong>: (integer) The ID of the feed item.</li> <li><strong>langcode</strong>: (language) The feed item language code.</li> <li><strong>fid</strong>: (entity_reference to aggregator_feed) The aggregator feed entity associated with this item.</li> <li><strong>title</strong>: (string) The title of the feed item.</li> <li><strong>link</strong>: (uri) The link of the feed item.</li> <li><strong>author</strong>: (string) The author of the feed item.</li> <li><strong>description</strong>: (string_long) The body of the feed item.</li> <li><strong>timestamp</strong>: (created) Posted date of the feed item, as a Unix timestamp.</li> <li><strong>guid</strong>: (string_long) Unique identifier for the feed item.</li> </ol><h2 id="block_content">Custom block entity</h2> <p><strong>Module</strong>: Custom Block (Drupal Core)<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core!modules!block_content!src!Entity!BlockContent.php/class/BlockContent/9.0.x">Drupal\block_content\Entity\BlockContent</a></p> <p>List of base field definitions:</p> <ol><li><strong>id</strong>: (integer) The custom block ID.</li> <li><strong>uuid</strong>: (uuid) The custom block UUID.</li> <li><strong>revision_id</strong>: (integer) The revision ID.</li> <li><strong>langcode</strong>: (language) The custom block language code.</li> <li><strong>type</strong>: (entity_reference to block_content_type) The block type.</li> <li><strong>revision_created</strong>: (created) The time that the current revision was created.</li> <li><strong>revision_user</strong>: (entity_reference to user) The user ID of the author of the current revision.</li> <li><strong>revision_log</strong>: (string_long) The log entry explaining the changes in this revision.</li> <li><strong>status</strong>: (boolean) Published.</li> <li><strong>info</strong>: (string) A brief description of your block.</li> <li><strong>changed</strong>: (changed) The time that the custom block was last edited.</li> <li><strong>reusable</strong>: (boolean) A boolean indicating whether this block is reusable.</li> <li><strong>default_langcode</strong>: (boolean) A flag indicating whether this is the default translation.</li> <li><strong>revision_default</strong>: (boolean) A flag indicating whether this was a default revision when it was saved.</li> <li><strong>revision_translation_affected</strong>: (boolean) Indicates if the last edit of a translation belongs to current revision.</li> <li><strong>workspace</strong>: (entity_reference to workspace) Indicates the workspace that this revision belongs to.</li> </ol><p>List of field storage configurations:</p> <ol><li><strong>body</strong>: text_with_summary field.</li> </ol><h2 id="contact_message">Contact message entity</h2> <p><strong>Module</strong>: Contact (Drupal Core)<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core!modules!contact!src!Entity!Message.php/class/Message/9.0.x">Drupal\contact\Entity\Message</a></p> <p>List of base field definitions:</p> <ol><li><strong>uuid</strong>: (uuid) The message UUID.</li> <li><strong>langcode</strong>: (language) The message language code.</li> <li><strong>contact_form</strong>: (entity_reference to contact_form) The ID of the associated form.</li> <li><strong>name</strong>: (string) The name of the person that is sending the contact message.</li> <li><strong>mail</strong>: (email) The email of the person that is sending the contact message.</li> <li><strong>subject</strong>: (string) Subject.</li> <li><strong>message</strong>: (string_long) Message.</li> <li><strong>copy</strong>: (boolean) Whether to send a copy of the message to the sender.</li> <li><strong>recipient</strong>: (entity_reference to user) The ID of the recipient user for personal contact messages.</li> </ol><h2 id="content_moderation_state">Content moderation state entity</h2> <p><strong>Module</strong>: Content Moderation (Drupal Core)<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core!modules!content_moderation!src!Entity!ContentModerationState.php/class/ContentModerationState/9.0.x">Drupal\content_moderation\Entity\ContentModerationState</a></p> <p>List of base field definitions:</p> <ol><li><strong>id</strong>: (integer) ID.</li> <li><strong>uuid</strong>: (uuid) UUID.</li> <li><strong>revision_id</strong>: (integer) Revision ID.</li> <li><strong>langcode</strong>: (language) Language.</li> <li><strong>uid</strong>: (entity_reference to user) The username of the entity creator.</li> <li><strong>workflow</strong>: (entity_reference to workflow) The workflow the moderation state is in.</li> <li><strong>moderation_state</strong>: (string) The moderation state of the referenced content.</li> <li><strong>content_entity_type_id</strong>: (string) The ID of the content entity type this moderation state is for.</li> <li><strong>content_entity_id</strong>: (integer) The ID of the content entity this moderation state is for.</li> <li><strong>content_entity_revision_id</strong>: (integer) The revision ID of the content entity this moderation state is for.</li> <li><strong>default_langcode</strong>: (boolean) A flag indicating whether this is the default translation.</li> <li><strong>revision_default</strong>: (boolean) A flag indicating whether this was a default revision when it was saved.</li> <li><strong>revision_translation_affected</strong>: (boolean) Indicates if the last edit of a translation belongs to current revision.</li> </ol><h2 id="path_alias">URL alias entity</h2> <p><strong>Module</strong>: Path alias (Drupal Core)<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core!modules!path_alias!src!Entity!PathAlias.php/class/PathAlias/9.0.x">Drupal\path_alias\Entity\PathAlias</a></p> <p>List of base field definitions:</p> <ol><li><strong>id</strong>: (integer) ID.</li> <li><strong>uuid</strong>: (uuid) UUID.</li> <li><strong>revision_id</strong>: (integer) Revision ID.</li> <li><strong>langcode</strong>: (language) Language.</li> <li><strong>path</strong>: (string) The path that this alias belongs to.</li> <li><strong>alias</strong>: (string) An alias used with this path.</li> <li><strong>status</strong>: (boolean) Published.</li> <li><strong>revision_default</strong>: (boolean) A flag indicating whether this was a default revision when it was saved.</li> <li><strong>workspace</strong>: (entity_reference to workspace) Indicates the workspace that this revision belongs to.</li> </ol><h2 id="shortcut">Shortcut link entity</h2> <p><strong>Module</strong>: Shortcut (Drupal Core)<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core!modules!shortcut!src!Entity!Shortcut.php/class/Shortcut/9.0.x">Drupal\shortcut\Entity\Shortcut</a></p> <p>List of base field definitions:</p> <ol><li><strong>id</strong>: (integer) The ID of the shortcut.</li> <li><strong>uuid</strong>: (uuid) The UUID of the shortcut.</li> <li><strong>langcode</strong>: (language) The language code of the shortcut.</li> <li><strong>shortcut_set</strong>: (entity_reference to shortcut_set) The bundle of the shortcut.</li> <li><strong>title</strong>: (string) The name of the shortcut.</li> <li><strong>weight</strong>: (integer) Weight among shortcuts in the same shortcut set.</li> <li><strong>link</strong>: (link) The location this shortcut points to.</li> <li><strong>default_langcode</strong>: (boolean) A flag indicating whether this is the default translation.</li> </ol><h2 id="workspace">Workspace entity</h2> <p><strong>Module</strong>: Workspaces (Drupal Core)<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core!modules!workspaces!src!Entity!Workspace.php/class/Workspace/9.0.x">Drupal\workspaces\Entity\Workspace</a></p> <p>List of base field definitions:</p> <ol><li><strong>id</strong>: (string) The workspace ID.</li> <li><strong>uuid</strong>: (uuid) UUID.</li> <li><strong>revision_id</strong>: (integer) Revision ID.</li> <li><strong>uid</strong>: (entity_reference to user) The workspace owner.</li> <li><strong>label</strong>: (string) The workspace name.</li> <li><strong>parent</strong>: (entity_reference to workspace) The parent workspace.</li> <li><strong>changed</strong>: (changed) The time that the workspace was last edited.</li> <li><strong>created</strong>: (created) The time that the workspace was created.</li> <li><strong>revision_default</strong>: (boolean) A flag indicating whether this was a default revision when it was saved.</li> </ol><h2 id="menu_link_content">Custom menu link entity</h2> <p><strong>Module</strong>: Custom Menu Links (Drupal Core)<br /><strong>Class</strong>: <a href="https://api.drupal.org/api/drupal/core!modules!menu_link_content!src!Entity!MenuLinkContent.php/class/MenuLinkContent/9.0.x">Drupal\menu_link_content\Entity\MenuLinkContent</a></p> <p>List of base field definitions:</p> <ol><li><strong>id</strong>: (integer) The entity ID for this menu link content entity.</li> <li><strong>uuid</strong>: (uuid) The content menu link UUID.</li> <li><strong>revision_id</strong>: (integer) Revision ID.</li> <li><strong>langcode</strong>: (language) The menu link language code.</li> <li><strong>bundle</strong>: (string) The content menu link bundle.</li> <li><strong>revision_created</strong>: (created) The time that the current revision was created.</li> <li><strong>revision_user</strong>: (entity_reference to user) The user ID of the author of the current revision.</li> <li><strong>revision_log_message</strong>: (string_long) Briefly describe the changes you have made.</li> <li><strong>enabled</strong>: (boolean) A flag for whether the link should be enabled in menus or hidden.</li> <li><strong>title</strong>: (string) The text to be used for this link in the menu.</li> <li><strong>description</strong>: (string) Shown when hovering over the menu link.</li> <li><strong>menu_name</strong>: (string) The menu name. All links with the same menu name (such as "tools") are part of the same menu.</li> <li><strong>link</strong>: (link) The location this menu link points to.</li> <li><strong>external</strong>: (boolean) A flag to indicate if the link points to a full URL starting with a protocol, like http:// (1 = external, 0 = internal).</li> <li><strong>rediscover</strong>: (boolean) Indicates whether the menu link should be rediscovered.</li> <li><strong>weight</strong>: (integer) Link weight among links in the same menu at the same depth. In the menu, the links with high weight will sink and links with a low weight will be positioned nearer the top.</li> <li><strong>expanded</strong>: (boolean) If selected and this menu link has children, the menu will always appear expanded. This option may be overridden for the entire menu tree when placing a menu block.</li> <li><strong>parent</strong>: (string) The ID of the parent menu link plugin, or empty string when at the top level of the hierarchy.</li> <li><strong>changed</strong>: (changed) The time that the menu link was last edited.</li> <li><strong>default_langcode</strong>: (boolean) A flag indicating whether this is the default translation.</li> <li><strong>revision_default</strong>: (boolean) A flag indicating whether this was a default revision when it was saved.</li> <li><strong>revision_translation_affected</strong>: (boolean) Indicates if the last edit of a translation belongs to current revision.</li> <li><strong>workspace</strong>: (entity_reference to workspace) Indicates the workspace that this revision belongs to.</li> </ol><h2 id="paragraph">Paragraph entity</h2> <p><strong>Module</strong>: <a href="https://www.drupal.org/project/paragraphs">Paragraphs module</a><br /><strong>Class</strong>: <a href="https://git.drupalcode.org/project/paragraphs/-/blob/8.x-1.x/src/Entity/Paragraph.php">Drupal\paragraphs\Entity\Paragraph</a><br /> Related article: <a href="https://agaric.coop/blog/introduction-paragraphs-migrations-drupal">Introduction to paragraphs migrations in Drupal</a></p> <p>List of base field definitions:</p> <ol><li><strong>id</strong>: (integer) ID.</li> <li><strong>uuid</strong>: (uuid) UUID.</li> <li><strong>revision_id</strong>: (integer) Revision ID.</li> <li><strong>langcode</strong>: (language) The paragraphs entity language code.</li> <li><strong>type</strong>: (entity_reference to paragraphs_type) Paragraph type.</li> <li><strong>status</strong>: (boolean) Published.</li> <li><strong>created</strong>: (created) The time that the Paragraph was created.</li> <li><strong>parent_id</strong>: (string) The ID of the parent entity of which this entity is referenced.</li> <li><strong>parent_type</strong>: (string) The entity parent type to which this entity is referenced.</li> <li><strong>parent_field_name</strong>: (string) The entity parent field name to which this entity is referenced.</li> <li><strong>behavior_settings</strong>: (string_long) The behavior plugin settings</li> <li><strong>default_langcode</strong>: (boolean) A flag indicating whether this is the default translation.</li> <li><strong>revision_default</strong>: (boolean) A flag indicating whether this was a default revision when it was saved.</li> <li><strong>revision_translation_affected</strong>: (boolean) Indicates if the last edit of a translation belongs to current revision.</li> <li><strong>workspace</strong>: (entity_reference to workspace) Indicates the workspace that this revision belongs to.</li> </ol><p>List of field storage configurations:</p> <ol><li><strong>field_reusable_paragraph</strong>: entity_reference field.</li> </ol><h2 id="paragraphs_library_item">Paragraphs library item entity</h2> <p><strong>Module</strong>: Paragraphs Library (part of <a href="https://www.drupal.org/project/paragraphs">paragraphs module</a>)<br /><strong>Class</strong>: <a href="https://git.drupalcode.org/project/paragraphs/-/blob/8.x-1.x/modules/paragraphs_library/src/Entity/LibraryItem.php">Drupal\paragraphs_library\Entity\LibraryItem</a></p> <p>List of base field definitions:</p> <ol><li><strong>id</strong>: (integer) ID.</li> <li><strong>uuid</strong>: (uuid) UUID.</li> <li><strong>revision_id</strong>: (integer) Revision ID.</li> <li><strong>langcode</strong>: (language) Language.</li> <li><strong>revision_created</strong>: (created) The time that the current revision was created.</li> <li><strong>revision_uid</strong>: (entity_reference to user) The user ID of the author of the current revision.</li> <li><strong>revision_log</strong>: (string_long) Briefly describe the changes you have made.</li> <li><strong>status</strong>: (boolean) Published.</li> <li><strong>label</strong>: (string) Label.</li> <li><strong>paragraphs</strong>: (entity_reference_revisions) Paragraphs.</li> <li><strong>created</strong>: (created) The time that the library item was created.</li> <li><strong>changed</strong>: (changed) The time that the library item was last edited.</li> <li><strong>uid</strong>: (entity_reference to user) The user ID of the library item author.</li> <li><strong>default_langcode</strong>: (boolean) A flag indicating whether this is the default translation.</li> <li><strong>revision_default</strong>: (boolean) A flag indicating whether this was a default revision when it was saved.</li> <li><strong>revision_translation_affected</strong>: (boolean) Indicates if the last edit of a translation belongs to current revision.</li> <li><strong>workspace</strong>: (entity_reference to workspace) Indicates the workspace that this revision belongs to.</li> </ol><h2 id="profile">Profile entity</h2> <p><strong>Module</strong>: <a href="https://www.drupal.org/project/profile">Profile module</a><br /><strong>Class</strong>: <a href="https://git.drupalcode.org/project/profile/-/blob/8.x-1.x/src/Entity/Profile.php">Drupal\profile\Entity\Profile</a></p> <p>List of base field definitions:</p> <ol><li><strong>profile_id</strong>: (integer) ID.</li> <li><strong>uuid</strong>: (uuid) UUID.</li> <li><strong>revision_id</strong>: (integer) Revision ID.</li> <li><strong>type</strong>: (entity_reference to profile_type) Profile type.</li> <li><strong>revision_created</strong>: (created) The time that the current revision was created.</li> <li><strong>revision_user</strong>: (entity_reference to user) The user ID of the author of the current revision.</li> <li><strong>revision_log_message</strong>: (string_long) Briefly describe the changes you have made.</li> <li><strong>status</strong>: (boolean) Whether the profile is active.</li> <li><strong>uid</strong>: (entity_reference to user) The user that owns this profile.</li> <li><strong>is_default</strong>: (boolean) Whether this is the default profile.</li> <li><strong>data</strong>: (map) A serialized array of additional data.</li> <li><strong>created</strong>: (created) The time when the profile was created.</li> <li><strong>changed</strong>: (changed) The time when the profile was last edited.</li> <li><strong>revision_default</strong>: (boolean) A flag indicating whether this was a default revision when it was saved.</li> <li><strong>workspace</strong>: (entity_reference to workspace) Indicates the workspace that this revision belongs to.</li> </ol><h2 id="contrib">Available properties for other content entities</h2> <p>This reference includes all core content entities and some provided by contributed modules. The next article will include a <a href="https://agaric.coop/blog/drupal-migrations-reference-list-properties-commerce-content-entity">reference for Drupal Commerce content entities</a>. That being said, it would be impractical to cover all contributed modules. To get a list of yourself for other content entities, load the <a href="https://git.drupalcode.org/project/drupal/-/blob/9.0.x/core/lib/Drupal/Core/Entity/EntityFieldManager.php">entity_type.manager</a> service and call its <a href="https://git.drupalcode.org/project/drupal/-/blob/9.0.x/core/lib/Drupal/Core/Entity/EntityFieldManager.php#L430">getFieldStorageDefinitions()</a> method passing the machine name of the entity as a parameter. Although this reference only covers content entities, the same process can be used for configuration entities.</p> <p>What did you learn in today’s article? Did you know that there were so many entity properties in Drupal core? Were you aware that the list of available properties depend on factors like if the entity is fieldable, translatable, and revisionable? Did you know how to find properties for content entities from contributed modules? Please share your answers in the comments. Also, we would be grateful if you shared this article with your friends and colleagues.</p> </div> 2020 July 08 Mauricio Dinarte https://agaric.coop/Drupal%20migrations%20reference%3A%20List%20of%20properties%20per%20content%20entity