Skip to content

Fix model table resolution from Source attributes - #17742

Open
QUAD69 wants to merge 4 commits into
phalcon:5.0.xfrom
QUAD69:fix-model-source-annotations
Open

QUAD69 wants to merge 4 commits into
phalcon:5.0.xfrom
QUAD69:fix-model-source-annotations

Conversation

@QUAD69

@QUAD69 QUAD69 commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Hello!

Fixes #17741.

In raising this pull request, I confirm the following:

  • I have read and understood the Contributing Guidelines
  • I have checked that another pull request for this purpose does not exist
  • I wrote some tests for this PR
  • I have updated the relevant CHANGELOG
  • I have created a documentation PR about this change

#[Source(table: 'co_customers')] is read by AttributesReader, but the ORM ignores it and uses customers for a Customers model. This causes queries to target the wrong table unless the model also calls setSource().

Resolve the class-level Source attribute only when the container's annotations service explicitly uses AttributesReader (or a subclass). A private getDefaultModelSource() method checks that opt-in and supplies the attribute value or the conventional table name. Both positional and named table arguments are supported. Explicit setSource() / setModelSource() values retain priority, the existing per-class source cache stores the result, and the manager applies its current table prefix on each lookup.

The default PHPDoc reader is not invoked for source resolution, so existing Introspection models keep working even when their unrelated docblocks are rejected by the annotation parser. PHPDoc @Source remains ignored. Missing DI/annotations services and classes without Source retain the inferred-name fallback; foreign attributes named Source do not override it.

Attributes are not inherited: a subclass without its own Source gets the table inferred from the subclass name. An inherited initialize() that calls setSource() still explicitly sets the child's source. Unit and SQLite tests cover both cases, an AttributesReader subclass, attribute-based persistence with both metadata strategies, and an Introspection model with malformed docblocks. Table resolution retains the configured metadata strategy. The Source API docblock includes a usage example; applications still configure AttributesReader on their annotations adapter.

Validation on PHP 8.5.10, Zephir 1.7.0 and PHPUnit 10.5.66:

  • Source unit suite: 36 tests, 77 assertions, all passing. The added compatibility regressions reproduce the previous reader invocation and parser exception before the opt-in fix.
  • Source SQLite integration suite: 7 tests, 46 assertions, all passing.
  • Related unit tests: 780 tests, 1842 assertions, all passing.
  • SQLite Manager, metadata-strategy and source tests: 99 tests, 352 assertions, 1 skipped, no failures.
  • Additional SQLite model tests excluding cache services and metadata-adapter tests: 743 tests, 2129 assertions, 11 skipped, no failures. These selected runs overlap; the counts are not additive.
  • PHPCS, PHP-CS-Fixer and PHPStan level 1 pass for the new and changed Source test files and fixtures; git diff --check passes.

The full suites requiring Redis/Memcached services, MySQL/PostgreSQL, and the remaining PHP-version matrix were not run locally.

Thanks!

Resolve a class-level Source through the configured annotations adapter
before falling back to the uncamelized model name. Support positional and
named table arguments while preserving explicit sources and table prefixes.

Add regression coverage for both readers, optional DI services, per-class
caching, column metadata, and SQLite persistence and queries.

Assisted-by: Codex

@niden niden left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Very nice fix and thank you for this. The logic i s correct but you missed the point where this starts running for every single model.

Looking forward to your fixes!

Comment thread phalcon/Mvc/Model/Manager.zep Outdated
let source = null,
container = this->container;

if typeof container === "object" && container->has("annotations") {

@niden niden Nov 7, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This check is true for almost every application. FactoryDefault always registers the annotations service, with the docblock Reader. Thus, each model that does not call setSource() now has its annotations parsed on the first getSource() call. This also occurs with the default Introspection metadata strategy, which did not read annotations before.

With the default Memory adapter, this parse occurs on each request, for each model class.

Can we make this opt-in? One option is to read Source only when the reader is AttributesReader. Then setReader(new AttributesReader()) is the opt-in. That reader does not parse docblocks, so it cannot throw a parser exception:

let annotations = <AnnotationsAdapterInterface> container->getShared("annotations");

if annotations->getReader() instanceof AttributesReader {
    // read the Source attribute
}

An even better implementation to the approach above is to extract this whole logic to its own method, which will be responsible for checking the annotations reader in the DI and the reader and return the scanned source or the default one.

Comment thread phalcon/Mvc/Model/Manager.zep Outdated

if typeof container === "object" && container->has("annotations") {
let annotations = <AnnotationsAdapterInterface> container->getShared("annotations"),
classAnnotations = annotations->get(get_class(model))->getClassAnnotations();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This call parses all the docblocks of the model: the class, the properties and the methods. Before this change, a model with a docblock that the annotations parser rejects worked with the Introspection strategy. Now getSource() throws Phalcon\Annotations\Exception for that model. The PR description says that these exceptions are not suppressed. For these applications, this is a breaking change in a bug-fix release i.e. v7


/**
* Names the table of a model. No framework code reads it today.
* Names the table of a model when the annotations service uses AttributesReader.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This text says that the attribute works only with AttributesReader. But the code and the tests also accept the docblock @Source. Please make the docblock and the code agree. The decision on the Manager.zep comments sets which one is correct.

* @Source(table="named_docblock_records")
*/
#[Source(table: 'named_attribute_records')]
class SourceNamedArgument extends SourceModel

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This class declares its own Source. What occurs for a subclass that does not? PHP does not inherit attributes or doc comments. Thus, a subclass of SourceModel with no Source gets its own inferred name. But when the parent calls setSource() in initialize(), the subclass gets the table of the parent. Please add a test for a subclass without Source, and tell us which result you expect.

Comment thread CHANGELOG-5.0.md Outdated

### Fixed

- `Phalcon\Mvc\Model\Manager::getModelSource()` ignoring class-level `Source` attributes and annotations. Explicit sources take precedence, and both positional and named `table` arguments are supported. [#17741](https://github.com/phalcon/cphalcon/issues/17741)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we shorten this a bit? The changelog is supposed to be short and sweet and any additional information goes in the documentation. So the "Explicit sources...." should end up in the docs in a new PR.

];
}

/**

@niden niden Nov 7, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Almost all the tests in tests/database/Mvc/Model/Manager/ use the testMvcModelManager<Method><Case>() name format, with a docblock that has @author and @since. Please use the same format for the new tests, for example testMvcModelManagerGetModelSourcePrefix(). Your name (if you wish) and the since have to be there for accreditation purposes

Only consult class attributes when the annotations service uses
AttributesReader. Extract fallback resolution to a private method so the
default docblock reader and Introspection models retain their behavior.

Cover malformed docblocks, unannotated subclasses, and inherited explicit
sources. Align test names and author/since tags with the repository style,
clarify the Source API documentation, and shorten the changelog entry.

Assisted-by: Codex
@QUAD69 QUAD69 changed the title Fix model table resolution from Source attributes and annotations Fix model table resolution from Source attributes Nov 8, 2026
Exercise attribute-based persistence and PHQL with Introspection as well
as Annotations, and assert that source resolution retains the configured
metadata strategy. Cover the opt-in behavior of an AttributesReader subclass.

Assisted-by: Codex
@QUAD69

QUAD69 commented Nov 8, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review, @niden! All six comments have been addressed:

  • Source resolution is now opt-in through AttributesReader (including subclasses), with the lookup and fallback logic extracted into getDefaultModelSource().
  • The default PHPDoc reader is no longer parsed for table resolution. Regression tests cover Introspection models with malformed docblocks.
  • The Source API docblock and PR description now consistently describe attribute-only support; PHPDoc @Source remains ignored.
  • Added inheritance tests: a subclass without its own Source gets its inferred table name, while an inherited initialize() calling setSource() still sets the explicit source.
  • Shortened the changelog and moved the details to the companion documentation PR: Document opt-in model table resolution from Source attributes documentation#351.
  • Updated test names and added @author / @since annotations to match the project conventions.

Additional tests cover both metadata strategies and an AttributesReader subclass. The selected local unit/SQLite suites and style/static-analysis checks pass; the documentation build and lint also pass.

Ready for another review. Thank you!

@QUAD69
QUAD69 requested a review from niden November 8, 2026 19:43
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