Skip to content

Instantly share code, notes, and snippets.

@guiwoda
Last active December 11, 2016 20:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save guiwoda/a8aa5d402a80e8641d994dbb1448cdfc to your computer and use it in GitHub Desktop.
Save guiwoda/a8aa5d402a80e8641d994dbb1448cdfc to your computer and use it in GitHub Desktop.
Brainstorming ORM

Wishlist

  • (Un)serialize from/to any source. Build an abstraction (Map) that could be implemented for SQL, NoSQL (Key-Value, Document...), JSON APIs, or anything that needs to build objects from scalar data.
  • Default to a table per entity, but allow views and multiple models from various data sources, even the ability to connect them (through events?).
// Write model: event sourcing?
class BookPublished(string $title, string $description, Author[] $authors, ...){}
class BookReviewed(Book $aBook, User $reviewer, string $review, int $rating){}

// Read model
class Book(string $title, string $description, Author[] $authors, Review[] $reviews){}
  • Reduce config duplication: mapping and state access should be configured once and used through the same objects.
  • The only difference between VOs and Entities should be how to determine identity (could be that Entities hold an Identity VO).
  • Iterable collections (relations, queries) and entity access (repositories) should share implementation.
  • Allow relations to abstract classes for both Entities and VOs (persistent polymorfism).
  • Immutability: Entities could be immutable and compared by identity + state. Comparisons could be provided as a trait.
  $data = Data\Map::fromArray([
      'id'          => new Identity(Book::class), // Identity VO provided by us?
      'title'       => 'Lord of the Rings: Fellowship of the Rings',
      'description' => 'The first book in the Lord of the Rings trilogy.',
      'authors'     => [$tolkien],
  ]);
  
  $aBook   = new Book($data);
  $another = new Book($data);
  // Immutable book would return a new instance of itself.
  $modified = $aBook->addReview(new Review(/*...*/));
  
  assert($aBook->equals($another));            // Both are equal, same identity, same state
  assert($aBook->equals($modified) === false); // Not equal anymore (different state)
  assert($aBook->is($modified));               // But still the same identity
  • Objects could be mapped to multiple sources. Implementing a L2 cache for a certain group of entities should be something like mapping the entity to the database, then also mapping it to a Key-Value store. Sync should be done in the library, maybe even configurable if using queues. Also useful for search engines. This would require multiple mapping points for each entity, which wouldn't be possible with a single map($builder) method on each entity as shown in the examples.
  • Maybe use factories for each object + data source need?

Repositories could behave as object Sets. Different data structures could be provided if the need arises: binary search for ordered collections that need in-memory searching capabilities, for example.

Repositories could also help with indexing definitions: if I add a method that will do lookups on a certain column, I could use that information to create an index in an SQL database. It could also offer out-of-the-box implementations of certain semantic lookups.

// Repository, Collection... would probably come from some sort of registry representing a database
$repo = new Repository;

$repo->findContainingTitle($aQuery); // index title column, wildcard lookup
$repo->getBySlug($aSlug);            // index slug column,  exact match lookup
$repo->findStartingWithFoo();

$repo->"{$fieldname}"
        startsWith($aQuery)
        endsWith($aQuery)
        has($aQuery)/contains($aQuery)
        is($aValue)/matches($aValue)/equals($aValue)
        isNot($aValue)
        greaterThan($aValue)
        greaterOrEqualThan($aValue)
        lessThan($aValue)
        lessOrEqualThan($aValue)
        between($aValue, $anotherValue);


// example
$repo
    // Magic method matches "StartsWith", takes first block as field name
    // in SQL: 'WHERE title LIKE "Star Wars%"
    ->titleStartsWith('Star Wars')
    // Magic method matches both "and" and "IsGreaterThan", rest of the name is field
    // in SQL: AND rating > 7
    ->andRatingIsGreaterThan(7);
    // Resulting Repo would be (for a movies table)
    // SELECT * FROM movies WHERE title LIKE "Star Wars%" AND rating > 7

All repositories should be able to accumulate conditions and should be iterable and immutable objects. This would allow filtering and traversing object lists without intermediate structures. All traditional collection method should be part of their API:

$repository->filter(function($candidate){
      // This could turn into 'WHERE fieldname = :value' 
      return $candidate->field('fieldname')->equals($aValue);
      // return $candidate->fieldname->equals($aValue);
  })
  ->map(function($candidate){
      // Map could be accumulated and processed when iterated though generators
      // But after map, another filter would be strange...
      return $candidate->field('fieldname')->value() + 1;
  });

Persistent context

The library could offer Decorator objects that act as middleware for a persistence context.

class Transactional 
{
    private $next;
    
    public function __construct(callable $next){ $this->next = $next; }
    
    public function __invoke(...$args)
    {
        $this->somehow->beginTransaction();
        
        try {
            $result = $this->next(...$args);
            $this->somehow->commit();
            
            return $result;
        } catch (Throwable $up) {
            $this->somehow->rollBack();
            
            throw $up;
        }
    }
}

class AutoPersist
{
    private $next;
    
    public function __construct(callable $next){ $this->next = $next; }
    
    public function __invoke(...$args)
    {
        $result = $this->next(...$args);
        
        $this->somehow->flush();
        
        return $result;
    }
}

$domainRequest = new AutoPersist(new Transactional(function(Repository $books, Request $request){
    $aBook = new Book(Data\Map::fromArray($request->only(['title', 'description']));
    
    $books->add($aBook);
    return $aBook;
}));

$domainRequest(new BookRepository, Request::fromGlobals());

We should strive to reduce the need for Service objects that add boilerplate to each domain call.

<?php
declare(strict_types = 1);
namespace Ts\examples;
use Ts\Orm\Data\Map;
class Book
{
private $data;
public function __construct(Map $data)
{
// Somehow associate a generic dictionary with this entity. This would assume mapping was preconfigured
// somewhere else, and in here we tell this generic dict that it's specific to this Entity / VO.
// At this point, the lib would assert all data constraints: types, required fields, foreign keys, etc.
$this->data = $data->toPersistentMap(self::class);
// This is another interesting approach.
// Mapping could be read only by instantiating an entity, but could be cached after first call.
// Schema would need to construct mapped objects, but that could be done with some reflection + dummy
// objects on schema tools, without polluting domain classes.
$this->data = $data->toPersistentMap(self::class, function($builder){
$builder->increments('id');
$builder->string('title');
$builder->text('description')->optional();
$builder->hasMany(Author::class);
$builder->hasMany(Review::class);
});
// Constraints-oriented?
$this->data = $data->withConstraints(function(ConstraintFactory $constraints){
return [
'id' => $constraints->identity()->autoincremental(),
'title' => $constraints->string(),
'description' => $constraints->string()->optional(),
'authors' => $constraints->hasMany(Author::class)->min(1)->max(4),
'reviews' => $constraints->hasMany(Review::class),
];
});
// Could be rewritten as:
$this->data = $data->toPersistentMap(self::class, [self::class, 'map']);
// Or the map static method could be a convention/configuration
$this->data = $data->toPersistentMap(self::class);
}
// If we want to allow static mapping analysis (eg.: schema tools)
public static function map($builder)
{
$builder->increments('id');
$builder->string('title');
$builder->text('description')->optional();
$builder->hasMany(Author::class);
$builder->hasMany(Review::class);
}
public function authors()
{
// This would navigate a preconfigured relation
return $this->data->authors;
// return $this->data->get('authors'); // if you don't like magic props
}
public function reviews(int $amount = 5)
{
// Each method would return a collection type object.
// Ideally, this wouldn't query the relation until iterated.
// Also, collection objects would be used as repositories too, so same API
// for relations and "whole table" querying
return $this->data->reviews->orderBy('createdAt')->descending()->take($amount);
}
}
// Entities / VOs that hold their mapping info themselves could implement a common interface, so detecting them is easier
interface Mapeable
{
public static function map($builder);
}
// Then, somewhere in the mapping lib:
function toPersistentMap($className, callable $mapper = null) {
if ($mapper === null) {
$refl = new ReflectionClass($className);
if (! $refl->implementsInterface(Mapeable::class)) {
throw new YouShouldMapThisObjectBeforeCallingMeException;
}
$mapper = [$className, 'map'];
}
}
// NOTES:
// ------------------
// // The `Map` looks like a `DAO` from J2EE patterns. Could be misused if the domain is not modeled and
// data access goes straight to the database.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment