perplorm/perpl

Perpl is an improved and still maintained fork of Propel2, an open-source Object-Relational Mapping (ORM) for PHP.

v2.4.0 2025-05-07 21:32 UTC

This package is auto-updated.

Last update: 2025-05-14 18:42:33 UTC


README

Perpl is a fork of the unmaintained Propel2, an open-source Object-Relational Mapping (ORM) for PHP. It adds several improvements and fixes, including proper versioning.

Github actions Status codecov PHPStan Psalm Code Climate Minimum PHP Version License Gitter

Installation

  • Replace the require declaration for Propel with Perpl:
  "require": {
+    "perplorm/perpl": ">=2.0",
-    "propel/propel": "dev-main as 2.0.x-dev",
  },
  • Remove the vcs entry for Propel2 dev in composer.json:
  "repositories": [
-    {
-      "type": "vcs",
-      "url": "git@github.com:propelorm/Propel2.git"
-    }
  ],
  • Update libraries:
$ composer update
  • Rebuild models:
$ vendor/bin/propel --config-dir <path/to/config> model:build
$ composer dump-autoload
  • Open a file where you call Query::find() and replace it with Query::findObjects(). If everything worked, you get return type ObjectCollection<YourModelName>. Yay!
⚡ Don't forget to analyze your project with PHPStan (or similar) to get notice where updates are necessary due to improved types in Perpl.

Features

Motivation for Perpl was to make features available around code-style, typing, performance and usability.

Ready for PHP 8.4

Code is tested to run in PHP 8.4 without deprecation notices or warnings.

Type-preserving queries

Improved types allow for code completion and statistic analysis.

  • preserves types between calls to useXXXQuery()/endUse()
  • adds methods findObjects()/findTuples(), which return typed Collection objects
$q = BookQuery::create();                              // BookQuery<null>
$q = $q->useAuthorQuery();                             // AuthorQuery<BookQuery<null>>
$q = $q->useEssayRelatedByFirstAuthorIdExistsQuery();  // EssayQuery<AuthorQuery<BookQuery<null>>>
$q = $q->endUse();                                     // AuthorQuery<BookQuery<null>>
$q = $q->endUse();                                     // BookQuery<null>

// findObjects() returns object collection
$o = $q->findObjects();                                // BookCollection
$a = $o->populateAuthor()                              // AuthorCollection
$a = $a->getFirst();                                   // Author|null

// findTuples() returns arrays
$a = $q->findTuples();                                 // ArrayCollection
$r = $q->getFirst();                                   // array<string, mixed>|null

Note that type propagation in endUse() requires child query classes to declare and pass on the generic parameter from their parent/base class:

/*
 * @template ParentQuery extends \Propel\Runtime\ActiveQuery\TypedModelCriteria|null = null
 * @extends BaseBookQuery<ParentQuery>
 */
class BookQuery extends BaseBookQuery

This cannot be added automatically for existing classes. While IDEs seem to figure it out without the declaration, phpstan or psalm will (correctly) see the return type as null and report errors. Add the declaration in the child class to fix it.

Generating collection classes for models can be configured in schema.xml, (see #47 for details)

Code cleanup and improved performance

These changes mostly improve internal readability/soundness of core Propel code. They mostly allow for easier and safe maintenance, but occasionally lead to performance improvements, for example when repetitive operations on strings are replaced by proper data structures.

Some notable changes:

  • columns in queries are turned to objects, which leads to more readable code and makes tons of string operations obsolete (~30-50% faster query build time, see #24)
  • fixes some confusing names (Criteria vs Criterion)
  • spreads out some "one size fits none" overloads, i.e. Criteria::map becomes Criteria::columnFilters and Criteria::updateValues

Nested filters through operators

Introduces Criteria::combineFilters()/Criteria::endCombineFilters() which build nested filter conditions:

// A=1 AND (B=2 OR C=3) AND (D=4 OR E=5)
(new Criteria())
  ->addFilter('A', 1)
  ->combineFilters()
    ->addFilter('B', 2)
    ->addOr('C', 3)
  ->endCombineFilters()
  ->combineFilters()
    ->addFilter('D', 4)
    ->addOr('E', 5)
  ->endCombineFilters()

Previously, this required to register the individual parts under an arbitrary name using Criteria::addCond() and then combining them with Criteria::combine(), possibly under another arbitrary name for further processing.

Read multiple behaviors from same repository

Propel restricts reading behaviors from repositories to one per repo. This allows to read multiple behaviors (see #25 for details).

Fixed cross-relations

Creates methods for all elements of ternary relation (Propel only uses first foreign key). Fixes naming issues and detects duplicates in model method names.

Breaking Changes

Perpl is fully backwards compatible with Propel2, with few exceptions. They mostly affect the low-level Criteria interface. Impact for regular users should be slim to none.

Setting update values and adding filters uses dedicated methods

Affects manual use of Criteria::update() - does not affect updates through Propel

Propel uses the same methods to set update values and filters. As a consequence, it requires a second Criteria to distinguish between filters and values, and a peculiar format to register update expressions:

$bookQuery = BookQuery::create()->add('id', 42);
$value = new Criteria
  ->add('title', 'foo')
  ->add('count', ['raw' => 'count + ?', 'value' => 5'], Criteria::CUSTOM_EQUAL)`);
$bookQuery->update($value);

Now dedicated methods are used to add filters or set update values/expressions, the update can be performed on a single query:

$bookQuery = BookQuery::create()
  ->addFilter('id', 42)
  ->setUpdateValue('title', 'foo')
  ->setUpdateExpression('count', 'count + ?', 5)
  ->update();

The add() method can still be used to set filter, but is deprecated in favor of addFilter(). Update values cannot be added using add().

UPDATE only affects one table per criteria

Affects manual use of Criteria::update() - does not affect updates through Propel

Propel allows to update multiple tables in the same query:

$criteria = (new Criteria())->add('a.name', 'foo')->add('b.count', 10, '>');
$values
  ->add('a.comment', 'I am foo')
  ->add('b.comment', 'I have count over 10')
  ->add('c.title', 'No title but me');
$criteria->update($values);
// runs three queries:
// UPDATE a SET comment="I am foo" WHERE name="foo";
// UPDATE b SET comment="I have count over 10" WHERE count > 10;
// UPDATE c SET title="No title but me";

This requires table names in filters to match table names in values, which is not save, considering that Propel allows multiple names and aliases but often fails to resolve them.

Now update() produces a single query and (in above example) subsequently a database error, when the columns cannot be found on the table. Usage has to be replaced with individual objects.

DELETE only affects one table per criteria

Affects manual use of Criteria::delete() or TableMap::doDelete()- does not affect updates through Propel

Same as with Criteria::update() above, delete can only affect one table per object.

// Causes exception now
$c = new Criteria();
$c->add(BookTableMap::COL_ID, $hp->getId());
$c->add(AuthorTableMap::COL_ID, $hp->getAuthorId());
$c->add(PublisherTableMap::COL_ID, $hp->getPublisherId());
BookTableMap::doDelete($c);

Use model objects ($hp->delete()) or query (BookQuery::create()->filterById($hp->getId())->delete()) to delete, one for each table.

Forcing table alias in UPDATE on Sqlite

Affects users that force table alias on a DBMS that does not allow table aliases in UPDATE

Even when forcing an alias in an update, Propel will not use it.

BookQuery::create()->setModelAlias('b', true)->...->update();

Now it does, breaking DBMSs that do not allow aliases in UPDATE (like Sqlite).

This will be resolved at some point, but for now: if you don't need it, don't force it.

Deprecated low-level API methods

Affects usage of old API methods like Criteria::get(), Criteria::put(), etc.

Obscure names, replaced functionality, unclear use-case - removing those methods is part of de-cluttering the query object interface and code. Internally, they are not used anymore.

The methods are still available, but only through magic __call(). They do not appear on the query object interface and thus are not suggested by autocomplete. Using them will trigger a deprecation warning. The method docs in src/Propel/Runtime/ActiveQuery/DeprecatedCriteriaMethods.php describe how to replace them.

A full list of deprecated methods can be found in #28. Notable mentions are Criteria::addCond() and Criteria::combine(), which are replaced by Criteria::combineFilters() (see above), and Criteria::addSelectQuery(), which is replaced by the aptly-named Criteria::addSubquery().

Changing method names of cross-relations

Affects cross-relations with parallel regular FK between target tables

Fixed naming might lead to exceptions during model:build, requesting to change relation names that cause duplicate names.

Cross-relations that were previously prefixed with "Cross" are now named correctly, meaning model methods like addCrossBook() are now replaced with the correctly named addBook().

Outlook

Some things I would like to do when I find the time:

  • Delay resolving of column names until query is created.
  • Automatically build subclasses of ObjectCollection for each model class, which provide typed entries to ObjectCollection::populateRelation() for model relations (i.e. AuthorQuery::create()->findObjects()->populateBooks()). ✔️
  • Get prepared statement parameters without building filters for QueryCache behavior.

Disclaimer

Built with care, tested, but not yet proven over time. Provided as is. Test before deployment. Let me know how it goes!

Feedback and PRs are welcome.

Thanks to Propel2!

License

MIT. See the LICENSE file for details.