Search by

mwebbers / laravel-code-commons

Michael Webbers

Shared Laravel/Livewire conventions: typed mixed-narrowing (Json), reusable stable sortable-table and pagination concerns, a lazy-table skeleton, and a SCOPE<->test coverage checker.

Package info

github.com/mwebbers/LaravelCodeCommons

pkg:composer/mwebbers/laravel-code-commons

Statistics

Installs: 1 009

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.0 2026-11-06 07:14 UTC

This package is auto-updated.

Last update: 2026-11-06 08:21:44 UTC


README

Shared Laravel/Livewire conventions, packaged once instead of hand-copied per project. These pieces used to live as near-identical copies across consuming apps and drifted; this package is the single canonical home.

Requires PHP 8.3+ and illuminate/support ^13.8 (Laravel 13). MIT-licensed.

Install

composer require mwebbers/laravel-code-commons

The package is on Packagist and a tagged release is picked up automatically, so no VCS repositories entry is needed — consumers that still carry one can drop it.

Versioning

From 1.0.0 this package follows semantic versioning: everything under src/ is public API, and breaking changes to it land only in a new major — preceded where practical by an @deprecated tag in the last minor. New features arrive in minors, fixes in patches. Consumers require ^1.0.

What's in it

Support\Json — typed mixed narrowing

The single sanctioned place a JSON/config() mixed becomes a real type, which is what lets a consuming app keep PHPStan at level 10 without scattered casts. Every accessor degrades to a default instead of crashing on a wrong-typed or missing value.

use Mwebbers\LaravelCodeCommons\Support\Json;

Json::str($node['title'] ?? null, '(untitled)');   // string, scalars stringified
Json::int($config['limit'] ?? null, 50);           // int, numeric strings coerced
Json::rows($response->json());                      // list<array> — non-array rows dropped

Support\DescriptionKey — the domain a WeFact description is for

One reading of a subscription's key for every app that needs it (Pulse measures the product, Chord matches the money). The key sits right after the label on the first line, or, in the older form, between its last parentheses; several are separated by commas, and a list with one part that is no hostname gives nothing.

use Mwebbers\LaravelCodeCommons\Support\DescriptionKey;

DescriptionKey::keys('<b>WordPress onderhoud:</b> a.nl, b.a.nl');  // ['a.nl', 'b.a.nl']
DescriptionKey::key('Webhosting: Standaard (Shop.Example.NL)');     // 'shop.example.nl' — one key or null
DescriptionKey::normalize('Bücher.example.');                       // 'xn--bcher-kva.example' (needs ext-intl)

Livewire\Concerns\WithCollectionSorting — a stable sortable-table concern

Click-to-sort for a Livewire table over an in-memory collection. Owns only the sort state and the toggle; the host supplies a column => extractor map. The sort is stable in both directions — equal-key rows keep their natural order whether ascending or descending (it uses sortByDesc, never a reverse() of the ascending sort, which would flip ties). Uses only Illuminate collections, so the logic is testable without a Livewire runtime.

use Mwebbers\LaravelCodeCommons\Livewire\Concerns\WithCollectionSorting;

class Tickets extends Component
{
    use WithCollectionSorting;

    public function render()
    {
        $rows = $this->sortedBy($this->tickets(), [
            'title'    => fn ($t) => $t->title,
            'deadline' => fn ($t) => $t->deadline?->getTimestamp() ?? PHP_INT_MAX,
        ]);
        // ...
    }
}

Livewire\Concerns\WithCollectionPagination — paginate an in-memory collection

Composes the sort concern with Livewire's pagination: it paginates an already-derived collection at perPage() rows, renders one page at a time, and resets to page 1 when the sort changes. An out-of-range ?page= clamps to the last real page — the URL is user-controlled input, and the data exists (just not 99 pages of it), so an empty table would be misleading. For a query builder use Livewire's native ->paginate(); this is for a set you already hold in memory.

Livewire\Concerns\LazyTableSkeleton — a #[Lazy] table's placeholder

Pairs with #[Lazy] to render a skeleton while the table body hydrates in a second request. It uses the page's real column headers so the skeleton auto-sizes like the live table (no layout shift). The placeholder view is app-provided — ship a livewire.placeholders.table Blade view (it carries your design system, e.g. Flux), or override skeletonView() to point at your own; a host overrides skeletonColumns() / skeletonRows() to match its table. A skeletonColumns() entry is either a bare label or a 'label' => alignment pair (start/center/end), so a column the live table end-aligns starts where it will end up — the view normalizes both forms. One caveat: a purely numeric label ('2024') cannot carry an alignment pair, because PHP coerces such array keys to int.

Testing\ScopeCoverage — the SCOPE↔test traceability checker

A coverage gate, extracted so every consuming app runs the same checker instead of a hand-copied ScopeCoverageTest. It reads the feature IDs from a SCOPE.md ## Features section and the #[Group('F-00X')] references across a tests directory, and reports either side's gaps.

An app's ScopeCoverageTest becomes a thin wrapper:

use Mwebbers\LaravelCodeCommons\Testing\ScopeCoverage;
use PHPUnit\Framework\TestCase;

final class ScopeCoverageTest extends TestCase
{
    public function test_scope_and_tests_stay_in_sync(): void
    {
        $coverage = new ScopeCoverage(dirname(__DIR__, 2).'/SCOPE.md', dirname(__DIR__, 2).'/tests');
        $result = $coverage->mismatch(__FILE__);

        $this->assertSame([], $result['missing'], 'SCOPE features with no test: '.implode(', ', $result['missing']));
        $this->assertSame([], $result['stale'], 'Test groups for unknown features: '.implode(', ', $result['stale']));
    }
}

Roadmap

Planned for later minors: a DuskTestCase base for browser smoke tests. See CHANGELOG.md.

Development

composer install
composer lint      # pint --test
composer analyse   # phpstan level 10
composer test      # phpunit