kjonski/how-to-bundle
A Laravel/PHP package for bundling reusable how-to content and step-by-step guides into your application, making instructions easier to organize, publish, and maintain. Simple to integrate and lightweight.
kjonski/how-to-bundle is not a Composer-installable library — it's a tutorial repository that walks through preparing a reusable Symfony bundle, step by step. There is no composer require step; you consume it by cloning it and reading it.
git clone https://github.com/kjonski/how-to-bundle
cd how-to-bundle
git log --oneline --reverse # if history is present, this is the actual tutorial
Look first at, in this order:
composer.json — note "type": "symfony-bundle", the PSR-4 autoload mapping, and any extra block. This is the contract that makes a bundle installable.SomethingBundle.php) at the namespace root — this is what the Symfony kernel registers.DependencyInjection/ — the Extension subclass and the Configuration tree. This is where 90% of real-world bundle bugs live.First use case: you have application code in a Laravel or Symfony app that you want to extract into something reusable. Use this repo as a checklist for the shape of that extraction: entry class → DI extension → configuration schema → services definition → tests.
Since you're a Laravel developer, be explicit about the mismatch: this repo teaches Symfony bundle mechanics. In Laravel the analogue is a package with a
ServiceProvider,mergeConfigFrom(),publishes(), andextra.laravel.providersfor auto-discovery. Read the Symfony version for the concepts (config schema, prepend/merge semantics, compile-time wiring), then translate.
Follow the commits, not the files. Tutorial repos are most useful as a diff sequence. Check out each commit and read the change in isolation; that's the "step-by-step" the description promises.
The skeleton to replicate (Symfony side):
src/
AcmeWidgetBundle.php # extends Bundle
DependencyInjection/
AcmeWidgetExtension.php # extends Extension, loads services
Configuration.php # TreeBuilder root = 'acme_widget'
Resources/config/services.yaml
Key wiring details that make it work:
AcmeWidgetExtension::load() receives $configs and must call $this->processConfiguration(new Configuration(), $configs).AcmeWidgetExtension → acme_widget. Mismatch here produces the classic "There is no extension able to load the configuration for …" error.Bundle::getContainerExtension() reflects on the class name to find the extension; overriding it is only needed for non-standard naming.$container->prependExtensionConfig() in Bundle::build() or in the extension's prepend() is how you inject sane defaults without stomping on the app's own config.Composer metadata is part of the tutorial and worth copying verbatim:
{
"type": "symfony-bundle",
"autoload": { "psr-4": { "Acme\\WidgetBundle\\": "src/" } },
"extra": { "branch-alias": { "dev-main": "1.0-dev" } }
}
Without type: symfony-bundle, Flex won't offer a recipe and tooling won't treat it as a bundle.
Integration tips for a Laravel developer translating this:
| Symfony bundle concept | Laravel equivalent |
|---|---|
Extension::load() |
ServiceProvider::register() |
Configuration tree |
config/widget.php + mergeConfigFrom() |
Resources/config/services.yaml |
container bindings / singletons array |
prependExtensionConfig() |
mergeConfigFrom() + $this->app->scoped() defaults |
| Bundle auto-registration | extra.laravel.providers |
| Flex recipe | php artisan vendor:publish |
Workflow that actually pays off: scaffold the bundle in a packages/acme/widget directory, add it to the host app's repositories as a path (Composer) or symlink (Laravel), and iterate with a live autoload rather than tagging releases. Only composer tag once the config schema and public service IDs are stable — those are your BC surface.
Test it from a host app, not in isolation. A bundle with no consumer is untested. At minimum, boot a kernel with the bundle registered and assert the container compiles and your service is reachable by its public alias. bin/console lint:container is the cheapest smoke test.
composer require kjonski/how-to-bundle. It's a tutorial, not a dependency. If you want to fork it as a starting point, fork it and change the namespace, package name, and license headers — otherwise you'll ship someone else's tutorial as your vendor code.Resources/config/ to config/ in Symfony 4.2+, and the old path is deprecated/removed in recent versions). Verify every snippet against the Symfony docs for your target version before copying.Acme\WidgetBundle\DependencyInjection\AcmeWidgetExtension implies alias acme_widget; your config/packages/acme_widget.yaml root key must match exactly. Use debug:config acme_widget to confirm the alias is registered.TreeBuilder with ->addDefaultsIfNotSet() and ->children() will throw on unknown keys. Add ->ignoreExtraKeys(false) only if you genuinely need forward compatibility; otherwise strict is better for catching typos in consumer config.services.yaml is private by default since Symfony 4. Services you intend consumers to fetch via $container->get() need an explicit public alias. Prefer constructor injection and keep almost nothing public.if ($config['enabled']) inside a service, that's a smell — do it at compile time via a CompilerPass or by conditionally loading the service file in load().bin/console debug:container --tag=…, debug:config <alias>, lint:container, lint:yaml config/. In a bundle context, debug:container --env-vars also helps confirm that env placeholders in your config schema resolve.CompilerPass registered in Bundle::build(), a tagged-interface convention for consumers to extend behaviour, and prepend() for defaults. These are what separate a usable bundle from a hardcoded one.How can I help you explore Laravel packages today?