Skip to content

ORM redesign #1074

@brendt

Description

@brendt

There are five viable options:

Keep our current approach

final class Book implements DatabaseModel
{
    use IsDatabaseModel;

    public string $title;

    public ?DateTimeImmutable $publishedAt = null;

    /** @var Collection<Chapter> */
    public Collection $chapters;

    public Author $author;
}

// Migrations stay the same

$books = Book::query()
    ->where('COUNT(chapters) > 10')
    ->where('published_at IS NOT NULL')
    ->orderByDesc('published_at')
    ->with('author')
    ->limit(10)
    ->get();

Pros:

  • We already have it
  • Inspired by Eloquent, so it feels very natural to a large group of PHP developers

Cons:

  • I really don't like the tight coupling
  • Query/object mapping is really complex and a mess

Doctrine

Pros:

  • Existing, battle-tested ORM
  • Also well known with the PHP community

Cons:

  • Doctrine is pretty verbose, you need a lot of attributes and handholding to make it work
  • It's a very classic approach to ORMs, not a super big con, but kind of in contrast with Tempest's philosophy

Mongo

Pros:

  • A totally different way of thinking about ORMs, could be a "breath of fresh air"
  • Andreas, who works at Mongo, is interested in collaborating if we go this route, might be an asset

Cons:

  • Document databases are very different from relational ones
  • Mongo isn't available by default on many servers and local installs, which I feel is still pretty important for many PHP developers.
  • Higher entry barrier because of the two points above

LINQ-inspired

final class Book
{
    public Id $id;

    public string $title;

    public ?DateTimeImmutable $publishedAt = null;

    /** @var Collection<Chapter> */
    public Collection $chapters;

    public Author $author;
}

// Migrations would be exactly the same as now

$books = ModelQuery::for(Book::class)
    ->where(fn (Book $book) => $book->chapters->count() > 10)
    ->where(fn (Book $book) => $book->publishedAt !== null)
    ->orderByDesc(fn (Book $book) => $book->publishedAt)
    ->load(fn (Book $book) => $book->author)
    ->groupBy(fn (Book $book) => $book->author)
    ->limit(10)
    ->get();

Pros:

  • No more trait or interface on the model
  • About ModelQuery: there could easily also be a variant like "EntityManager" that's injected for people who don't like using service location (internally, ModelQuery would have to reach within the container to resolve the database dependency)
  • The coolest thing is the query builder, where we move away from strings, and instead use the object itself. You'd get autocompletion anywhere. We would have to jump through a LOT OF HOOPS to get this to work though 😅 Because none of the above closures are actually run! They are used as a template to build the query, which is executed when calling get (inspired by LINQ)

Cons:

  • We'd have to parse the query statements manually. We're essentially building another language within PHP

Manual mapping

$query = new Query(<<<SQL
SELECT * 
FROM books 
    INNER JOIN chapters ON chapters.book_id = books.id
    INNER JOIN authors ON books.author_id = authors.id
WHERE book.published_at IS NOT NULL
HAVING COUNT(chapters) > 10
SQL);

$book = map($query)->collection()->to(Book::class);

Pros:

  • It's easy
  • We don't have to reinvent a language between SQL and PHP, we just use SQL
  • Users could easily add a layer on top of the mapper without any framework-defined abstractions (eg. repository classes)

Cons:

  • Mapping between relational tables and objects isn't trivial, forcing people to do it themselves can become quite cumbersome, trying to do it for them (as with the above example) is currently not possible, and very complex
  • People have gotten so used to ORMs that they don't know SQL anymore…

Metadata

Metadata

Assignees

No one assigned

    Labels

    DatabaseLiveBrent's filter for livestreams

    Type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions