feral/symfony
feral/symfony is a small integration package for PHP/Laravel projects that bridges Symfony components into the Laravel ecosystem, offering compatibility helpers for developers who need both frameworks side by side.
Reality check first: this is a Symfony bundle (0 stars, 0 dependents, no recipe), and you're working in Laravel. Two viable paths:
composer require feral/feral) and treat this bundle purely as a reference for how to wire Feral's services into a container. Laravel's container does the same job; you just write a ServiceProvider instead of a Bundle + Extension.Minimal install (Symfony side):
composer require feral/symfony
There is almost certainly no Flex recipe, so registration is manual — open config/bundles.php and add the bundle class:
return [
// ...
Feral\Symfony\FeralBundle::class => ['all' => true],
];
Where to look first, in this order:
vendor/feral/symfony/ — list the tree. Confirm the actual namespace and bundle class name; small bundles frequently disagree with their README (Cybermantix\… vs Feral\Symfony\…).DependencyInjection/*Extension.php and DependencyInjection/Configuration.php — this is the ground truth for what services exist and what config keys are real. Ignore the README until you've read these two files.Resources/config/ (or config/services.php on newer bundles) — the service IDs, tags, and constructor arguments. Copy these definitions verbatim into a Laravel ServiceProvider if you're porting.composer.json inside the package — check the symfony/* version constraints and the feral/* requirement. That tells you whether it will even compile on your Symfony version.First use case: don't start with the interesting stuff. Register the bundle, run bin/console cache:clear && bin/console lint:container, and inject one Feral service into a throwaway console command:
final class SmokeCommand extends Command
{
public function __construct(private SomeFeralService $feral) { parent::__construct(); }
}
If that compiles and runs, the wiring works and you can start building on it. If it doesn't, you've spent ten minutes instead of a day.
Configuration-driven pipelines. The main value a bundle adds over the raw library is the config tree: define named pipelines/processors in config/packages/feral.yaml and let the extension turn them into tagged services. Read Configuration.php to learn the exact shape, then keep all environment-specific wiring in YAML rather than in PHP. In Laravel terms: think of it as config/feral.php plus a provider that loops over the array and binds each entry — same pattern, different syntax.
Constructor injection everywhere. Once registered, autowiring is the whole interface. Inject Feral services into controllers, Messenger handlers, and console commands. Never reach into the container ($container->get(...)) — that's how you lose the ability to test the wiring.
Tag your own nodes instead of forking. If Feral exposes an expression-node / processor / handler interface, the bundle almost certainly autoconfigures a tag for it. Implement the interface, register the class normally, and let autoconfigure pick it up:
App\Feral\NormalizeNode:
tags: ['feral.node'] # verify the exact tag in the bundle's services config
This is the primary extension point and it's the reason to use the bundle rather than hand-wiring the library. Confirm the tag name with bin/console debug:container --tag=feral (once you know the prefix).
Decorate, don't subclass. The bundle's service definitions are not yours to override. To add logging, metrics, or input validation around a Feral service, use decorates::
App\Feral\LoggingProcessor:
decorates: feral.some_service
arguments: ['@.inner']
Same idea in Laravel: $this->app->extend(...).
Messenger as the entry point. The cleanest day-to-day pattern is one Feral pipeline per message type, invoked from a Messenger handler. The handler is a thin adapter: pull the payload, build the Feral input (immutable collection / scalar), run the pipeline, return the result. Keep the pipeline itself free of Symfony types so it stays unit-testable.
Keep Feral types inside the boundary. Convert to plain arrays/DTOs before they touch Doctrine entities, serialized message payloads, or HTTP responses. Persistent/immutable collection objects are awkward to serialize and their API is not part of your app's contract.
Testing strategy — split it in two:
KernelTestCase that boots the kernel and asserts the services you depend on are public or injectable. One test, not twenty — a bundle is mostly configuration.For the Laravel port, replace the kernel test with a test that resolves the bindings from the container ($this->app->make(...)).
The maturity risk is the headline. Zero stars and zero dependents means: no second pair of eyes on the service definitions, no upgrade guide, no security policy, and a very real chance the package is a personal scaffold that tracks a specific Symfony version. Mitigations, in order of preference:
"feral/symfony": "1.0.*") and vendor-review the diff on every update.feral/feral package directly and write ~40 lines of ServiceProvider / service YAML yourself. You get the same result with a dependency you control.Namespace and class-name drift. The README, the composer.json PSR-4 map, and the actual bundle class frequently disagree on small packages. Trust the filesystem, not the docs. Symptoms: Class "Feral\Symfony\FeralBundle" not found at kernel boot.
Missing or incomplete Configuration class. If the bundle builds its tree with ignoreExtraKeys(true) or doesn't call ->validate(), a typo in your YAML (proccessors: instead of processors:) is silently ignored and you'll debug the wrong thing for an hour. Cross-check every key against Configuration.php and consider enabling strict mode:
feral:
# ... your keys
If the extension permits it, override the tree in your app to reject unknown keys — otherwise add a smoke test that asserts your configured services actually exist.
Container cache is stubborn. After editing feral.yaml, always bin/console cache:clear (and rm -rf var/cache/* if you've been toggling debug). A stale container in prod-like mode will happily run yesterday's wiring.
Autowiring failures are the most common error and the message is usually good: Cannot autowire service "App\…": argument "$foo" of type "…" references class "…" but no such service exists. Fix by explicitly defining the service or binding the argument, not by making the constructor nullable. Use bin/console debug:autowiring feral and bin/console debug:container --show-private feral to see what's actually registered.
Bundles without recipes don't get config files created for you. You'll be writing config/packages/feral.yaml from scratch by reading Configuration.php. Copy the tree structure into the file as comments so the next developer doesn't have to repeat the archaeology.
Debugging inside a pipeline. Feral-style pipelines are composition-heavy, so stack traces point at framework internals. Add tracing at the boundaries: a decorator that logs input/output per node, or a Stopwatch around the whole pipeline. Do this before you have a production bug, not after.
Extension points summary (verify names against the source): custom node/processor via tag + autoconfigure; decorators via decorates:; parameters via the config tree; anything else — don't. There is no supported way to replace the bundle's Extension or CompilerPass, so if you need to change how services are built, fork.
Don't let Feral become the app's public vocabulary. These types will appear in your controllers, handlers, and tests within a week. Keep them at the edges (adapters, DTO mappers) so a future swap to plain arrays or a maintained alternative is mechanical. If you can't articulate what Feral buys you over a few array_map/array_filter calls and a match expression, you probably don't need this dependency at all — especially at this maturity level.
How can I help you explore Laravel packages today?