Categorization
Overview
Depending on your needs you may want to have a single category tree, or more than one. As an example some shops prefer to have "brands" as a hierarchical tree besides the usual "category" classification. Some other shops use brand as a product attribute. Both solutions can be good depending on the unique shop's needs.
Another example is a wine shop that classifies wines based on region and on type.
Taken from other ecommerce systems, separate category trees are called Taxonomies and their child branches are called Taxons.
Example 1:
Category <- Taxonomy
│
├─> Men <- Taxon
│ └> T-shirts <- Taxon
│ └> Jeans <- Taxon
└─> Women <- Taxon
└> Skirts <- Taxon
└> Accessories <- Taxon
Example 2:
Region <- Taxonomy
│
├─> France <- Taxon
│ └> Bordeaux <- Taxon
│ └> Côtes du Rhone <- Taxon
└─> Italy <- Taxon
└> Veneto <- Taxon
└> Tuscany <- Taxon
└> Piedmont <- Taxon
Example 3:
Type <- Taxonomy
│
├─> Red <- Taxon
│ └> Cabernet Franc <- Taxon
│ └> Merlot <- Taxon
│ └> Porto <- Taxon
├─> White <- Taxon
│ └> Muscat Ottonel <- Taxon
│ └> Tokaji <- Taxon
│ └> Furmint <- Taxon
└─> Rosé <- Taxon
└> Cabernet Franc <- Taxon
└> Cuvée <- Taxon
Creating Taxonomies
Taxonomies basically have two properties: name and slug. The slug must be unique and gets autogenerated (URL-ified) from the name if it doesn't explicitly get specified.
use Vanilo\Category\Models\Taxonomy;
$category = Taxonomy::create(['name' => 'Wine Regions']);
echo $category->name;
// Wine Regions
echo $category->slug;
// wine-regions
The Taxonomy and the Taxon models use the Eloquent Sluggable package.
If you explicitly set the slug, no autogeneration will take place:
use Vanilo\Category\Models\Taxonomy;
$category = Taxonomy::create(['name' => 'Wine Regions', 'slug' => 'regions']);
echo $category->slug;
// regions
In case a slug already exists, the slug will be automatically extended to prevent duplicates:
use Vanilo\Category\Models\Taxonomy;
$category1 = Taxonomy::create(['name' => 'Category']);
$category2 = Taxonomy::create(['name' => 'Category']);
echo $category1->slug;
// category
echo $category2->slug;
// category-1
Finding Taxonomies
By Name
There's a dedicated finder method to retrieve a single taxonomy by name:
Taxonomy::create(['name' => 'Brands']);
$brands = Taxonomy::findOneByName('Brands');
By Slug
Added in v2.1
There's a dedicated static method to retrieve a single taxonomy by slug:
Taxonomy::create(['name' => 'Gift Ideas', 'slug' => 'gift-ideas']);
$giftIdeas = Taxonomy::findOneBySlug('gift-ideas');
Creating Taxons
Taxons are the actual category entries like "Smartphones" or "Riesling", etc. Every Taxon must belong to a Taxonomy and must have a name.
use Vanilo\Category\Models\Taxonomy;
use Vanilo\Category\Models\Taxon;
$category = Taxonomy::create(['name' => 'Category']);
$smartphones = Taxon::create([
'taxonomy_id' => $category->id,
'name' => 'Smartphones'
]);
// You can also use the setTaxonomy method:
$smartphones->setTaxonomy($category);
$smartphones->save();
To retrieve the taxonomy the taxon belongs to, use the taxonomy
property:
$category = Taxonomy::create(['name' => 'Category']);
$taxon = new Taxon();
$taxon->setTaxonomy($category);
echo get_class($taxon->taxonomy);
// Vanilo\Category\Models\Taxonomy
echo $taxon->taxonomy->name;
// Category
Taxon Slug (URL)
Taxons also have a slug field to be used for their URLs, and work very similar to Taxonomies.
If no value is given for the slug
field, it gets autogenerated from
the value of the name field:
$category = Taxonomy::create(['name' => 'Category']);
$monitors = new Taxon();
$monitors->name = 'Monitors';
$monitors->setTaxonomy($category);
$monitors->save();
echo $monitors->slug;
// monitors
If you explicitly set the slug, no autogeneration will take place:
$taxon = Taxon::create([
'taxonomy_id' => Taxonomy::create(['name' => 'Wine Regions']),
'name' => 'Carcavelos DOC',
'slug' => 'carcavelos'
]);
echo $taxon->slug;
// carcavelos
Taxon slugs must be unique within the same taxonomy and level.
Example of same slug allowed:
wine-type <- Taxonomy Slug
│
├─> red <- Taxon Slug
│ └> cabernet-franc <- Taxon Slug, duplicate ✔
└─> rose <- Taxon Slug
└> cabernet-franc <- Taxon Slug, duplicate ✔
Example of same slug forbidden:
wine-type <- Taxonomy Slug
│
├─> red <- Taxon Slug
│ └> cabernet <- Taxon Slug, duplicate ❌
│ └> cabernet <- Taxon Slug, duplicate ❌
└─> rose <- Taxon Slug
└> cabernet <- Taxon Slug, duplicate ✔
Taxon Parents
Taxons can optionally have one parent taxon they belong under. Taxons that don't have a parent taxon are considered root level entries.
use Vanilo\Category\Models\Taxonomy;
use Vanilo\Category\Models\Taxon;
$category = Taxonomy::create(['name' => 'Category']);
$audio = Taxon::create([
'taxonomy_id' => $category->id,
'name' => 'Audio'
]);
$speakers = Taxon::create([
'taxonomy_id' => $category->id,
'parent_id' => $audio->id,
'name' => 'Speakers'
]);
echo get_class($speakers->parent);
// Vanilo\Category\Models\Taxon
echo $speakers->parent->name;
// Audio
Other than setting the parent_id
field directly, it is also possible to call the setter method:
$childTaxon->setParent($parentTaxon);
$childTaxon->save();
To dissociate the parent use:
$childTaxon->removeParent();
$childTaxon->save();
var_dump($childTaxon->parent_id);
// NULL
var_dump($childTaxon->parent);
// NULL
Taxon Children
Since taxons are a tree type of hierarchy, they can have multiple children.
The children
property returns a Collection of child taxons.
$category = Taxonomy::create(['name' => 'Category']);
$topLevelTaxon = Taxon::create([
'taxonomy_id' => $category->id,
'name' => 'Rigging'
]);
$childTaxon1 = Taxon::create([
'taxonomy_id' => $category->id,
'parent_id' => $topLevelTaxon->id,
'name' => 'Halyards'
]);
$childTaxon1 = Taxon::create([
'taxonomy_id' => $category->id,
'parent_id' => $topLevelTaxon->id,
'name' => 'Sheets'
]);
foreach ($topLevelTaxon->children as $child) {
echo "{$child->name}\n";
}
// Halyards
// Sheets
Taxon Level
Taxons can tell their level in the hierarchy.
Top level entries (without parent) are on level 0, their children are level 1, and so on.
$category = Taxonomy::create(['name' => 'Category']);
$audio = Taxon::create([
'taxonomy_id' => $category->id,
'name' => 'Audio'
]);
$speakers = Taxon::create([
'taxonomy_id' => $category->id,
'parent_id' => $audio->id,
'name' => 'Speakers'
]);
echo $audio->level;
// 0
var_dump($audio->isRootLevel());
// true
echo $speakers->level;
// 1
Neighbours
Neighbours are the taxons which are under a common parent (within the same taxonomy).
It is defined as a HasMany Eloquent relationship
thus available as a property ($taxon->neighbours
) which gives a collection.
Due to the internals of relationships, the relationship doesn't work for root level taxons (
parent_id === NULL
)
$books = Taxon::create(['name' => 'Books']);
Taxon::create(['name' => 'Sci-fi', 'parent_id' => $books->id]);
Taxon::create(['name' => 'Thriller', 'parent_id' => $books->id]);
$fantasy = Taxon::create(['name' => 'Fantasy', 'parent_id' => $books->id]);
$fantasy->neighbours;
// Sci-fi
// Thriller
// Fantasy
// Yes, it returns the caller itself as well (read below how to filter it)
It is also possible to invoke $taxon->neighbours()
as a method and further use it as query builder:
$taxon->neighbours()->get();
// To exclude the caller from the result use the `except` scope:
$taxon->neighbours()->except($taxon)->get();
// To get them in a reverse order
$taxon->neighbours()->sortReverse()->get();
Get First And Last Neighbours
Unlike the
neighbours
relationship, this works properly on root level taxons as well
There are two dedicated methods to retrieve the first or the last neighbour:
-
$taxon->lastNeighbour()
and -
$taxon->firstNeighbour()
The order of the taxons is based on the priority
field.
$gadgets = Taxon::create(['Gadgets']);
$laptops = Taxon::create(['name' => 'Laptops', 'priority' => 1, 'parent_id' => $gadgets->id]);
$watches = Taxon::create(['name' => 'Watches', 'priority' => 2, 'parent_id' => $gadgets->id]);
$phones = Taxon::create(['name' => 'Phones', 'priority' => 3, 'parent_id' => $gadgets->id]);
$tablets = Taxon::create(['name' => 'Tablets', 'priority' => 4, 'parent_id' => $gadgets->id]);
echo $phones->firstNeighbour()->name;
// Laptops
echo $phones->lastNeighbour()->name;
// Tablets
// It may return itself if that happens to be the case:
echo $laptops->firstNeighbour()->name;
// Laptops
echo $tablets->lastNeighbour()->name;
// Tablets
// To exclude itself from the result, set the `$excludeSelf` parameter of the method to true:
echo $laptops->firstNeighbour(true)->name;
// Watches
echo $tablets->lastNeighbour(true)->name;
// Phones
Retrieving Taxons (Scopes)
Easy Retrieval By Slug(s)
Added in v2.1
There is a static convenience method called Taxon::findOneByParentsAndSlug()
to locate a taxon
based on its and its parents' slug. These methods can be handy in Controller contexts where
you get a series of slugs as route parameters.
Retrieve a taxon by slug within a given taxonomy:
$brands = Taxonomy::create(['name' => 'Brands']);
Taxon::create(['name' => 'Klarna', 'taxonomy_id' => $brands->id]);
$klarna = Taxon::findOneByParentsAndSlug('brands', 'klarna');
echo $klarna->name;
// Klarna
Due to their tree-like nature, taxon slugs aren't unique. If you need to locate a specific Taxon
with a given parent, use the third (parentSlug
) parameter of the method:
$locations = Taxonomy::create(['name' => 'Locations']);
$tennessee = Taxon::create(['name' => 'Tennessee', 'taxonomy_id' => $locations->id]);
$egypt = Taxon::create(['name' => 'Egypt', 'taxonomy_id' => $locations->id]);
Taxon::create(['name' => 'Memphis', 'parent_id' => $tennessee->id, 'taxonomy_id' => $locations->id]);
Taxon::create(['name' => 'Memphis', 'parent_id' => $egypt->id, 'taxonomy_id' => $locations->id]);
$memphisEgypt = Taxon::findOneByParentsAndSlug('locations', 'memphis', 'egypt');
$memphisTennessee = Taxon::findOneByParentsAndSlug('locations', 'memphis', 'tennessee');
The default Taxon model that ships with this package defines a several Query Scopes.
Due to the nature of Eloquent query scopes, these are chainable so it is possible to combine them arbitrarily.
Retrieve By Taxonomy
To retrieve all taxons belonging to a taxonomy, use the byTaxonomy
scope:
$category = Taxonomy::findOneByName('Category');
// Returns a collection of taxons
$taxons = Taxon::byTaxonomy($category)->get();
// The method also works by passing the taxonomy id:
$id = $category->id;
$taxons = Taxon::byTaxonomy($id)->get();
Retrieve Root Level Taxons
An alternative to $taxonomy->rootLevelTaxons()
is to retrieve all the root level taxons:
// It returns all the taxons without parent, from all taxonomies:
$allRootLevelTaxons = Taxon::roots()->get();
// It is possible of course to combine with byTaxonomy scope:
$taxonomy = Taxonomy::findOneByName('Brands');
$rootTaxonsForBrands = Taxon::roots()->byTaxonomy($taxonomy)->get();
Sorting Taxons
Taxons have a field called priority
which is designed to make taxons sortable.
The sort()
and sortReverse()
query scopes sort results by priority:
$spirits = Taxonomy::create(['name' => 'Spirits']);
Taxon::create(['name' => 'Gin', 'priority' => 3, 'taxonomy_id' => $spirits->id]);
Taxon::create(['name' => 'Whisky', 'priority' => 1, 'taxonomy_id' => $spirits->id]);
Taxon::create(['name' => 'Armagnac', 'priority' => 2, 'taxonomy_id' => $spirits->id]);
foreach(Taxon::sort()->get() as $taxon) {
echo $taxon->name . "\n";
}
// Output:
// Whisky
// Armagnac
// Gin
// To get taxons in reverse order:
foreach(Taxon::sortReverse()->get() as $taxon) {
echo $taxon->name . "\n";
}
// Output:
// Gin
// Armagnac
// Whisky
Exclude One Taxon From The List
There are cases when you want to exclude a taxon from the list of taxons.
For that purpose you can utilize the except(Taxon $taxon)
scope:
$me = Taxon::create(['name' => 'Me']);
Taxon::create(['name' => 'You']);
Taxon::create(['name' => 'She']);
Taxon::create(['name' => 'We']);
Taxon::except($me)->get();
// You
// She
// We
Assign Taxons To Products
The goal of categorization is to define "things" to be categorized.
The most common use case is to arrange products in categories, and this is already configured
in the Framework (Foundation
classes), but not in standalone modules.
Think of possible use cases like categorizing customers, subscribers, etc.
The assignment is done with Eloquent Many To Many Polymorphic Relations.
This category module has prepared the model_taxons
table for this purpose and is ready to be used without
any further database change.
Assigning Taxons To A Product:
$product = Product::find(1);
$taxon1 = Taxon::find(1);
$taxon2 = Taxon::find(2);
// To assign a single taxon:
$product->addTaxon($taxon1);
//To assign multiple taxons:
$product->addTaxons([$taxon1, $taxon2]);
The Inverse: Add Products To Taxons
Another common use case is to retrieve all the products within a category (Taxon). This is also preconfigured in the Framework (but not in the standalone modules!).
To manipulate the products within a taxon:
$taxon = \App\Models\Taxon::find(1);
$product = Product::find(1);
// Add the product to the taxon
$taxon->addProduct($product);
// Note that it has exactly the same effect as this:
$product->addTaxon($taxon);
// To retrieve all the products within the taxon:
$taxon->products;
// Collection of Product objects
Assign Taxons To Models (Other Than Products)
Let's say you have a Subscriber
model and you want to put them in categories.
Here's how to define the relationship on the Subscriber model class:
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Vanilo\Category\Models\TaxonProxy;
class Subscriber extends Model
{
public function taxons(): MorphToMany
{
return $this->morphToMany(
TaxonProxy::modelClass(), 'model', 'model_taxons', 'model_id', 'taxon_id'
);
}
}
Assigning Taxons To A Subscriber:
$subscriber = Subscriber::find(1);
$taxon1 = Taxon::find(1);
$taxon2 = Taxon::find(2);
// To assign a single taxon:
$subscriber->taxons()->save($taxon1);
//To assign multiple taxons:
$subscriber->taxons()->saveMany([$taxon1, $taxon2]);
Removing Taxons From A Subscriber:
$subscriber = Subscriber::find(1);
$taxon = Taxon::find(1);
// To assign a single taxon:
$subscriber->taxons()->detach($taxon);
Defining The Inverse Of The Relationship
Another common use case is to retrieve all the models within a category (Taxon).
This way you'll be able to do this:
$taxon = Taxon::find(1);
// To return a collection of subscribers within the taxon:
$taxon->subscribers();
To do this you need to:
- Extend the Taxon model
- Define the (inverse) relationship
- Register the extended Taxon model
The extended Taxon model with the relationship:
namespace App;
class Taxon extends \Vanilo\Category\Models\Taxon
{
public function subscribers()
{
return $this->morphedByMany(
Subscriber::class, 'model', 'model_taxons', 'taxon_id', 'model_id'
);
}
}
To register the model:
// app/Providers/AppServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Vanilo\Category\Contracts\Taxon as TaxonContract;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
$this->app->concord->registerModel(
TaxonContract::class, \App\Models\Taxon::class
);
}
}
After this you can get and manipulate the subscribers within a taxon:
$taxon = \App\Models\Taxon::find(1);
$subscriber = Subscriber::find(1);
// Add the subscriber to the taxon
$taxon->subscribers()->save($subscriber);
// Note that it has exactly the same effect as
$subscriber->taxons()->save($taxon);
// To retrieve all the subscribers within the taxon:
$taxon->subscribers;
// Collection of Subscriber objects
Known Issues
Duplicate Taxon Slugs On Root Level
Uniqueness of taxon slugs within a taxonomy level is currently guaranteed by unique DB keys. Most contemporary DB engines allow NULLs in composite unique keys.
Therefore root level taxons can have duplicate slugs.
Neighbours Relationship On Root Level
The neighbours()
relationship does not work on root level taxons.
It returns an empty result set.