ErrLookup › Background articles › PHP \LogicException: the programmer-error exception — missing components, conflicting options, and misconfigured services that fail loud
PHP \LogicException: the programmer-error exception — missing components, conflicting options, and misconfigured services that fail loud
PHP's \LogicException signals a fault in how code is written or configured — a missing optional dependency, mutually exclusive options, a service declared in a way the container cannot honour, or a precondition the caller skipped. Symfony, Laravel, Composer, and Guzzle all reach for it to refuse an operation outright instead of silently degrading, so when a developer meets it the fix is almost always at the call site or in configuration, not in retrying or waiting.
Distilled from 199 documented records across 4 repositories.
Background
LogicException is a built-in PHP base class for errors that represent faults in the program itself. The PHP manual frames it as the sibling of RuntimeException: where RuntimeException covers conditions that only arise at run time, LogicException covers calls that should never have been made in the current state. When a library throws it, the message is not "something transient went wrong" but "this call is wrong". Across the four repositories in this family, the posture is the same — fail loud, do not fall back, do not return a silent no-op.
The mechanism varies by layer. At the dependency-injection and container layer (heavily used by Symfony), LogicException fires during compilation or at first service resolution when an optional component is referenced but not installed — symfony/form, symfony/validator, symfony/mime, symfony/security-core, symfony/acl, or a too-old symfony/http-client — or when a service is declared in a way the container cannot honour (a synthetic service asked to reset, a non-lazy service asked to resetLazyObject). At the configuration layer it rejects mutually exclusive option combinations at construction time, so the misuse surfaces the moment a config object or attribute is built rather than pages later — for example #[MapEntity] given both id and mapping, or both id and exclude, or a messenger routing wildcard that is not a valid namespace prefix.
From the caller's side the exception usually arrives with no ambiguity about which call is at fault, because the message names the symbol, the option, or the missing class. It is distinct from a fatal "Class not found": the class was autoloaded and the code chose to refuse the operation. Composer uses LogicException to assert install-state invariants — querying a Locker before composer.lock exists, or bootstrapping a plugin whose install path resolved to null. Laravel uses it to refuse an operation a driver never implemented (dropAllTables on the base Schema\Builder) or that the data shape forbids (queueing an Eloquent\Collection whose models span more than one class, or more than one connection). Guzzle uses it as a deliberate security control: FileCookieJar::__unserialize() throws unconditionally because a deserialized jar is an object-injection gadget that could write attacker bytes to an attacker-chosen path.
A handful of records mark states the library treats as unreachable — ImportMapUpdateChecker comparing non-semver versions, GitBitbucketDriver finding no fallback driver after getRepoData() returned false, PluginManager with an empty autoload list. Reaching those usually means a subclass overrode a method it should not have, an upstream tag produced an unexpected version shape, or a genuine bug worth reporting upstream. The unifying rule across the family: do not swallow the exception, do not paper over it — fix the configuration or the call.
Common causes
- Optional component configured but not installed.A feature references a component the project does not require. Symfony throws this for forms, validator, mime, security-core, acl, and an older-than-7.4 http-client when the corresponding config or service path is exercised. The class_exists() guard fires at autoload or compile time and the message names the missing package.
- Mutually exclusive options combined.Two options that cannot both hold are passed together. Symfony's #[MapEntity] rejects id with mapping and id with exclude at construction time; ImportMapGenerator rejects any integrity algorithm outside sha256/sha384/sha512; messenger routing rejects '*' patterns that are not pure namespace-prefix wildcards.
- Service declared so the container cannot reset or recreate it.A synthetic service (set at runtime) has no factory to re-invoke, and a non-lazy service returns false from resetLazyObject(). ManagerRegistry::resetService throws in both cases during cache:clear or per-request resets, because continuing would leak state or silently no-op.
- API called before its preconditions are satisfied.Composer's Locker queried before composer.lock exists, Symfony's History::current() read before any request was pushed, or AbstractBrowser::insulate enabled without overriding getScript(). The object exists but is not in the state the method requires.
- Capability not implemented by the active driver or builder.Laravel's base Schema\Builder::dropAllTables is a deliberate stub; a custom or third-party driver that subclassed Builder without overriding it (and is then hit via migrate:fresh) triggers the throw. The connection type is wrong for the call.
- Heterogeneous collection handed to the queue.Laravel's Eloquent\Collection refuses to serialize for a queued job when its models span more than one concrete class (getQueueableClass) or more than one database connection (getQueueableConnection). One class string or one connection name cannot represent the mixed payload.
- Unreachable internal state reached.An assertion the library believed could not fire: ImportMapUpdateChecker cannot classify a version diff, GitBitbucketDriver has no fallback after a false return, PluginManager built an empty autoload list. Usually caused by subclassing that broke a contract, an upstream non-semver tag, or a genuine bug to report.
What usually fixes it
- Install or pin the missing optional component: require the named package (symfony/form, symfony/validator, symfony/mime, symfony/security-core, symfony/acl-bundle, or symfony/http-client:^7.4), or remove the config that exercises it. The exception message tells you which.
- Pick exactly one of any set of exclusive options — drop either id or mapping/exclude on #[MapEntity], use only sha256/384/512 for SRI, and shape messenger wildcards as Namespace\* with a trailing backslash before the asterisk.
- Satisfy preconditions before the call: run composer install/update (or guard with isLocked()) before querying a Locker, issue at least one request before reading History::current(), and override getScript() only when you actually call insulate(true).
- Fix the service definition so the container can honour the operation: add lazy: true to entity managers (and keep symfony/proxy-manager-bridge installed), exclude synthetic services from registry resets, or replace a synthetic service with a real factory — do not combine synthetic with a factory.
- Switch to a driver or operation that supports the call: use a Laravel connection whose Builder overrides dropAll* (mysql, sqlite, pgsql, sqlsrv), or replace migrate:fresh with migrate:rollback/migrate:refresh, or drop tables per-table with Schema::drop().
- Make data shapes uniform before serializing to the queue, or pass the missing type explicitly: group a mixed Eloquent\Collection by get_class() or getConnectionName() and dispatch one job per group; add a #[UseResource(...)] attribute or pass the resource class to toResource() so Laravel does not have to guess.
Documented occurrences
- The "%s" service is synthetic and cannot be reset.(symfony/symfony)
- To insulate requests, you need to override the getScript() method.(symfony/symfony)
- Caching cannot be enabled as version 7.4+ of the HttpClient component is required.(symfony/symfony)
- Calling "cache:clear" with a kernel that does not implement "Symfony\Component\HttpKernel\RebootableInterface" is not supported.(symfony/symfony)
- GuzzleHttp\Cookie\FileCookieJar should never be unserialized(guzzle/guzzle)
- The extension with alias "%s" does not have configuration.(symfony/symfony)
- Not defining the IdReader explicitly as a value callback when the query can be optimized is not supported.(symfony/symfony)
- Resetting a non-lazy manager service is not supported. Declare the "%s" service as lazy.(symfony/symfony)
- Invalid Messenger routing configuration: invalid namespace "%s" wildcard.(symfony/symfony)
- An instance of "%s" must be provided to use "%s()".(symfony/symfony)
- At least the plugin package should always be autoloaded for the code below to work(composer/composer)
- Unable to determine update type for "%s" and "%s".(symfony/symfony)
- This database driver does not support dropping all tables.(laravel/framework)
- The "id" and "mapping" options cannot be used together on #[MapEntity] attributes.(symfony/symfony)
- Queueing collections with multiple model types is not supported.(laravel/framework)
- A fallback driver should be setup if getRepoData returns false(composer/composer)
- The "id" and "exclude" options cannot be used together on #[MapEntity] attributes.(symfony/symfony)
- The page history is empty.(symfony/symfony)
- No lockfile found. Unable to read locked packages(composer/composer)
- Queueing collections with multiple model connections is not supported.(laravel/framework)
…and 179 more across the corpus — use search.
Honest provenance: generated on 2026-08-12 from AI-assisted analysis of the linked records. See how records are made.