﻿---
title: "LindemannRock Base: different plugins, a shared foundation."
url: "https://lindemann.design/work/base/"
description: "A shared Yii module for the LindemannRock Craft CMS plugins, bringing common interfaces, configuration and development tools into one maintained package."
---

# LindemannRock Base: different plugins, _a shared foundation._

Search, redirects and translations solve different problems. The tools around them should still feel familiar. I built LindemannRock Base to give the plugin family shared Control Panel layouts, settings and supporting services. It is a Yii module installed through Composer, with each plugin building its own features on top.

-   [GitHub](https://github.com/LindemannRock/craft-plugin-base)
-   [Docs](https://github.com/LindemannRock/craft-plugin-base/tree/main/docs)

Shared foundation

-   A shared foundation
    
    ```text
    Base Layouts, settings, dates, exports and supporting services
    Search Manager Search indices, queries and results
    Redirect Manager Redirect rules and broken links
    Translation Manager Messages and their translations
    ```
    
    A map of responsibilities. Base supplies the common parts; each plugin owns its domain.
    
-   Dates that follow the site
    
    ```text
    // PHP: display a date and time
    DateFormatHelper::formatDatetime(
        $date,
        pluginHandle: 'my-plugin'
    );
    
    // Twig: use the same display helper
    {{ entry.dateCreated|lrDatetime }}
    ```
    
    PHP and Twig use the same formatting tools. Display preferences are resolved from the site and plugin configuration.
    
-   One export path
    
    ```text
    // In a plugin controller
    $filename = ExportHelper::filename(
        'activity', 'csv'
    );
    
    return ExportHelper::toCsv(
        $rows,
        ['Name', 'Created'],
        $filename,
        ['dateCreated']
    );
    ```
    
    The plugin supplies its records. Base handles the CSV response, filename conventions and date columns, with Excel and JSON helpers available too.
    
-   Shared status colours
    
    ```text
    // Resolve meaning to a shared palette
    $enabled = ColorHelper::getSetColor(
        'status', 'enabled'
    );
    
    $error = ColorHelper::getSetColor(
        'status', 'error'
    );
    ```
    
    Status colours come from named sets. Badges and filters can share those mappings, and plugins can register sets for their own domain.
    
-   Clear one cache family
    
    ```text
    $cache = new ScopedCache(
        cache: $decision->applicationCache,
        pluginHandle: 'my-plugin',
        family: 'statistics'
    );
    
    $cache->set('monthly', $statistics, 3600);
    
    // Invalidate this family, not the site
    $cache->invalidateFamily();
    ```
    
    Using the selected cache backend, the plugin can invalidate its statistics without flushing the site cache. Old values expire after their set lifetime.
    
-   One query, two databases
    
    ```text
    // Ask for a value inside a JSON column
    $source = DbHelper::jsonExtractExpression(
        'metadata',
        'source'
    );
    
    $query->addSelect([
        'source' => $source,
    ]);
    ```
    
    The helper handles the database-specific expression and column quoting. Plugins can describe the query without duplicating that SQL logic.
    
-   A common redirect rule
    
    ```text
    $target = $settings->notFoundRedirectUrl
        ?? '/';
    
    $url = UrlSafetyHelper::sanitizeRedirectUrl(
        $target,
        '/'
    );
    
    return $this->redirect($url);
    ```
    
    The shared rule accepts relative paths and HTTP or HTTPS destinations by default. Unsupported targets use the fallback; destination trust still belongs to the plugin.
    

Role

Design, architecture and engineering

Stack

-   Craft CMS 5.10+
-   PHP 8.2+
-   Yii module
-   Composer

Status

In use across the plugin family

1.  **Familiar** Shared patterns for tables, filters and settings.
2.  **Reusable** Common services maintained in one package.
3.  **Configurable** Site preferences carried across the family.

The problem

## Small differences become a maintenance problem.

A plugin needs more than its main feature. People need to find records, filter a table, understand a date, export data and change settings. Rebuilding those interactions for every plugin creates repeated work and room for subtle differences.

Those differences affect both sides of the product. A person managing a site has to learn another variation of a familiar task. A developer fixing a pagination problem or changing a date format has to find every implementation that needs the same change.

My work was to identify those shared responsibilities and give them a home. Base combines interface patterns with the services that make them behave consistently. It is part of the plugins' implementation, rather than another product for a site owner to configure separately through the Plugin Store.

The examples above show a selection of the shared APIs. The consuming plugin supplies the records, settings and runtime context. The wider module also covers the less visible work that keeps those interfaces dependable.

| Shared area | What the family gains |
| --- | --- |
| Control Panel patterns | Table, analytics, settings and utility layouts bring search, filters, pagination and bulk actions into familiar patterns. Teams can move between plugins without learning a new version of each interaction. |
| Visual language | ColorHelper and shared badge components map states to named colour sets. PluginThemeStyleHelper supplies branding variables for setup and documentation surfaces, keeping identity separate from the underlying layout. |
| Dates and ranges | DateFormatHelper handles timezone-aware formatting; DateRangeHelper supplies common ranges and query bounds. Display and export formats have explicit conventions, so each plugin does not have to invent its own date handling. |
| Imports and exports | CsvImportHelper parses uploads and detects delimiters. ExportHelper supplies CSV, JSON and Excel output, plus file bundles and spreadsheet formula-injection protection. Each plugin remains responsible for its records and domain validation. |
| Settings and editions | Shared traits cover persistence, config overrides, display names and edition checks. Matching settings controls show when configuration owns a value, making the source of a setting easier to understand. |
| Navigation and access | CpNavHelper builds permission-aware navigation; GqlHelper supports schema permission checks. Shared bootstrap registers translations and connects log permissions when Logging Library is installed. |
| URLs and content | UrlSafetyHelper checks redirect targets, ContentSafetyHelper detects executable markup in text, and shared validators cover route and storage paths. These give forms and imports common checks while leaving context-specific validation with the plugin. |
| Location and devices | GeoHelper, lookup and device-detection traits provide reusable location and user-agent handling. AnalyticsIpHelper centralises IP anonymisation, hashing and lookup eligibility, reducing repeated data-handling logic. |
| Cache storage | Storage selection and ScopedCache account for the host's capabilities, finite expiry and isolation between plugins. A plugin can invalidate its own disposable data without clearing the shared cache for everything else. |
| Background work | Scheduling helpers calculate recurring runs, manage ownership of pending jobs and support delayed work across queue backends. Plugins share the mechanics of scheduling while defining the work each job performs. |
| Database portability | DbHelper supplies expressions for operations that differ between MySQL and PostgreSQL. Column quoting and JSON extraction have one maintained implementation instead of separate fixes throughout the family. |
| Assets and QR codes | Asset-volume checks validate submitted assets and permissions. The QR renderer adapts to Craft's image driver, giving consumers a shared rendering path across supported server configurations. |
| Testing and diagnostics | The integration-test base provides fixtures, component restoration and cleanup. SQL dialect checks and shared console help make recurring quality and support work reusable too. |

Sharing infrastructure also concentrates responsibility. A correction to a common helper has one implementation to review, but consuming plugins still need integration checks and a compatible release. The benefit is a maintained contract across the family, rather than a promise that every change is automatically safe everywhere.

Interface

## A familiar way to work with different information.

The shared table layout provides search, filters, sorting, pagination, column visibility and bulk actions. A plugin supplies its records, labels and actions. The layout supplies the surrounding interaction.

This is already used in the source of Redirect Manager and Search Manager. Redirect rules and search indices have different meanings, but the basic task of finding and reviewing a record follows the same pattern.

Shared table layout

```twig
{% extends 'lindemannrock-base/_layouts/cp-table' %}

{# The plugin supplies tableConfig and its records. #}
{% block tableRow %}
  <td>{{ item.name }}</td>
  <td>{{ item.status }}</td>
{% endblock %}
```

This excerpt shows the division of responsibility. The full configuration also declares the columns, page URL, sorting and pagination. Keeping that context together lets the shared layout preserve filters and site context as someone moves through a table.

The same approach extends to analytics pages, settings and utilities. Shared components cover recurring details such as badges and date filters without dictating every screen a plugin can have.

Configuration

## A site preference should survive an export.

Consistency also depends on information. A table and its refreshed results should use the same timezone and display preferences. Downloads need formats suited to spreadsheets or APIs. Base centralises those conventions, the available export formats and the initial date range used by date-filtered views.

Site configuration

```php
// config/lindemannrock-base.php
return [
  'timeFormat' => '24',
  'monthFormat' => 'short',
  'dateOrder' => 'dmy',
  'dateSeparator' => '/',
  'exports' => [
      'excel' => true,
      'csv' => true,
      'json' => false,
  ],
  'defaultDateRange' => 'last30days',
];
```

These are example overrides from the configuration template. They let a team set its preferences once for the plugins using the shared helpers. Individual calls can still choose the formatting appropriate to their context.

Translations are registered through the common setup, while the messages remain with the plugin that owns them. Shared conventions support localisation without turning every product's language into a single, undifferentiated catalogue.

Engineering

## Make the common setup explicit.

A plugin registers Base during its own initialisation. The bootstrap helper loads the module, registers the plugin's Twig helper and translations, and can connect its log permissions to Logging Library when that library is installed.

Plugin bootstrap

```php
PluginHelper::bootstrap(
  $this,
  'redirectHelper',
  ['redirectManager:viewSystemLogs'],
  ['redirectManager:downloadSystemLogs']
);
```

The two packages have separate responsibilities. Base handles shared setup and integration; [Logging Library](/work/logging-library/) handles recording and reviewing logs. Base also provides database helpers, settings traits and cache utilities for recurring implementation work.

This makes the shared package an architectural responsibility. A change can benefit multiple plugins, but it can also affect them together. The module includes reusable testing helpers and SQL dialect checks, and its documentation explains how consumers should use the common patterns.

Result

## A maintained foundation beneath the product family.

The result is a working shared module used across the LindemannRock plugin family. Its value is visible in repeated interactions: familiar tables, common settings behaviour and the same formatting tools behind different products.

Centralising those parts gives improvements a clear place to live. It also leaves room for the differences that matter: how a search is configured, how a redirect is resolved or how a translation is reviewed remains the responsibility of its plugin.