Skip to content

Index Filtering

Total CMS provides a powerful IndexFilter service for filtering index objects based on include/exclude criteria. This filtering system is used throughout the CMS for sitemaps, RSS feeds, API endpoints, and custom implementations.

The IndexFilter service provides a flexible way to filter collection objects using simple property-based criteria:

  • Include filters - Object must match ALL specified criteria
  • Exclude filters - Object is excluded if it matches ANY criteria
  • Boolean & String values - Automatic type conversion
  • Shorthand syntax - Property name defaults to true

Include only objects where specified properties match ALL values:

include=property:value # Single include filter
include=property1:value1,property2:value2 # Multiple include filters (AND logic)
include=property # Shorthand for property:true

Logic: ALL conditions must be true for the object to be included.

Exclude objects where specified properties match ANY value:

exclude=property:value # Single exclusion
exclude=property1:value1,property2:value2 # Multiple exclusions (OR logic)
exclude=property # Shorthand for property:true

Logic: If ANY condition matches, the object is excluded.

Exclude takes precedence over include. If an object matches an exclude filter, it will be removed even if it also matches an include filter.

// Object: ['published' => true, 'draft' => true]
// Filters: include=published:true, exclude=draft:true
// Result: EXCLUDED (draft:true matches exclude)

The system automatically converts common values:

ValueTypeComparisonExample
trueBooleanStrict (===)published:true
falseBooleanStrict (===)draft:false
OtherStringCase-insensitivestatus:active

Comparison behavior:

  • Boolean values - Fast strict comparison for optimal performance
  • String values - Case-insensitive matching for flexibility
  • Array fields - Case-insensitive search within array
?include=featured:true # Boolean true (strict match)
?include=status:published # String "published" (matches "Published", "PUBLISHED", etc.)
?exclude=draft:false # Boolean false (strict match)
?exclude=category:news # String "news" (matches "News", "NEWS", etc.)

When a field contains an array, the filter checks if the value exists within the array. String comparisons are case-insensitive for better usability. This is particularly useful for fields like tags, categories, or any multi-value properties.

Example with tags:

?include=tags:travel # Matches if "travel" is in the tags array (case-insensitive)
?exclude=tags:archived # Excludes if "archived" is in the tags array (case-insensitive)

Sample data:

['id' => '1', 'tags' => ['Travel', 'Adventure', 'Europe']]

Filter behavior (case-insensitive):

  • include=tags:travel → ✓ Matches (“travel” matches “Travel”)
  • include=tags:ADVENTURE → ✓ Matches (“ADVENTURE” matches “Adventure”)
  • include=tags:food → ✗ No match (food not in array)
  • exclude=tags:archived → ✓ Included (archived not in array)
  • exclude=tags:europe → ✗ Excluded (“europe” matches “Europe”)

Combined with scalar fields:

// Include published posts with "travel" tag
?include=published:true,tags:travel
// Exclude drafts or posts with "archived" tag
?exclude=draft:true,tags:archived

When no value is provided, the property defaults to :true:

?include=featured # Same as ?include=featured:true
?exclude=draft # Same as ?exclude=draft:true
?include=published # Same as ?include=published:true

IndexFilter is available via dependency injection — it requires both an IndexReader and ObjectFilter, so let the DI container handle construction:

use TotalCMS\Domain\Index\Service\IndexFilter;
// Inject via constructor
public function __construct(private IndexFilter $filter) {}
// Fetch and filter in one call
$objects = $this->filter->fetchFilteredIndex('blog', [
'include' => 'published:true',
'exclude' => 'draft:true'
]);
// Returns IndexData with filtered objects
$indexData = $filter->fetchFilteredIndexData('blog', [
'include' => 'featured:true',
'exclude' => 'archived:true'
]);
// Access filtered objects
foreach ($indexData->objects as $object) {
// Process filtered objects
}
// If you already have objects
$objects = [
['id' => '1', 'published' => true, 'draft' => false],
['id' => '2', 'published' => true, 'draft' => true],
['id' => '3', 'published' => false, 'draft' => false],
];
$filtered = $filter->filterObjects($objects, [
'include' => 'published:true',
'exclude' => 'draft:true'
]);
// Result: Only object '1'
$object = ['published' => true, 'featured' => true];
$matches = $filter->matchesFilter($object, [
'include' => 'published:true,featured:true'
]);
// Result: true (matches all criteria)

URL Parameters:

?exclude=draft # Exclude draft posts
?include=featured # Only featured posts
?include=published:true # Only published posts
?include=published&exclude=draft,private # Published, not draft or private

PHP Code:

$posts = $filter->fetchFilteredIndex('blog', [
'include' => 'published:true,featured:true',
'exclude' => 'draft:true'
]);

URL Parameters:

?exclude=discontinued # Exclude discontinued products
?include=instock:true # Only in-stock products
?include=category:electronics,featured # Electronics category + featured
?include=instock&exclude=discontinued # In stock, not discontinued

PHP Code:

$products = $filter->fetchFilteredIndex('products', [
'include' => 'instock:true,category:electronics',
'exclude' => 'discontinued:true'
]);

URL Parameters:

?exclude=cancelled # Exclude cancelled events
?include=status:upcoming # Only upcoming events
?include=featured&exclude=soldout # Featured, not sold out

PHP Code:

$events = $filter->fetchFilteredIndex('events', [
'include' => 'status:upcoming',
'exclude' => 'cancelled:true,soldout:true'
]);

URL Parameters:

?include=published # Only published work
?exclude=private # Exclude private projects
?include=category:webdesign,featured # Web design + featured

PHP Code:

$portfolio = $filter->fetchFilteredIndex('portfolio', [
'include' => 'published:true,category:webdesign',
'exclude' => 'private:true'
]);

URL Parameters:

?include=tags:travel # Posts with "travel" tag
?exclude=tags:archived # Exclude posts with "archived" tag
?include=tags:featured,published:true # Featured tag + published

PHP Code:

// Posts with "travel" tag that aren't drafts
$posts = $filter->fetchFilteredIndex('blog', [
'include' => 'tags:travel',
'exclude' => 'draft:true'
]);
// Products in "electronics" category that are in stock
$products = $filter->fetchFilteredIndex('products', [
'include' => 'categories:electronics,instock:true'
]);
// Events with "featured" tag, excluding cancelled
$events = $filter->fetchFilteredIndex('events', [
'include' => 'tags:featured',
'exclude' => 'tags:cancelled,tags:soldout'
]);

All conditions must match:

// Object must be published AND featured AND in electronics category
$objects = $filter->fetchFilteredIndex('products', [
'include' => 'published:true,featured:true,category:electronics'
]);

If ANY condition matches, object is excluded:

// Exclude if draft OR archived OR deleted
$objects = $filter->fetchFilteredIndex('blog', [
'exclude' => 'draft:true,archived:true,deleted:true'
]);
// Must be published AND featured
// AND NOT draft AND NOT private
$objects = $filter->fetchFilteredIndex('blog', [
'include' => 'published:true,featured:true',
'exclude' => 'draft:true,private:true'
]);
// Match specific string values
$objects = $filter->fetchFilteredIndex('products', [
'include' => 'status:active,category:electronics',
'exclude' => 'vendor:discontinued'
]);
// Include only objects where draft is explicitly false
$objects = $filter->fetchFilteredIndex('blog', [
'include' => 'draft:false'
]);
$options = [
'include' => 'published:true',
'exclude' => 'draft:true',
'limit' => 10,
'offset' => 0
];
$filterOptions = $filter->extractFilterOptions($options);
// Returns: ['include' => 'published:true', 'exclude' => 'draft:true']
// Note: limit and offset remain in $options
$parsed = $filter->parseFilterString('published:true,featured:true,status:active');
// Returns:
// [
// ['field' => 'published', 'value' => true],
// ['field' => 'featured', 'value' => true],
// ['field' => 'status', 'value' => 'active']
// ]

The IndexFilter service is used throughout Total CMS:

  • Sitemap Builder - Filter which objects appear in XML sitemaps (Sitemap Documentation)
  • Collection Index API - Filter collection objects via API endpoint
  • RSS Feeds - Control feed content based on object properties
  • Data Export - Filter which objects are included in JSON and CSV exports (Export Documentation)
  • Form Fields - Filter relational dropdown options (Field Settings)
  • Grid Display - Filter objects in Twig templates
  • Custom Services - Build your own filtered collections

The collection index API endpoint supports filtering via URL parameters:

GET /collections/{collection}/index?include=published:true&exclude=draft:true

Examples:

Terminal window
# Get all published blog posts
GET /collections/blog/index?include=published:true
# Get featured products that are in stock
GET /collections/products/index?include=featured:true,instock:true
# Get events excluding cancelled
GET /collections/events/index?exclude=cancelled:true
# Get blog posts with "travel" tag
GET /collections/blog/index?include=tags:travel
# Complex filtering: published posts with travel tag, excluding drafts
GET /collections/blog/index?include=published:true,tags:travel&exclude=draft:true

The API returns a filtered IndexData object with only matching objects.

// Good - specific criteria
'include' => 'published:true,status:active'
// Less specific
'include' => 'published'

Exclude filters run first for better performance:

// Efficient - excluded objects skip include check
'exclude' => 'deleted:true,archived:true'
'include' => 'published:true'

Use shorthand for boolean true values:

// Concise
'include' => 'published,featured'
// Verbose but equivalent
'include' => 'published:true,featured:true'

When using filters in code, document the business logic:

// Only show active, non-discontinued products to customers
$products = $filter->fetchFilteredIndex('products', [
'include' => 'active:true,instock:true',
'exclude' => 'discontinued:true'
]);
Object: {published: true, featured: true}
Filter: include=published:true,featured:true
Result: ✓ INCLUDED (both match)
Object: {published: true, featured: false}
Filter: include=published:true,featured:true
Result: ✗ EXCLUDED (featured doesn't match)
Object: {draft: true, private: false}
Filter: exclude=draft:true,private:true
Result: ✗ EXCLUDED (draft matches)
Object: {draft: false, private: false}
Filter: exclude=draft:true,private:true
Result: ✓ INCLUDED (neither match)
Object: {published: true, draft: true}
Filter: include=published:true, exclude=draft:true
Result: ✗ EXCLUDED (exclude takes precedence)

If filtering returns no results:

  1. Check field names - Ensure properties exist in your objects
  2. Check values - Boolean vs string comparison
  3. Check logic - Remember include=AND, exclude=OR
  4. Check data - Verify objects have expected values
// Debug: Check filter options
$filterOptions = $filter->extractFilterOptions($options);
var_dump($filterOptions);
// Debug: Check parsed filters
$parsed = $filter->parseFilterString($options['include']);
var_dump($parsed);

If filtering returns unexpected objects:

  1. Exclude precedence - Exclude runs before include
  2. Missing fields - Objects without the field won’t match include filters
  3. Type mismatches - ‘true’ (string) vs true (boolean)
// Check single object
$matches = $filter->matchesFilter($object, $filterOptions);
var_dump($matches); // true or false