graham-campbell/core
Core functionality for Laravel and PHP packages by Graham Campbell. Provides reusable base service providers, helpers, and bootstrapping utilities that other packages build on, keeping setup consistent, minimal, and lightweight across the ecosystem.
graham-campbell/core is not a feature package — it is an abstract base class you extend. Day-to-day, you never call it directly; you either inherit from it (in your own internal package) or you inherit through it (via a dependent like Flysystem or Security). The first step is always to establish which situation you are in.
composer why graham-campbell/core
composer show graham-campbell/core
vendor/graham-campbell/core/src/; skim the abstract provider and move on.AbstractServiceProvider. You do not remove it here; you remove it by upgrading or replacing that dependent.grep -rn "GrahamCampbell\\\\Core" app/ config/ src/ packages/
Where to look first. Do not start with the abstract class. Start with the concrete provider that says extends AbstractServiceProvider — either yours or the one inside the dependent package. That subclass tells you which protected properties ($config, $views, $lang, $routes, $migrations, $commands, $bindings, $singletons, $aliases) are actually in play. The abstract class is reference material; the subclass is the behavior.
Your first real use case is standing up a small internal package with the ritual already done for you. With this package in place, a complete package provider looks like:
namespace Acme\Reports;
use GrahamCampbell\Core\AbstractServiceProvider;
class ReportsServiceProvider extends AbstractServiceProvider
{
protected $config = true;
protected $views = true;
protected $lang = true;
protected $migrations = true;
protected $bindings = [
Contracts\ReportRepository::class => Repositories\EloquentReportRepository::class,
];
protected $singletons = [
ReportManager::class,
];
protected $aliases = [
Facades\Reports::class => 'reports',
];
}
Register it in config/app.php (pre-5.5) or via composer.json extra.laravel.providers (5.5+), then run php artisan vendor:publish --tag=reports and the config, views, translations, and migrations are in place. That is the entire loop you do every day: declare the property, publish, use.
The mental model is one line: declare what your package needs as protected properties; the abstract register()/boot() walk them and do the framework calls. Every pattern below is that model applied.
Pattern 1 — Config is a register() concern; everything else is a boot() concern. This is the single most important rule and the one most often broken. mergeConfigFrom must run in register() so the config exists by the time other providers boot; views, translations, migrations, routes, and commands belong in boot(). If you write your own replacement or a subclass that overrides one of these, honor the split.
Pattern 2 — Let publishing drive your dev loop. Use namespaced tags (reports-config, reports-migrations) rather than the sugar default, so vendor:publish --tag=config does not fire every package's config at once:
php artisan vendor:publish --tag=reports-config --force
--force after you edit the package config during development. Without namespacing, one broad tag publishes everything and you end up diffing noise.
Pattern 3 — Aliases are for facades only. $aliases maps a facade class to an alias string, and the package registers it via class_alias. Do not use it for domain value objects or interfaces — those are $bindings/$singletons. If you need a helper class alias outside the container, do it deliberately and document it; implicit aliasing is how autoload surprises happen.
Pattern 4 — Auto-discovery replaces registration, not ordering. For a new package, forget config/app.php and put this in composer.json:
"extra": {
"laravel": { "providers": ["Acme\\Reports\\ReportsServiceProvider"] }
}
But note: discovery changes how the provider is registered, not when mergeConfigFrom runs. Keep config merging in register() — this package does, and hand-rolled replacements frequently regress it by moving it to boot(). The symptom is "config missing in a service constructor" at startup.
Pattern 5 — Reproduce the value in ~30 lines for greenfield. If you are starting fresh and are tempted to pull this package in for the sugar alone, don't. Write this under your own namespace instead:
trait BootsPackage
{
protected function bootPackage(string $name, string $path): void
{
$this->mergeConfigFrom("$path/config/$name.php", $name);
$this->loadViewsFrom("$path/resources/views", $name);
$this->loadTranslationsFrom("$path/resources/lang", $name);
$this->publishes(["$path/config/$name.php" => config_path("$name.php")], "$name-config");
}
}
That is the whole product, expressed in native Illuminate\Support\ServiceProvider calls that survive framework upgrades.
Pattern 6 — Subclass the dependent provider for a targeted stopgap. When a dependent must stay on the base class and one hook misbehaves, extend the dependent provider in your namespace, override only the offending hook, and register your subclass instead:
namespace App\Providers;
use GrahamCampbell\Flysystem\FlysystemServiceProvider;
class AppFlysystemProvider extends FlysystemServiceProvider
{
protected function boot()
{
parent::boot();
// targeted fix only; leave a TODO tying this to the migration ticket
}
}
This buys time; it is not a solution.
Workflow tip — keep the two workstreams separate. If composer why says transitive, your ticket is "bump/replace the dependent." If it says direct, your ticket is "edit code in our repo." Different reviewers, different tests, different rollback plans. Conflating them doubles the estimate.
Workflow tip — snapshot the lock file (cp composer.lock composer.lock.bak) before touching any constraint. Abandoned packages resolve cleanly until a transitive dep floats, then fail hard; the backup lets you bisect.
PHP, not Laravel, is the hard blocker. The package targets Laravel 5.0–5.8 and PHP 5.5–7.x. Pinning illuminate/* to a 5.x branch will not help if CI runs PHP 8.x — Composer refuses at install time. Reproduce legacy environments in a container (docker run --rm -it php:7.1-cli) rather than fighting your local toolchain.
Config merge ordering is the most common regression. mergeConfigFrom in boot() instead of register() produces intermittent "missing config" errors that only surface in specific provider orderings. When debugging, check the registration phase first.
Abstract hooks hide in subclasses. Before you delete an extends, grep the concrete provider for overridden lifecycle methods — those are behavior you must reproduce explicitly, not just the property values:
grep -nE "function (register|boot|publishes|getConfig)" path/to/Provider.php
Publish tags collide. The sugar uses the package name as the tag. Two packages both tagging config means vendor:publish --tag=config fires both. Namespace your tags (--tag=reports-config, --tag=reports-migrations) and audit existing ones when you inherit a codebase.
Move storage linking out of boot(). The package ships a storage-link helper, and its changelog shows the fix itself was fixed once (v5.3.2 — "Actually fixed broken storage linking"). Provider-time filesystem side effects are hard to test and hard to roll back on a fresh environment. Run php artisan storage:link from your deploy script instead.
Debugging boot failures is asymmetric. Failures surface inside the abstract class with no frame pointing at your package. Temporarily wrap the parent call to turn a silent failure into a named one:
protected function boot()
{
try { parent::boot(); }
catch (\Throwable $e) { dd($e); } // remove before merging
}
Zero dependents means zero answers. No issue history with your stack trace, no maintainer to ping. When you hit a bug, read the source (it's short), reproduce in a scratch 5.x app, and write the finding down. Do not budget "waiting on upstream" — it is archived.
Do not fork to "keep it alive." The code is small enough to inline and small enough to rewrite that a fork is strictly worse than both: you inherit maintenance, CVE triage, and the false comfort of upstream in exchange for saving a few hundred lines. The two valid endpoints are delete and vendor thirty lines under your own namespace.
Bundle removal with a framework upgrade. Removal folded into a Laravel upgrade you are already doing is nearly free. Removal as a standalone change incurs a review cycle and a regression window for a package that ships no user-visible behavior. Never open a dedicated "remove graham-campbell/core" ticket.
Treat it as a code-review anti-pattern, not a dependency. It demonstrates the pre-auto-discovery convention layer. Reference it when reviewing new internal packages that try to recreate it, and steer those to native mergeConfigFrom / loadViewsFrom / loadTranslationsFrom / loadMigrationsFrom / loadRoutesFrom plus extra.laravel.providers.
How can I help you explore Laravel packages today?