Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Perpl Laravel Package

perplorm/perpl

Perpl is a lightweight ORM for Laravel and PHP that simplifies database access with an expressive, intuitive API. Define models, relationships, and queries in clean PHP, with seamless Laravel integration and minimal configuration.

Deep Wiki
Context7

Getting Started

Perpl is a maintained fork of Propel2: a schema-first, code-generating ORM. You describe tables in XML, generate PHP model and query classes, then use them as ActiveRecord objects and ModelCriteria query objects. It does not plug into Eloquent, config/database.php, or artisan migrate, so treat it as a second persistence layer alongside Laravel, not a drop-in replacement.

1. Install and verify the fork's shape

composer require perplorm/perpl
composer show perplorm/perpl
ls vendor/bin          # binary may be `propel` or `perpl`
ls vendor/perplorm/perpl/src

Before writing imports, confirm two fork-specific details:

  • The runtime namespace: the fork may keep Propel\Runtime\... for compatibility or expose Perpl\Runtime\....
  • The console binary name: vendor/bin/propel vs vendor/bin/perpl.

Also check that composer.json does not declare replace: propel/propel while you still have propel/propel installed—running both is a common source of class collisions.

2. Create the runtime config

Copy the package's distributed config (check the package root or resources/ for propel.dist.php / propel.php.dist) to your project root as propel.php. The shape is Propel2-style:

<?php
return [
    'propel' => [
        'database' => [
            'connections' => [
                'default' => [
                    'adapter'    => 'mysql',
                    'dsn'        => 'mysql:host=127.0.0.1;dbname=app;charset=utf8mb4',
                    'user'       => getenv('DB_USER'),
                    'password'   => getenv('DB_PASSWORD'),
                    'attributes' => [],
                ],
            ],
        ],
        'runtime' => [
            'defaultConnection' => 'default',
            'connections'       => ['default'],
        ],
        'generator' => [
            'defaultConnection' => 'default',
            'connections'       => ['default'],
        ],
        'paths' => [
            'schemaDir'        => 'schema',
            'phpDir'           => 'generated-classes',
            'phpConfDir'       => 'generated-conf',
            'sqlDir'           => 'generated-sql',
            'migrationDir'     => 'generated-migrations',
        ],
    ],
];

Keep this file out of config/ unless you deliberately want Laravel's config() helper; Perpl expects a plain PHP array, and env() inside it only works after Laravel's bootstrap has loaded.

3. Write a first schema

<!-- schema/schema.xml -->
<database name="default" defaultIdMethod="native" namespace="App\Model">
  <table name="book">
    <column name="id" type="INTEGER" primaryKey="true" autoIncrement="true"/>
    <column name="title" type="VARCHAR" size="255" required="true"/>
    <column name="author_id" type="INTEGER"/>
    <foreign-key foreignTable="author" name="book_author">
      <reference local="author_id" foreign="id"/>
    </foreign-key>
  </table>
</database>

4. Generate and autoload

vendor/bin/propel model:build
vendor/bin/propel sql:build
vendor/bin/propel sql:insert     # convenience for a fresh dev DB
composer dump-autoload -o

Register the generated directory in composer.json if the generator does not already do it:

"autoload": {
  "classmap": ["generated-classes/"]
}

5. Bootstrap and first use case

require __DIR__.'/vendor/autoload.php';

\Propel\Runtime\Propel::init(__DIR__.'/propel.php');

use App\Model\Book;
use App\Model\BookQuery;

$book = new Book();
$book->setTitle('High-Performance PHP');
$book->save();

$books = BookQuery::create()
    ->filterByTitle('%PHP%', \Propel\Runtime\ActiveQuery\Criteria::LIKE)
    ->orderByTitle()
    ->find();

In Laravel, put the Propel::init() call in a service provider's register() or boot() and resolve connections through a small repository/service rather than calling static runtime classes from controllers.

Implementation Patterns

Query objects as repositories. Generated *Query classes are the main day-to-day API. Compose filters rather than dropping to raw SQL:

$books = BookQuery::create()
    ->filterByTitle('%orm%', Criteria::LIKE)
    ->filterByAuthorId($authorId)
    ->orderByCreatedAt(Criteria::DESC)
    ->limit(50)
    ->find();

Use findOne(), findPk($id), findOneOrCreate(), count(), and exists() for the common cases.

Relations and joins. Use joinWith() and useXQuery() to avoid N+1 queries when serializing collections:

$books = BookQuery::create()
    ->joinWith('Book.Author')
    ->useAuthorQuery()
        ->filterByActive(true)
    ->endUse()
    ->find();

Generated relation accessors ($book->getAuthor(), $author->getBooks()) are convenient but lazy-load. For API responses, prefer joined queries plus an explicit formatter.

Pagination. paginate($page, $perPage) returns a pager with getResults(), getNbResults(), getLastPage(), and haveToPaginate(). This maps cleanly onto Laravel pagination resources if you adapt the pager fields.

Hydration and serialization. find() returns model collections with toArray() / toJSON(). For read-only endpoints, use an array formatter to skip object hydration:

$rows = BookQuery::create()
    ->setFormatter(\Propel\Runtime\Formatter\ArrayFormatter::class)
    ->find();

Be aware that toArray() on models can trigger lazy relation loads and can expose columns you did not intend to return.

Transactions. Wrap multi-object writes explicitly; each save() outside a transaction is effectively its own unit of work:

$con = \Propel\Runtime\Propel::getConnection('default');
$con->beginTransaction();
try {
    $book->save($con);
    $author->save($con);
    $con->commit();
} catch (\Throwable $e) {
    $con->rollBack();
    throw $e;
}

Bulk operations. Use query-level updates/deletes for batch jobs instead of iterating models:

BookQuery::create()
    ->filterByStatus('draft')
    ->update(['Status' => 'archived']);

Behaviors for cross-cutting columns. Timestamps, slugs, soft deletes, and versioning are schema behaviors, not Laravel traits:

<behavior name="timestampable"/>
<behavior name="sluggable">
  <parameter name="slug_column" value="slug"/>
  <parameter name="slug_pattern" value="{Title}"/>
</behavior>

After any schema or behavior change, rerun model:build and composer dump-autoload. The generated filterBy* methods are compiled from the schema, so stale classes silently lack new filters.

Migrations workflow. Pick one strategy and stick to it:

  • Development: sql:build + sql:insert for a clean schema.
  • Production: migration:diff, review the generated SQL, then migration:migrate.
  • Existing database: database:reverse to generate schema XML, then model:build.

Do not mix sql:insert and migrations on the same database; they track state differently.

Laravel integration pattern. Keep Perpl behind repositories/services:

final class BookRepository
{
    public function findActive(): array
    {
        return BookQuery::create()
            ->filterByActive(true)
            ->find()
            ->toArray();
    }
}

This isolates the fork, makes testing easier, and gives you a migration path to Doctrine or Eloquent if the package stalls. Perpl connections are separate PDO connections; they do not participate in Laravel's DB::transaction().

Gotchas and Tips

Verify fork identity before adoption. This is a small fork (26 stars, low opportunity score in your evaluation). Confirm the Packagist release you are installing actually maps to a tagged commit in the repository, inspect the changelog for fork-specific breaking changes, and pin an exact version in composer.json. Prefer "perplorm/perpl": "x.y.z" over a floating constraint.

Namespace and binary drift are the first failure points. If imports like Propel\Runtime\Propel fail after install, check the fork's src/ namespace and generated base classes. The generator may emit base classes extending a renamed runtime class. Adjust your bootstrap and service provider accordingly rather than assuming Propel2 documentation applies verbatim.

Generated code must be rebuilt in deploy. Add propel model:build and composer dump-autoload -o to your deployment pipeline, and clear OPcache after deploying new generated classes. A schema change without a rebuild produces "method does not exist" errors on filterBy* calls.

Do not hand-edit Base* classes. They are regenerated. Put custom methods in the non-base model class, or better, in services/repositories and custom query subclasses. Confirm your fork's overwrite behavior for the non-base stub before putting business logic there.

Long-running processes and Octane. Propel keeps an identity map/session and open PDO connections. In queue workers and Octane, clear or reset the session between jobs and close connections when idle if you see stale data or "server has gone away" errors. At minimum, call Propel::close() in a terminating hook and verify against the fork's API.

filterBy* null semantics. filterByAuthorId(null) does not always mean what you expect depending on the argument signature. Use explicit criteria (Criteria::ISNULL, Criteria::ISNOTNULL) for nullable columns, and remember that LIKE does not add % wildcards for you.

Debugging queries. Call ->toString() on a ModelCriteria to inspect SQL before execution. Attach a PSR-3 logger via the runtime's logger hook and wrap Laravel's Log so query logs land in your normal channels. In tests, assert on generated SQL when behavior is surprising.

Test databases. SQLite in-memory is fast but platform-specific SQL from sql:build/migrations may not run there. Generate SQL for the same platform you test against, or run integration tests on MySQL/PostgreSQL in CI. Migration files generated for MySQL will often fail on SQLite.

Config quirks. propel.php returns an array and is not Laravel config. If you need environment values, read them with getenv() or build the array in a service provider and pass it to Propel::init() via a generated file. Avoid committing credentials; keep the file out of version control or template it during deploy.

Extension points. Custom behaviors, custom ModelCriteria subclasses, and custom formatters are the stable extension surfaces. Prefer them over patching generated code or the vendor package. If you need hooks around save/delete, check whether the behavior system covers it before adding event dispatchers on top.

Have an exit plan. Because the fork's community is small, keep Perpl usage concentrated in a repository layer and avoid leaking generated model objects into controllers, jobs, and Blade templates. That boundary is what makes a future swap to Eloquent or Doctrine tractable.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons