artisanpack-ui/core
ArtisanPack UI Core unifies configuration from all ArtisanPack UI packages into a single config/artisanpack.php file. A scaffold command auto-detects and merges installed package settings while preserving your existing customizations.
The Core package is infrastructure: you will rarely call it directly, but you will notice it the moment you install a second ArtisanPack UI package and want its settings in one place instead of scattered across config/cms-framework.php, config/visual-editor.php, and so on.
Minimum steps
composer require artisanpack-ui/core
php artisan vendor:publish --tag=artisanpack-config
php artisan artisanpack:scaffold-config
Auto-discovery registers CoreServiceProvider; there is nothing to add to bootstrap/providers.php.
Where to look first
config/artisanpack.php — the single source of truth. After scaffolding you will see one top-level key per installed package ('cms-framework' => [...], 'visual-editor' => [...]).php artisan artisanpack:diagnose — your first health check. It validates PHP/Laravel/Livewire versions, required extensions, installed packages, configuration, service bindings (core, a11y), and artisan commands. Run it before you start debugging anything else.ArtisanPackUI\Core\Config\ConfigurationManager — the programmatic API behind the config file.First real use case: consolidating configuration
Install two or three sibling packages, publish, then scaffold. Existing keys you hand-edited are preserved; only missing keys are filled in. Commit the resulting config/artisanpack.php and delete the per-package config files you no longer need (only after confirming each package actually reads from the central file — see Gotchas).
Read settings with Laravel's normal helper, not a bespoke API:
$title = config('artisanpack.cms-framework.settings.site_title', 'Default Title');
$theme = config('artisanpack.ui-components.default_theme', 'light');
$all = config('artisanpack');
Package-scoped access via the manager
ConfigurationManager hides the artisanpack.<package>.<key> namespacing behind package-aware helpers, which is more readable than long dot-strings and gives you validation:
use ArtisanPackUI\Core\Config\ConfigurationManager;
public function __construct(private ConfigurationManager $config) {}
$this->config->get('cms-framework', 'settings.site_title', 'Default Title');
$this->config->set('visual-editor', 'autosave_interval', 300);
$this->config->has('ui-components', 'enable_animations');
$this->config->merge('ui-components', ['enable_animations' => true]);
$this->config->package('cms-framework'); // that package's whole config array
$this->config->all(); // everything under artisanpack
merge returns a ValidationResult (immutable outcome object), so it slots into guards:
$result = $this->config->merge('cms-framework', $incoming);
if ($result->fails()) { /* report $result->errors() */ }
For one-off reads in a Blade file or a quick script, use the artisanpack_config() helper and the ArtisanPackConfig facade instead of injecting the manager.
Declaring a schema so bad config fails loudly
Register a schema once (typically in a sibling package's service provider) and let the manager validate before settings are trusted:
$this->config->registerSchema('visual-editor', [
'autosave_interval' => ['type' => 'integer', 'required' => true],
'toolbar_style' => ['type' => 'string', 'in' => ['minimal', 'full']],
]);
$this->config->validate('visual-editor'); // -> ValidationResult
Shared logging with audit trail
Instead of each package inventing its own channel and prefix, route through the core logger. Logs get consistent [package] prefixes and structured context; audit() additionally dispatches AuditLogCreated for compliance-relevant actions you can listen for.
use ArtisanPackUI\Core\Facades\ArtisanPackLog;
ArtisanPackLog::info('Layout saved', ['layout_id' => $layout->id]);
ArtisanPackLog::audit('user.deleted', ['actor' => auth()->id(), 'target' => $user->id]);
Configure via artisanpack.core.logging.channel (falls back to Laravel's default) and artisanpack.core.logging.audit.enabled.
Blade directives without string templates
artisanpack_blade() gives intent-revealing methods and validates directive names at registration, so typos surface at boot rather than at render:
use function ArtisanPackUI\Core\artisanpack_blade;
artisanpack_blade()->escaped('money', fn ($expr) => "number_format({$expr}, 2)");
artisanpack_blade()->conditional('canEdit', fn ($expr) => "auth()->user()->can('edit', {$expr})");
Building a sibling package (the main reason to depend on Core)
Extend the abstract ArtisanPackServiceProvider instead of hand-rolling config merging, publish tags, command registration, view/route loading, and singletons. Declare properties, implement one hook:
class VisualEditorServiceProvider extends ArtisanPackServiceProvider
{
protected string $configFile = __DIR__.'/../config/visual-editor.php';
protected string $configKey = 'visual-editor';
protected string $configPublishTag = 'visual-editor-config';
protected array $singletons = [Editor::class => Editor::class];
protected function registerPackageBindings(): void
{
$this->app->bind(EditorContract::class, Editor::class);
}
protected function bootPackage(): void
{
// anything that doesn't map to a standard hook
}
}
Likewise extend ArtisanPackFacade for facades (override getPackageName() so missing-service exceptions name the package to install) and use the exception hierarchy (ArtisanPackException plus ConfigurationException, DependencyException, InstallationException, ServiceException, ValidationException) so consumers can catch broadly or narrowly.
Testing with ecosystem-aware assertions
use ArtisanPackUI\Core\Testing\ArtisanPackTestCase;
class VisualEditorTest extends ArtisanPackTestCase
{
use InteractsWithConfiguration;
public function test_autosave(): void
{
$this->withPackageConfig('visual-editor', ['autosave_interval' => 60]);
$this->assertPackageInstalled('artisanpack-ui/visual-editor');
$this->assertConfigEquals(60, 'artisanpack.visual-editor.autosave_interval');
$this->assertFacadeWorks(\ArtisanPackUI\Core\Facades\Core::class);
}
}
swapArtisanPackService() / mockArtisanPackService() and shortcuts for the configuration manager, logger factory, and Blade registrar save you from app()->instance() boilerplate.
CI pipeline patterns
php artisan artisanpack:diagnose --json
php artisan artisanpack:check-compatibility --strict --json
php artisan artisanpack:check-updates --minor-only --security-only
All three exit non-zero on failure/error, so they drop straight into a CI step. --fix on diagnose will invoke artisanpack:scaffold-config for you when the config check fails. Use --package=<id> to scope a diagnosis to one package when the whole ecosystem report is too noisy.
Scaffolding a brand-new package — php artisan artisanpack:make-package vendor/name --with-livewire --with-views --with-routes --with-migrations --with-commands produces the standard blueprint (provider extending the base class, facade, config stub, Pest + Testbench). Prefer this over copying an existing repo so you inherit the current conventions.
--force is destructive-by-design. Without it, artisanpack:scaffold-config preserves your existing keys — that's the desired default. With it, existing values are overwritten with package defaults. Never put --force in a deploy script; run it locally and commit the diff so you can review what changed.php artisan config:clear (or config:cache in the deploy step). A stale cached config file is the number one reason "my new setting isn't picked up."artisanpack.php harmlessly — clean it up manually so future readers aren't misled.config/<package>.php. If a setting stops taking effect after removing the old file, restore it and check the sibling's provider.UPGRADE.md first.a11y() family moved out in 1.1.0. a11y(), a11yCSSVarBlackOrWhite(), a11yGetContrastColor(), a11yCheckContrastColor(), and generateAccessibleTextColor() now live in artisanpack-ui/accessibility. Calls failing with "undefined function" after an upgrade are this, not a bug.--force on make-package only protects you from yourself. The command validates vendor/package format up front and refuses non-empty directories unless you pass --force. Prefer creating a fresh directory over forcing.check-updates hits Packagist. It reads composer.lock and queries once per installed package, cached for artisanpack.core.updates.cache_ttl (default 3600s). In air-gapped CI, skip this command — it will not fail gracefully on network errors.check-updates flags releases containing words like security, vulnerability, cve-, xss, csrf. Treat --security-only as a filter for reading, not as a substitute for composer audit.registerSchema(), config keys, and event listeners (AuditLogCreated). For packages: registerPackageBindings() / bootPackage() / the $singletons, $viewsPath, $routesPath properties. If you find yourself editing a Core class, you're off the supported path.ArtisanPackException as the ecosystem-wide net. Catching ServiceException specifically is useful when you want to detect a sibling package's service is missing and degrade gracefully (app()->bound() / the facade's availability probes) rather than 500.artisanpack:diagnose --package=<id> --json → check config('artisanpack.<package>') with dd() → php artisan config:clear → inspect the sibling's registered schema. That sequence resolves most "setting ignored" and "service not bound" reports.assertPackageInstalled, assertCommandExists) over assuming sibling packages track Core's API changes in lockstep — 1.2 was careful to be additive, but that's a policy, not a guarantee.How can I help you explore Laravel packages today?