01 Aug 2026

feedDrupal.org aggregator

Freelock Blog: Zero to new Drupal site in 133 seconds

Zero to new Drupal site in 133 seconds

Drupal developer local environment setup tools

John Locke

Gabor asked in Slack about how people contributing to Drupal manage multiple different Drupal versions, and different contributed module branches, when using AI agents:

dev corner icon
Dev Corner

01 Aug 2026 10:00pm GMT

BloomIdea: When two terms called Food are not the same term

You have a supplier spreadsheet with a category column that reads Animals > Dogs > Food, and you want Drupal to end up with a real taxonomy tree: Animals, with a child Dogs, with a child Food, each level created only if it does not exist yet, and every product referencing the leaf of its own path.

Neither Drupal core nor Feeds does that on its own. Feeds Tamper Term Hierarchy does: it reads the path out of the column, creates the taxonomy terms that do not already exist, and respects the hierarchy between them. It has been on drupal.org since 2021 and reached its first stable release, 1.0.0, this week.

The interesting part is not the splitting. Any parser can split a string on a delimiter. The interesting part is deciding, for each segment, whether the term you are looking at already exists, and that turns out to be a question about identity that most import tooling gets wrong.

Name is not identity

Take a small catalogue:

sku,name,category
1001,Rope leash,Animals > Dogs > Accessories
1002,Dry food 3kg,Animals > Dogs > Food
1003,Dry food 1kg,Animals > Cats > Food

The tree you want out of it:

Animals
├── Dogs
│   ├── Accessories
│   └── Food
└── Cats
    └── Food

There are two terms called Food in that tree. One is the dog food category, the other is the cat food category, and they have to stay separate. If they collapse into one, every dog product and every cat product point at the same category, your faceted search stops making sense, and the client finds out before you do.

Now ask what "does this term already exist?" means while the third row is being imported. Food exists, in the sense that a term with that name is already in the vocabulary. It is the wrong one. The right answer is that Food under Cats does not exist yet, even though Food under Dogs does.

That is the whole problem in one sentence: in a hierarchy, a term is identified by its name together with its parent, not by its name. A lookup that ignores the parent will happily hand you a term from a different branch.

This is why Feeds' built-in term autocreation cannot be pressed into service here. It matches by name within the vocabulary, because for a flat vocabulary of tags that is exactly right. Give it a path and it does something reasonable and useless: it looks for a term named Animals > Dogs > Food, does not find one, and creates a single term with that literal name, greater-than signs included. One flat term per distinct path.

Resolving a path one parent at a time

The fix follows from the diagnosis. Rather than looking up each segment in the vocabulary, look it up among the children of the segment resolved just before it:

  • Animals is looked up among the root terms.
  • Dogs is looked up among the children of Animals.
  • Food is looked up among the children of Dogs.

Walking Animals > Cats > Food takes a different turn at step two, finds no Food under Cats, and creates one. Two terms, same name, different parents, which is what the source data described all along.

So how does the plugin know which Food is the right one? It does not. The path tells it, one level at a time, and the only thing carried between levels is a single term ID. This is the loop, with the caching and the options stripped out:

$parent = 0;

foreach ($names as $name) {
  $terms = $storage->loadByProperties([
    'name' => $name,
    'vid' => $vocabulary,
    'parent' => $parent,
  ]);

  $term = $terms ? reset($terms) : $this->createTerm($name, $vocabulary, $parent);

  // The term just resolved becomes the parent of the next lookup.
  $parent = (int) $term->id();
}

return $parent;

$parent starts at 0, which is how Drupal spells "no parent, this is a root term". Every lookup is constrained by it, and the last line of the body is what makes the walk work at all.

Trace Animals > Cats > Food through it and the three queries are (Animals, parent 0), (Cats, parent 1), (Food, parent 5). Trace Animals > Dogs > Food and the third one is (Food, parent 3), which is why it finds the dog food term instead of creating a second one. Neither of them ever asks "is there a term called Food?", which is the question that would produce the wrong answer.

One honest caveat: Drupal does allow two sibling terms with the same name under the same parent. If your vocabulary already contains such a pair, reset() picks whichever the storage returns first. The plugin cannot disambiguate what the data itself does not distinguish.

Two further properties fall out of this for free.

The import becomes idempotent. Re-run the same feed and every segment resolves to the term created the first time, so nothing is duplicated. Add a row with a new leaf under an existing branch and only the leaf is created. That matters because supplier feeds are re-imported nightly, and an import that duplicates its own output is worse than no import.

And the created terms carry the hierarchy, not just the labels. You get a tree you can render as a menu, use in a facet, or attach access rules to, rather than a flat list of strings that happen to contain angle brackets.

Setting it up

With Feeds, Feeds Tamper and Feeds Tamper Term Hierarchy installed, on a feed type with a CSV parser:

  1. In Mapping, add your taxonomy term reference field as a target and map the category column to it.
  2. In that target's settings, set Reference by: Term ID. See the warning below.
  3. In the Tamper tab, add the Import Taxonomy Terms Hierarchy plugin to the same source.
  4. Set the input delimiter to whatever separates your levels. It defaults to >, and spaces around each segment are trimmed, so A > B and A>B behave identically.
  5. Pick the vocabulary the terms belong to.
  6. Import.

The one step people miss

Step 2 is worth dwelling on for a moment, because getting it wrong produces a failure that does not look like one.

The plugin creates the terms itself and returns the ID of the last one in the path. If the mapping is left on its default of matching by term name, Feeds receives that number and does the only sensible thing with it: it looks for a term called 23, does not find one, and creates it. The import reports success. The log is clean. The vocabulary contains a perfectly correct hierarchy, built by the tamper, sitting next to a handful of terms called 23, 25 and 26, which are the ones your content actually references.

We reproduced exactly that on a clean Drupal 11 site while preparing this release. If your categories are numbers, this is why.

More than one path per column

Sources often pack several categories into one cell:

sku,name,categories
1001,Rope leash,"Animals > Dogs > Accessories, Animals > Cats > Toys"

That works, by combining two plugins in the right order. Tamper runs its plugins as a pipeline, and when one of them turns a single value into several, the ones after it run once per value:

  1. Explode, using the separator between paths, here ,.
  2. Import Taxonomy Terms Hierarchy, using the separator between levels, here >.

The Explode produces two paths, the hierarchy plugin runs twice, and the field receives two term IDs. Reverse the order and the hierarchy plugin is handed the whole cell, treats the comma as part of a term name, and you get a category called Accessories, Animals.

When you do not want new terms at all

Creating missing terms is the right default for a first import into an empty vocabulary. It is the wrong default when the taxonomy is curated and the feed is a third-party file you do not control, because then a typo at the supplier silently becomes a category.

1.0.0 adds an option for that. Uncheck Allow terms to be auto created and the plugin stops inventing terms: a path that does not fully exist is skipped instead. A supplier renaming Accessories to Accesories shows up as skipped rows rather than as a quietly duplicated branch.

This one came from a support request by someone who wanted precisely that behavior, and from an implementation contributed by someone else two years ago. It sat in the queue until this release, which is on us.

When the source only has part of the path

The opposite case also happens. The vocabulary already holds Animals > Dogs > Food and the source only carries Dogs > Food, because whoever exported it dropped the top level.

By default the first segment has to be a root term, so Dogs > Food creates a second, unrelated Dogs at the root. Correct by the rule above, unhelpful in practice.

Match the first term anywhere in the hierarchy relaxes that first lookup only: the first segment may match a term at any depth, and everything after it resolves underneath whatever it matched. Dogs > Food then attaches to the existing branch.

It is off by default and should stay off unless you need it, because it trades away exactly the identity rule this post is about. When several terms share a name at different depths the first match wins, and which one that is depends on term IDs rather than on anything meaningful.

Where it stands

Feeds Tamper Term Hierarchy 1.0.0 works with Drupal 10 and 11. It depends on Tamper and core's Taxonomy module, and on Feeds Tamper if you are driving it from Feeds, which is the common case but not the only one: it is an ordinary Tamper plugin and works anywhere Tamper plugins run.

composer require drupal/feeds_tamper_term_hierarchy

The release exists because people kept filing issues against it. The autocreate option, the partial path matching, the Drupal 11 compatibility and the dependency cleanup were all reported, and in three cases implemented, by ethant, bbu23, longwave, damienmckenna and kazah. Their names are on the commits.

01 Aug 2026 6:40pm GMT

31 Jul 2026

feedDrupal.org aggregator

The Drop Times: Randy Kolenko on Maestro and Drupal’s Emerging Orchestration Work

Drupal's automation tools solve different problems. The harder question is how they can exchange work without weakening their execution models, permissions, or auditability.

31 Jul 2026 1:51pm GMT

feedSymfony Blog

Introducing Symfony Reprise: The Symfony Integration Layer for Modern Bundlers

For years, Webpack Encore was the answer to asset management in Symfony, and it still works. But the JavaScript bundler landscape has moved on since Encore was designed. Webpack needed a loader wired up for nearly everything (Sass, TypeScript, you name it),…

31 Jul 2026 7:52am GMT

30 Jul 2026

feedSymfony Blog

Symfony 8.0 reaches its end of maintenance

Symfony 8.0.16 was released on July 29, 2026. It is the last release of the 8.0 branch. Symfony's release policy gives standard versions eight months of support, with bug fixes and security fixes ending at the same time. Symfony 8.0.0 was published on November…

30 Jul 2026 10:21am GMT

29 Jul 2026

feedSymfony Blog

Symfony 8.1.3 released

Symfony 8.1.3 has just been released. Read the Symfony upgrade guide to learn more about upgrading Symfony and use the SymfonyInsight upgrade reports to detect the code you will need to change in your project. Tip…

29 Jul 2026 9:51pm GMT

01 Apr 2004

feedPlanet PHP

ezSystems are classy folks

cover
Last week I helped the folks at ezSystems debug some APC problems they were having. The problems ended up being a 64bit architecture problem (they have uber-fast Opterons) and the bug is now fixed in 2.0.3.

Today I received Python & XML from them (off my Amazon wishlist). Thanks guys!

On a side note, my wishlist seems borked. The list I get when I search on my email address or name is not the same one I can edit when I log into the site.

01 Apr 2004 6:53pm GMT

PHP april fools...

1st of April 2004 get's to it's end and I guess it's time, to summarize the recent April fools a bit. Not that I think anyone in the world believes in them, but some were quite funny:

1. Changes to case sensitivity in PHP.
Alan Knowles announced that PHP will change to the studlyCase API and therefor will get everything broken by changing established functions.

2. IBM takes over Zend.
Myself hacked a little article about IBM taking over Zend to make PHP a compete of Java.

3. The first PHP virus has been seen.
Wasn't there one last year, too?

4. PHP has been overtaken by Micro$oft.
Mhhh... a little bit unreliable, if they had been taken over by IBM this morning... Maybe one should first look, what others wrote...

5. And finally, PHP4 and 5 showed their real faces...
Take a look at a phpinfo() output!

I guess I missed some, so feel free to comment on this entry, if you found another!

01 Apr 2004 5:49pm GMT

PHP Virus Attacking Web Hosts

Symantec have a report of the virus here. I've yet to see any of the PHP news sites picking up on it but, using a virtual host account, managed to deliberately expose some PHP scripts to it. From examining the infected scripts, what's disturbing is once infected, every tim...

01 Apr 2004 12:19pm GMT