Repository navigation
Bring src/Adapter to Mago compliance - #248
Open
simon-mundy wants to merge 7 commits into
Open
simon-mundy wants to merge 7 commits into
simon-mundy wants to merge 7 commits into
Conversation
Tag every Adapter interface, trait, abstract class and leaf exception @api, type the interface constants, and give the array parameters and returns their element types. Connection parameters get a ConnectionParameters alias on ConnectionInterface, and ResultInterface declares its Iterator template. DriverInterface's createResult() and createStatement() take an explicit mixed parameter, which every existing implementation already satisfies.
Integer names now resolve through one helper. An unknown position throws the existing missing-data exception from the errata and max-length setters instead of writing under an empty key, offsetGet() returns null for an integer name that is not set instead of failing in ltrim(), and the errata and max-length getters return null without a warning when the parameter has none. offsetUnset() still leaves the name in its position.
AbstractPdo rejects a bare PDO connection with forBarePdoConnection() where it used to call methods PDO does not have. AbstractPdoConnection throws forMissingConnectionResource() when connect() leaves it without a PDO, and forMissingDriver() when no driver is set, instead of calling methods on null. DriverFeatureProviderTrait checks that the composing class is both a driver and a feature provider before it changes state.
Statement::bindParametersFromContainer() maps names and types through two private helpers, and passes the prepared resource to createResult(). Result::count() resolves the row count in one place and the PDOException paths are declared. Statement::setSql(null) and StatementContainer's setSql(null) store an empty string, as the interface allows null.
getHelpers() matches helper names strictly, quoteIdentifierInFragment() reads its quote pair once and tolerates a failed split, and Profiler::getLastProfile() returns null before any profile starts instead of failing on end()'s false.
Remove the src/Adapter entries these changes fix, plus one TableGateway entry that ResultInterface's Iterator template also clears. The three ArrayAccess parameter-name findings on ParameterContainer replace the parameter-type findings for the same methods. Exclude the contract-sized Adapter types from too-many-methods and register Pdo\Statement's initializers.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
🟡 Changes recommended
The quote-identifier helper renames its parameter from $value to $identifier, breaking existing named-argument calls unless $value is preserved or the breaking API change is explicitly documented.
1 open issue
What changed in this PR
This PR reduces Mago debt in the database Adapter layer through stronger type documentation, safer failure handling, and focused refactoring, while leaving broader API changes for separate decisions.
Changes:
- Adds API annotations, typed constants, array shapes, and exception documentation.
- Guards unusable PDO connections and missing drivers, and fixes parameter lookup, nullable SQL, and empty-profile handling.
- Expands regression tests and updates Mago configuration and baselines.
| File | Description |
|---|---|
test/unit/Adapter/StatementContainerTest.php |
Tests null SQL normalization. |
test/unit/Adapter/Profiler/ProfilerTest.php |
Tests profile ordering, empty state, and timing. |
test/unit/Adapter/Platform/TestAsset/TestPlatform.php |
Allows configurable identifier quotes. |
test/unit/Adapter/Platform/Sql92Test.php |
Tests identifier escaping and asymmetric quotes. |
test/unit/Adapter/ParameterContainerTest.php |
Covers missing positions, metadata, and name mappings. |
test/unit/Adapter/Exception/RuntimeExceptionTest.php |
Tests new exception constructors. |
test/unit/Adapter/Driver/TestAsset/TestNonProviderFeatureDriver.php |
Adds an invalid feature-provider fixture. |
test/unit/Adapter/Driver/Pdo/TestAsset/UnconnectableConnection.php |
Adds a connection fixture without a PDO resource. |
test/unit/Adapter/Driver/Pdo/TestAsset/BindingObservableStatement.php |
Exposes binding state for tests. |
test/unit/Adapter/Driver/Pdo/StatementTest.php |
Tests binding types, state, and nullable SQL. |
test/unit/Adapter/Driver/Pdo/ResultTest.php |
Declares coverage for the row-count helper. |
test/unit/Adapter/Driver/Pdo/PdoTest.php |
Tests connection guards and statement creation. |
test/unit/Adapter/Driver/Pdo/ConnectionTransactionsTest.php |
Tests transactions against SQLite PDO. |
test/unit/Adapter/Driver/Pdo/ConnectionTest.php |
Tests connection resources and missing-driver guards. |
test/unit/Adapter/Driver/Feature/DriverFeatureProviderTraitTest.php |
Tests composition guards and fluent returns. |
test/unit/Adapter/AdapterTest.php |
Tests delegation, initial parameters, and helper selection. |
src/Adapter/StatementContainerInterface.php |
Marks the contract as public API. |
src/Adapter/StatementContainer.php |
Enables strict types and normalizes null SQL. |
src/Adapter/SchemaAwareInterface.php |
Adds API annotation and removes an obsolete comment. |
src/Adapter/Profiler/ProfilerInterface.php |
Marks the profiler contract as public API. |
src/Adapter/Profiler/ProfilerAwareInterface.php |
Marks profiler integration as public API. |
src/Adapter/Profiler/Profiler.php |
Handles empty profiles and clarifies profile types. |
src/Adapter/Platform/Sql92.php |
Types its constant and updates guard documentation. |
src/Adapter/Platform/PlatformInterface.php |
Documents API status and safe-word element types. |
src/Adapter/Platform/AbstractPlatform.php |
Types quote pairs and handles failed fragment splitting. |
src/Adapter/ParameterContainer.php |
Centralizes position resolution and guards metadata access. |
src/Adapter/Exception/RuntimeException.php |
Adds exceptions for unusable connection state. |
src/Adapter/Exception/InvalidQueryException.php |
Marks the exception as public API. |
src/Adapter/Exception/InvalidConnectionParametersException.php |
Documents rejected parameter types and API status. |
src/Adapter/Exception/InvalidArgumentException.php |
Marks the exception as public API. |
src/Adapter/Exception/ErrorException.php |
Marks the exception as public API. |
src/Adapter/Driver/StatementInterface.php |
Documents statement parameter element types. |
src/Adapter/Driver/ResultInterface.php |
Adds iterator generics and API annotation. |
src/Adapter/Driver/PdoDriverInterface.php |
Marks the PDO driver contract as public API. |
src/Adapter/Driver/PdoDriverAwareInterface.php |
Marks PDO driver integration as public API. |
src/Adapter/Driver/PdoConnectionInterface.php |
Marks the PDO connection contract as public API. |
src/Adapter/Driver/Pdo/Statement.php |
Extracts binding helpers and normalizes null SQL. |
src/Adapter/Driver/Pdo/Result.php |
Extracts row-count resolution and improves types. |
src/Adapter/Driver/Pdo/AbstractPdoConnection.php |
Guards missing resources and drivers. |
src/Adapter/Driver/Pdo/AbstractPdo.php |
Guards bare PDO connections and simplifies parameter formatting. |
src/Adapter/Driver/Feature/DriverFeatureProviderTrait.php |
Validates feature-provider composition. |
src/Adapter/Driver/Feature/DriverFeatureProviderInterface.php |
Clarifies feature collection types and API status. |
src/Adapter/Driver/Feature/DriverFeatureInterface.php |
Marks the feature contract as public API. |
src/Adapter/Driver/Feature/AbstractFeature.php |
Marks the feature base class as public API. |
src/Adapter/Driver/DriverInterface.php |
Types constants and explicitly declares mixed inputs. |
src/Adapter/Driver/DriverAwareInterface.php |
Marks driver integration as public API. |
src/Adapter/Driver/ConnectionInterface.php |
Defines a connection-parameter type alias. |
src/Adapter/Driver/AbstractConnection.php |
Applies connection-parameter and resource documentation. |
src/Adapter/AdapterInterface.php |
Types constants and documents parameter collections. |
src/Adapter/AdapterAwareTrait.php |
Adds strict types and API annotation. |
src/Adapter/AdapterAwareInterface.php |
Adds strict types and API annotation. |
src/Adapter/Adapter.php |
Uses strict helper selection and first-class callables. |
mago.toml |
Configures justified exclusions and statement initializers. |
lint-baseline.toml |
Removes resolved Adapter lint entries. |
analyzer-baseline.toml |
Removes resolved findings and retains deferred API issues. |
Review effort: Balanced
The first-class callable exposed quoteIdentifier()'s $identifier name, so calling the helper with value: threw an unknown named parameter error. Forward through a closure instead, and cover both helpers with a test that passes value by name.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Takes
src/Adapterfrom 265 baselined Mago findings to 22 (212 → 21 analyzer, 53 → 1 lint). Every finding left needs a public signature change or a class split, listed under Still baselined for a decision.Problem
After #233,
src/Adaptercarries most of the remaining Mago debt: about 72% of the analyzer baseline, plus 53 lint entries. Several of those findings were real failure paths, not just missing types:nullwhen a connection has no driver, or whenconnect()leaves it without a PDO;PDOthat it doesn't have (isConnected(),getResource(),getLastGeneratedValue());Profiler::getLastProfile()returningfalsethrough a?arrayreturn type;ParameterContainerreading undefined positions.Cause
@apior@internal.$driver,$resource, the PDO-or-wrapper$connection) were used without a guard.ParameterContainerresolved integer names with an unchecked$this->positions[$name].Fix
Types and tags
@api. That also clearsclass-must-be-finalwithout making any classfinal.public const string, andpublic const arrayforVALID_FETCH_MODES.ParameterContainer|array<array-key, mixed>for statement parameters;ConnectionParametersalias (array<string, mixed>) onConnectionInterface;array<array-key, string>for safe words;array{string, string}for the identifier quote pair.ResultInterface,Pdo\ResultandParameterContainerdeclare theirIteratorandArrayAccesstemplates.DriverInterface::createResult()andcreateStatement()take an explicitmixedparameter. Untyped parameters already meanmixed, so every existing implementation still conforms.@throwstags carryPDOExceptionand the AdapterRuntimeExceptionthrough the methods that reach them.Guards, with new named constructors on
Adapter\Exception\RuntimeExceptionAbstractPdo::$connectionis a barePDO(createStatement(),getConnection(),getLastGeneratedValue())forBarePdoConnection()connect()leavesAbstractPdoConnectionwithout a PDO (beginTransaction(),commit(),execute(),rollback())forMissingConnectionResource()AbstractPdoConnection(execute(),prepare())forMissingDriver()DriverFeatureProviderTraitcomposed into a driver that is not aDriverFeatureProviderInterfaceforUncomposableTrait()Each of these used to end in an
Error(method call onnull, or an undefined method onPDO) or aTypeError.Refactors with no change in behaviour
ParameterContainerresolves integer names throughnameAt(),requireName()andrequireDataName().offsetSet()resolves its key in a singlematch.Pdo\Statementbinds throughtoPdoParameter()andtoPdoType().Pdo\Result::count()goes throughresolveRowCount().AbstractPdo::formatParameterName()decides named-versus-positional in one expression.isset()became?? nullorarray_key_exists(), keeping isset's null semantics where it matters.Scoped expectations, each with a docblock reason
AbstractPdoConnection::execute()hastoo-many-arguments. It passes the SQL as a second argument tocreateResult()for drivers that accept a context, such as SQLite.AbstractPdo::formatParameterName()hasredundant-cast. The analyzer reads$nameas the parent'sstring, but this implementation also takesint.mixed-assignmentcovers parameter values, which aremixedby nature, in:Pdo\Statement::bindParametersFromContainer();ParameterContainer::setFromArray();Adapter::getHelpers().ParameterContainer::offsetSetReference()haspossibly-undefined-string-array-index. Taking a reference creates the key.mago.tomlConnectionInterface,PlatformInterface,Pdo\ResultandParameterContainerjoin thetoo-many-methodsexclusions, with a reason. Their method count is set by the interfaces they implement and by the driver contract.Pdo\Statement::initialize()andsetDriver()are registered as class initializers for$pdoand$driver.Behaviour changes
ParameterContainer::offsetSetErrata()andoffsetSetMaxLength()throwInvalidArgumentException::forMissingData()for an integer name that is not a known position. They used to raise an undefined-key warning and write under the''key.ParameterContainer::offsetGetErrata()andoffsetGetMaxLength()returnnullwithout a warning when the parameter exists but has no errata or max length. Unknown positions throwforMissingData()as before, now without the warning.ParameterContainer::offsetGet()returnsnullfor an integer name that is not set. It used to throw aTypeErrorfromltrim().StatementContainer::setSql(null)andPdo\Statement::setSql(null)store'', asStatementContainerInterface::setSql(?string)allows. They used to throw aTypeError.StatementContainer,AdapterAwareInterfaceandAdapterAwareTraitnow declarestrict_types.StatementContainer::setSql(5)throws aTypeErrorinstead of storing"5".DriverFeatureProviderTrait::addFeatures()checks the composing class even when given an empty list. Its methods drop#[Override], which Mago rejects on trait methods.Adapter::getHelpers()matches helper names strictly.getHelpers(true)no longer returns the quote-identifier helper through the old looseswitch.Profiler::getLastProfile()returnsnullbefore any profile starts. It used to throw aTypeErroronend()'sfalse.ProfileShapesqlkey is documented asstring|null, becauseStatementContainerInterface::getSql()can returnnull.AbstractPdoConnection::setResource()casts the PDO driver name to a string before lowercasing it.AbstractPlatform::quoteIdentifierInFragment()returns''when the split fails, without aforeachwarning. The return value is unchanged.What stays the same
final. No public or protected method signature, property type or parameter name changed, except the equivalent explicitmixedonDriverInterface's two parameters.Pdo\Statement::$isQuery, which is never written, gains afalsedefault.ParameterContainer::offsetUnset()still leaves the name inpositions. A later write to that integer position goes back to the same name, and a new test pins this.ParameterContainer::offsetGet()andoffsetExists()keepisset()semantics: anullvalue counts as unset.integer,nullandlobmap to their PDO types, and any other errata binds as a string;int,boolandnullvalues pick their PDO types, and anything else binds as a string.formatParameterName()returns the same names for every input.AbstractConnection::$resourcestays untyped, becausephpdb-mysqlandphpdb-pgsqlredeclare it untyped.Still baselined: decisions needed
Each of these needs a public signature change, so it is left for a decision:
missing-return-typeonAdapterInterface::getHelpers(),ConnectionInterface::getResource(),StatementInterface::getResource()array,mixed)missing-property-typeandmixed-return-statementonAbstractConnection::$resource/getResource()phpdb-mysqlandphpdb-pgsql, which redeclare it untypedmissing-property-type×7 andmissing-parameter-typeonPdo\Resultinitialize(string|int|false|null $generatedValue)incompatible-return-typeonPdo\Result::next()voidinstead of returning the rownext()'s return valuemissing-property-type×2 onProfiler$profilesand$currentIndexoverly-wide-return-typeonAbstractPdo::getResultPrototype()ResultInterface, not?ResultInterface?ResultInterfaceincompatible-parameter-name×4 onParameterContainer'sArrayAccessmethods$nameto$offsetcyclomatic-complexityonParameterContainer(lint)The three
ArrayAccessparameter-name entries foroffsetExists,offsetGetandoffsetSetare new in the baseline. They replace the threeincompatible-parameter-typeentries for the same methods, which@implements ArrayAccess<array-key, mixed>clears. A separate question is whetherAbstractPdo::$connectionshould dropPDOfrom its union, since no driver method can use a barePDO.Tests
src/line is covered, and each new private helper is listed in its test's#[CoversMethod].typeof(), plus aPDOStatementmock fornull→PDO::PARAM_NULL;ParameterContainerposition edge cases;Profilerindex and elapsed time.UnconnectableConnection, whoseconnect()leaves no PDO;TestNonProviderFeatureDriver, a driver withoutDriverFeatureProviderInterface;BindingObservableStatement, which exposes the protected binding hook and$parametersBound.0.6.x. On0.6.x, 49 of these files' mutants were "killed" only because they raised analyzer findings the baseline didn't cover. With the files clean, 34 survived at first. The new tests kill all of them, and two of them showed redundant code that this PR removes.Result
0.6.x)src/Adapter, both baselinesmago format --check,lint,analyzeandguardexit 0 withMAGO_PHP_VERSIONset to 8.3, 8.4 and 8.5, and PHPUnit is OK with no issues.