GithubHelp home page GithubHelp logo

atlas.transit's Introduction

Atlas.Transit

Moves data from Atlas persistence Records and RecordSets; to domain Entities, Aggregates, and collections; and back again.

  • Creates domain Entities and Aggregates with constructor parameter values taken from fields in a source Record; updates the source Record fields from the domain Entity and Aggregate properties; refreshes Entities with autoincrement values after source inserts.

  • Creates domain collection members from Records in a RecordSet; updates Records in the RecordSet from the domain collection members.

  • Allows for common case conversion between Records and domain objects (snake_case, camelCase, PascalCase); defaults to snake_case on the Record side and camelCase on the domain side.

  • Allows for custom conversion of values between source Records and domain objects.

  • Provides select($domainClass) ... ->fetchDomain() functionality for creating domain objects fluently from Atlas.Orm queries.

Atlas.Transit depends on a number of conventions in the Domain implementation:

  • That you can build an entire Entity or Aggregate from the values in a single Record (i.e., both the Row and the Related for the Record).

  • That domain Entities and Aggregates take constructor parameters for their creation, and that the constructor parameter names are identical to their internal property names.

  • That Aggregates have their Aggregate Root (i.e., their root Entity) as their first constructor parameter.

  • That Collections use the member class name suffixed with 'Collection'. (NOTE: This is only so that Transit can find the mapper; if you want, you can specify the mapper with the @Atlas\Transit\Source\Mapper annotation.)

  • That Collections take a single constructor parameter: an array of the member objects in the collection.

  • That Collections are traversable/interable, and return the member objects when doing so.

Finally, unlike Atlas.Orm and its supporting packages, Atlas.Transit makes some light use of annotations; this is to help keep the Domain layer as free from the persistence layer as possible. Annotate your domain classes as follows to help Transit identify their purpose in the domain:

  • Entities are annotated with @Atlas\Transit\Entity
  • Collections are annotated with @Atlas\Transit\Collection
  • Aggregates are annotated with @Atlas\Transit\Aggregate
  • Value Objects are annotated with @Atlas\Transit\ValueObject

Your entity classes are presumed by default to have the same names as your persisence mapper classes. For example, a domain class named Thread automatically uses a source mapper class named Thread. If your entity class uses a different source mapper, add the fully-qualified mapper class name:

/**
 * @Atlas\Transit\Entity App\DataSource\Other\Other
 */

Example

$transit = Transit::new(
    Atlas::new('sqlite::memory:'),
    'App\\DataSource\\', // data source namespace
);

// select records from the mappers to create entities and collections
$thread = $transit
    ->select(Thread::CLASS) // the domain class
    ->where('id = ', 1)
    ->fetchDomain();

$responses = $transit
    ->select(Responses::CLASS) // the domain class
    ->where('thread_id IN ', [2, 3, 4])
    ->fetchDomain();

// do stuff to $thread and $responses

// then plan to save/update all of $responses ...
$transit->store($responses);

// ... and plan to delete $thread
$transit->discard($thread);

// finally, persist all the domain changes in Transit
$success = $transit->persist();

Value Objects

For embedded Value Objects, you need to implement the following methods in each Value Object class to move data from and back into the source Record objects. The example code is the minimum for a naive transit back-and-forth:

/**
 * @Atlas\Transit\ValueObject
 * @Atlas\Transit\Factory self::transitFactory()
 * @Atlas\Transit\Updater self::transitUpdater()
 */
class ...
{
    private static function transitFactory(object $record, string $field) : self
    {
        return new self($record->$field);
    }

    private static function transitUpdater(self $domain, object $record, string $field) : void
    {
        $record->$field = $domain->$field;
    }
}

Note that Record-backed Value Objects are going to be very tricky.

atlas.transit's People

Contributors

adamdyson avatar elgigi avatar pmjones avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

atlas.transit's Issues

[QUESTION] Events

Hi,

How manage events on domain object with Atlas.Transit ?
In Atlas.ORM, we have MapperEvents class.

For example, if i have a File domain class, when i create this object, it will be interesting to have a callback when insert performed to move a physical file...

The approach can be:

  • Domain classes implements an interface with a method "event(TransitEvent $event)"
  • Annotations

Regards.

[BUG] Undefined method

Hi,

I have some times the error :
Error: Call to undefined method Atlas\Transit\Handler\ValueObjectHandler::refreshDomain() in ...\vendor\atlas\transit\src\Handler\EntityHandler.php:177

I identify that's it's when i have a relation entity in constructor.

Thanks.

Store a Domain from another context

Hi,

In method \Atlas\Transit\Handler\EntityHandler::updateSource(), it's checked if the domain object exists in storage and if no, Transit do an INSERT to the database instead of UPDATE.
But if we save the Domain object in session for example, and store/persist in another context, Transit do an INSERT and thrown an exception for duplicate entry (because the domain doesn't not getted in the same context).

How do an update of a domain getted from another context?

Thanks.

[BUG] Expected 1 row affected, actual 0

To reproduces:
1 - I create an entity with 3 sub entities,
2 - I do changes on 1 of subentities,
3 - I persist the main entity.

An exception is thrown for all not modified entities (main or sub entities):
Atlas \ Table \ Exception: Expected 1 row affected, actual 0.

[QUESTION] X-To-Many Relationships

Hi,

I would like to know how manage X-To-Many relationships with Atlas.Transit.

Example :

  • Article: a class who represent an article (table: article)
  • Tag: a class who represent a tag (table: tag)
  • article_tag: a table to make relationship between article and tag tables, and to have multiple tags for one article

The idea would be to have a method : Article::getTags() who returns a collection of tags. When we persist the article object, the associates tags would be save in relationship table.

It's possible with the current beta? Or that's needs development?

Regards,
Ronan

[Discussion] Paginating data

Hi Paul, we briefly talked on Twitter about pagination and whether this is something Atlas or even Atlas.Trasit could/should offer.

Having integrated Atlas into a Zend Expressive application, where paginating Entities was a requirement; I definitely feel there's scope-creep, as you like to call it. So I'm reluctant at this stage to begin developing something, encase a feature like this is too project specific.

In my case, having the ability to paginate data with an Atlas.x package wouldn't have saved me much time or eased development, because of how zend-expressive-hal handles generating resources which is tied directly to a Paginator instance. See Line 54.

I've posted the relevant code below encase you're interested. I figure having a use-case might help with any architectural considerations and planning, or if this is even something you'd want under the Atlas umbrella.

Happy to discuss further and assist with development.

<?php
declare(strict_types=1);

namespace App;

use Atlas\Orm;
use Atlas\Transit;
use Zend\Expressive\Authentication;
use Zend\Expressive\Hal\Metadata\MetadataMap;
use Zend\Expressive\Hal\Metadata\RouteBasedCollectionMetadata;
use Zend\Expressive\Hal\Metadata\RouteBasedResourceMetadata;
use Zend\Hydrator\ArraySerializable;

class ConfigProvider
{
    ...

   public function getHalConfig() : array
    {
        return [
            [
                '__class__' => RouteBasedResourceMetadata::class,
                'resource_class' => Domain\Entity\User\User::class,
                'route' => 'api.user.read',
                'extractor' => ArraySerializable::class,
            ],
            [
                '__class__' => RouteBasedCollectionMetadata::class,
                'collection_class' => Domain\Entity\User\UserCollectionPaginator::class,
                'collection_relation' => 'users',
                'pagination_param_type' => 'query',
                'pagination_param' => 'offset',
                'route' => 'api.user.browse',
            ]
        ];
    }
}
<?php
declare(strict_types=1);

namespace App\Handler\User;

use App\DataSource\User\UserRepository;
use App\Handler\Traits\RestDispatchTrait;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Expressive\Hal\HalResponseFactory;
use Zend\Expressive\Hal\ResourceGenerator;

class ReadUserHandler implements RequestHandlerInterface
{
    private $userRepository;

    use RestDispatchTrait;

    public function __construct(
        ResourceGenerator $resourceGenerator,
        HalResponseFactory $responseFactory,
        UserRepository $userRepository
    ) {
        $this->resourceGenerator = $resourceGenerator;
        $this->responseFactory = $responseFactory;
        $this->userRepository = $userRepository;
    }

    public function get(ServerRequestInterface $request) : ResponseInterface
    {
        $id = (int) $request->getAttribute('id', false);

        return $this->createResponse($request, $this->userRepository->fetchUserById($id));
    }
}
<?php
declare(strict_types=1);

namespace App\Handler\User;

use App\DataSource\User\UserRepository;
use App\Handler\Traits\RestDispatchTrait;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Expressive\Hal\HalResponseFactory;
use Zend\Expressive\Hal\ResourceGenerator;

class BrowseUserHandler implements RequestHandlerInterface
{
    private $userRepository;

    use RestDispatchTrait;

    public function __construct(
        ResourceGenerator $resourceGenerator,
        HalResponseFactory $responseFactory,
        UserRepository $userRepository
    ) {
        $this->resourceGenerator = $resourceGenerator;
        $this->responseFactory = $responseFactory;
        $this->userRepository = $userRepository;
    }

    public function get(ServerRequestInterface $request) : ResponseInterface
    {
        $offset = (int) $request->getQueryParams()['offset'] ?? 1;
        $limit = (int) $request->getQueryParams()['limit'] ?? 25;

        return $this->createResponse($request, $this->userRepository->browseUsers($offset, $limit));
    }
}
<?php
declare(strict_types=1);

namespace App\DataSource\User;

use App\Atlas\AtlasPaginatorAdapter;
use App\Domain\Entity\User\User;
use App\Domain\Entity\User\UserCollection;
use App\Domain\Entity\User\UserCollectionPaginator;
use Atlas\Transit\Transit;

class UserRepository
{
    protected $transit;

    public function __construct(Transit $transit)
    {
        $this->transit = $transit;
    }

    public function fetchUserById(int $id)
    {
        return $this->transit
            ->select(User::class)
            ->where('id = ', $id)
            ->fetchDomain();
    }

    public function browseUsers(int $offset = 1, int $limit = 25, array $whereEquals = [])
    {
        $select = $this->transit->select(UserCollection::class, $whereEquals);
        $adapter = new AtlasPaginatorAdapter($select);
        $paginator = new UserCollectionPaginator($adapter);
        $paginator->setCurrentPageNumber($offset);
        $paginator->setItemCountPerPage($limit);

        return $paginator;
    }
}
<?php
declare(strict_types=1);

namespace App\Atlas;

use Atlas\Transit\TransitSelect;
use Zend\Paginator\Adapter\AdapterInterface;

class AtlasPaginatorAdapter implements AdapterInterface
{
    protected $select;

    public function __construct(TransitSelect $select)
    {
        $this->select = $select;
    }

    public function getItems($offset, $limit)
    {
        $items = $this->select
            ->offset((int) $offset)
            ->limit((int) $limit)
            ->fetchDomain();

        return $items;
    }

    public function count()
    {
        return $this->select->fetchCount();
    }
}
<?php
declare(strict_types=1);

namespace App\Domain\Entity\User;

use Zend\Paginator\Paginator;

class UserCollectionPaginator extends Paginator
{

}

[BUG] Boolean value

Hi,

In MySQL the boolean value is similar to TINYINT (0: false / 1: true).

In my Domain class, i give in constructor parameters the bool type value, it's work fine when i select a domain. The integer is convert to boolean.

But when i persist the Domain class:

  1. The value taken and given to the mapper is boolean value
  2. The Mapper found that the value is different (boolean <> int)
  3. The ORM execute the UPDATE
  4. The ORM throw an exception because PDO return no updated entry (because boolean converted to integer)

Ronan

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.