Skip to content

feat: implement phase 2 ORM improvements#24

Merged
davebarnwell merged 3 commits intomainfrom
feature/phase-2-model-ergonomics
Mar 8, 2026
Merged

feat: implement phase 2 ORM improvements#24
davebarnwell merged 3 commits intomainfrom
feature/phase-2-model-ergonomics

Conversation

@davebarnwell
Copy link
Owner

Summary

This PR implements the Phase 2 roadmap work for the ORM by improving developer ergonomics and tightening the typed API without expanding the package into a heavyweight query builder or relationship system.

The user-visible effect is that model validation can now inspect instance state directly, strict field mode can fail fast on unknown assignments, and several common read patterns no longer require handwritten SQL fragments. The library also now declares strict types across the PHP files that participate in the package and test suite, which makes the public API clearer to IDEs and static analysis.

The root cause behind the current friction was that the base model still leaned on legacy patterns that were functional but awkward in practice. Validation lived on a static hook even though writes operate on object state, typo-prone field assignments could silently disappear at persistence time, and common "exists", ordered fetch, and single-column read operations required users to assemble SQL fragments manually. The codebase also still carried loosely typed entry points even after the package moved to PHP 8.3+.

The fix keeps backward compatibility where it matters while making the preferred API more explicit. Model now supports instance-aware validateForSave(), validateForInsert(), and validateForUpdate() hooks, while still honoring the legacy static validate() method for older models. Strict field mode can be enabled per model or at runtime. The ORM also adds focused helper methods such as exists(), existsWhere(), fetchAllWhereOrderedBy(), fetchOneWhereOrderedBy(), and pluck() to cover high-value query cases without introducing a chainable query builder. On top of that, the PHP files now declare strict types and several signatures in Model have been tightened.

The PR includes new regression coverage for strict field behavior, instance validation hooks, legacy validation compatibility, and the new query helpers. The documentation in README.md and EXAMPLE.md has been updated to lead with camelCase dynamic methods, instance-aware validation, strict field mode, and the new helper APIs.

Validation

I validated the changes with the existing quality gates:

  • vendor/bin/phpunit -c phpunit.xml.dist
  • vendor/bin/phpstan analyse -c phpstan.neon

@davebarnwell davebarnwell marked this pull request as ready for review March 8, 2026 09:02
Copilot AI review requested due to automatic review settings March 8, 2026 09:02
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements “Phase 2” ORM ergonomics improvements by adding instance-aware validation hooks, opt-in strict field assignment, and a small set of focused query helper APIs, while tightening typing across the library and tests.

Changes:

  • Add strict field mode (per-model static flag + runtime toggle) and instance-aware validation hooks with legacy validate() compatibility.
  • Add focused read helpers: exists(), existsWhere(), fetchAllWhereOrderedBy(), fetchOneWhereOrderedBy(), and pluck().
  • Enable declare(strict_types=1) across core + tests and expand regression coverage + docs to match the new preferred APIs.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/Model/Model.php Core implementation: strict fields, instance validation hooks, focused query helpers, and tightened signatures/types.
src/Model/Exception/ModelException.php Enable strict types for exception base class.
src/Model/Exception/UnknownFieldException.php Enable strict types for exception.
src/Model/Exception/MissingDataException.php Enable strict types for exception.
src/Model/Exception/InvalidDynamicMethodException.php Enable strict types for exception.
src/Model/Exception/ConnectionException.php Enable strict types for exception.
src/Model/Exception/ConfigurationException.php Enable strict types for exception.
tests/Model/CategoryTest.php Adds coverage for query helpers, strict fields, instance validation hooks, and legacy validation compatibility; enables strict types.
tests/Model/SqliteModelTest.php Enables strict types.
test-src/Model/ValidatingCategory.php Test helper model implementing instance validation hooks.
test-src/Model/LegacyValidatingCategory.php Test helper model exercising legacy static validate() support.
test-src/Model/StrictCategory.php Test helper model enabling strict field mode via static flag.
test-src/Model/Category.php Enables strict types in test helper model.
test-src/Model/SqliteCategory.php Enables strict types in test helper model.
test-src/Model/SqliteCodeCategory.php Enables strict types in test helper model.
test-src/Model/SqliteStringCodeCategory.php Enables strict types in test helper model.
test-src/Model/IsolatedConnectionCategoryA.php Enables strict types in test helper model.
test-src/Model/IsolatedConnectionCategoryB.php Enables strict types in test helper model.
README.md Documents new focused query helpers, instance-aware validation, strict field mode, and preferred camelCase dynamic methods.
EXAMPLE.md Updates examples to lead with camelCase dynamic methods and document new helpers/validation/strict fields.
.php-cs-fixer.dist.php Enables strict types in tooling config file.
Comments suppressed due to low confidence (1)

src/Model/Model.php:105

  • __construct now requires an array and the file is strict_types=1, which turns previously-tolerated inputs (e.g. null or stdClass) into a TypeError. If backward compatibility is intended, consider accepting array|object|null (or iterable) and normalising to an array before calling hydrate() (and drop the redundant is_array() check once the signature matches the supported inputs).
    public function __construct(array $data = [])
    {
        static::getFieldnames(); // only called once first time an object is created
        $this->clearDirtyFields();
        if (is_array($data)) {
            $this->hydrate($data);
        }
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@davebarnwell davebarnwell merged commit bfdbc35 into main Mar 8, 2026
7 checks passed
@davebarnwell davebarnwell deleted the feature/phase-2-model-ergonomics branch March 8, 2026 09:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants