wikimedia/object-factory
Wikimedia ObjectFactory builds PHP objects from declarative specifications, mapping class names, constructor arguments, and factory callables to instances. Lightweight, dependency-free, and used by Wikibase and other Wikimedia projects.
ObjectFactory is a ~300-line library with one job: turn a declarative array (['class' => ..., 'args' => [...]]) into a live object. It is the Wikimedia equivalent of a hand-rolled match/switch factory, extracted so that extensions can declare what to build without knowing how.
composer require wikimedia/object-factory
Look at two things first, in this order:
README.md in the repo — it is the entire public contract (spec keys + options bag).src/ObjectFactory.php — small enough to read end-to-end in ten minutes; there is no magic beyond argument unpacking and a service callback.Minimal use:
use Wikimedia\ObjectFactory\ObjectFactory;
$factory = new ObjectFactory();
$formatter = $factory->getObjectFromSpec( [
'class' => MyFormatter::class,
'args' => [ 'Y-m-d', true ],
] );
// equivalent to: new MyFormatter( 'Y-m-d', true )
First real use case: a config-driven registry. You are building something plugin-shaped (export drivers, payment gateways, webhook handlers) and you want config/export.php to say 'driver' => ['class' => CsvExporter::class, 'args' => [';']] instead of hard-coding a match in a service provider. That is exactly the shape MediaWiki uses for extension attributes (ContentHandlers, Actions, API modules), and it is where this package earns its keep.
If you are coming from Laravel: this is not a replacement for the container. app()->make() resolves by type with autowiring and contextual bindings; ObjectFactory resolves by explicit spec with explicit positional args. The two compose — ObjectFactory for "user-supplied plugin spec", the container for your own wiring.
The four spec forms. Everything is one of these; recognise them and the library is memorised:
// 1. class + positional args
[ 'class' => Foo::class, 'args' => [ $a, $b ] ]
// 2. already-built object (returned as-is — great for test doubles)
[ 'class' => $fooInstance ]
// 3. factory callable (static constructors, closures)
[ 'factory' => [ Foo::class, 'newFromConfig' ], 'args' => [ $config ] ]
// 4. class + scoped services
[ 'class' => Foo::class, 'services' => [ 'Logger', 'Clock' ] ]
Form 2 is the one people forget: it makes every registry trivially injectable in tests without touching the spec.
Scope the service locator. Construct the factory with a resolver, then let plugin specs request only what they are allowed to see. Because services names an allow-list per spec, a plugin physically cannot reach a service you did not grant — a cheap capability boundary that app() does not give you.
$factory = new ObjectFactory( fn ( string $id ) => $this->app->make( $id ) );
$handler = $factory->getObjectFromSpec( [
'class' => StripeWebhook::class,
'services' => [ 'http', 'logger' ], // nothing else is reachable
] );
Normalise ergonomics with the options bag. If your config format should also accept a bare string ('driver' => CsvExporter::class), do not write the branching yourself:
$driver = $factory->getObjectFromSpec( $spec, [
'allowClassName' => true, // accept a plain class-string spec
'assertClass' => Driver::class, // type-check at the boundary
] );
assertClass is the single highest-value option: it converts a config typo into an immediate, localised exception instead of a Call to undefined method five layers deep. extraArgs (appending runtime context — request, tenant, output buffer — after the config-declared args) is how you keep the spec static while still passing per-call state.
Wrap it, don't sprinkle it. In a real app, call sites should never touch getObjectFromSpec directly:
final class DriverRegistry
{
/** @var array<string, Driver> */
private array $resolved = [];
public function __construct(
private ObjectFactory $factory,
private array $specs, // from config/drivers.php
) {}
public function get( string $name ): Driver
{
return $this->resolved[$name] ??= $this->factory->getObjectFromSpec(
$this->specs[$name],
[ 'assertClass' => Driver::class ]
);
}
}
ObjectFactory does no caching and no singleton management — it calls new every time. Memoise in your wrapper, keyed by name, exactly as above.
Prefer factory over args for anything non-trivial. A 'factory' => [Foo::class, 'newFromSpec'] spec gives you a stable, named seam that survives constructor refactors. Positional args do not: adding a parameter in the middle of a constructor silently shifts every config file in your repo.
Integration with Laravel config: keep specs in plain arrays (no closures) so they can be cached by config:cache, and resolve lazily at the point of use. That is the whole trick — config/drivers.php stays pure data, the registry stays pure code.
Wikimedia\ObjectFactory\ObjectFactory; 1.x was Wikimedia\ObjectFactory. Half the blog posts and Stack Overflow answers online are 1.x. If your use statement fails, this is why.args are packed positionally into the constructor; missing or extra args are your problem. This is the biggest conceptual gap versus Laravel's container, and the reason to keep specs thin.services and factory don't mix. Services are injected by ObjectFactory as the first constructor argument during instantiation; when you supply your own factory, you own construction and the services key is not doing what you think. Use one or the other.args as an API contract. Internally, prefer named factory methods.'class' is an arbitrary class-name instantiator. A spec arriving from a request body is a remote object-construction primitive. Config files only.'factory' => function () {...} closure will explode config:cache, serialize(), and any PSR-6 cache. Closures are fine only for specs built in code at runtime.'class' even when using factory. It costs nothing and makes exception messages and assertClass useful; the factory spec otherwise reports as anonymous.InvalidArgumentException carries the offending spec, so log the spec, not just the message. Ninety percent of failures are (a) class not autoloadable, (b) argument count/signature mismatch, (c) service name not registered with the resolver you passed to the constructor.'argument' instead of 'args' builds the object with zero arguments and fails obscurely. If this bites you, validate specs with a schema (Laravel's Validator, or opis/json-schema) at boot.ObjectFactory is not designed as an extension point; the supported seam is the service resolver passed to the constructor. Layer your registry/pooling/validation on top instead.'class' => $object) short-circuits construction, you can swap any spec for a fake in a unit test without touching the class under test.How can I help you explore Laravel packages today?