florianv / petrinet

:traffic_light: Petrinet framework for PHP
http://florianv.github.io/petrinet
MIT License
124 stars 14 forks source link

PDO Storage Implementation #19

Closed mdwheele closed 10 years ago

mdwheele commented 10 years ago

Please excuse if I missed it somewhere, but I've looked around the repo for a PDO / Database loader for the model and could not find anything. I'm starting a project using the library and will be storing user-configurable workflows locally and the question I'm asking myself is whether or not it'd be useful to add a within-scope storage implementation that others could use or just slap something together for my own uses.

If you are open to a database storage implementation, I would be happy to take a first stab and get feedback from you. Either way, I've got to do the work of storing the nets in a database!

If this doesn't already exist, any thoughts or notes you have towards the implementation would be great! (i.e. how you'd want to organize migrations, design ideas, etc). It looks to me to be a pretty straight-forward implementation of LoaderInterface and the addition of an interface to store. Typically, most storage implementations I've seen provide an interface that has a contract for serializing + deserializing, which is why I ask you! "LoaderInterface" may not be the most expressive nomenclature after adding contracts for saving / serializing.

Anywho, I would be glad to hear your thoughts. I love the work put into this so far. It looks great!

florianv commented 10 years ago

Hello, no you didn't miss anything, there is no storage layer yet.

I was thinking that Doctrine ORM could be used. Most models (place, transition, petrinet..) are plain PHP object, so they can be easily persisted by Doctrine by providing some mapping files. The main advantage of Doctrine is supporting many storage systems out of the box, without writing any query. Doctrine ODM could also be used to persist big Petrinets in MongoDB.

With the different events triggered during the execution process, it is possible to create listeners to update the entities in the database. For example the AFTER_TRANSITION_FIRE event can be used to update the marking.

Thanks for your feedback.

mdwheele commented 10 years ago

Excellent!

Is this work you already have planned for the near future? If you don't have time at the moment, I'd be happy to start work towards implementation of a storage layer using Doctrine ORM with your involvement. I don't want to step on any toes! :smiley:

Thanks.

florianv commented 10 years ago

No, I didn't plan to work on it soon. Ok thanks :)

mdwheele commented 10 years ago

With the different events triggered during the execution process, it is possible to create listeners to update the entities in the database. For example the AFTER_TRANSITION_FIRE event can be used to update the marking.

I think the events and listeners will serve well to keep the Petrinets up-to-date as you say. However, I'm not sure if the storage implementation should be aware of the events. That seems like either something that could be documented as, "Hey, it's a good idea to wire these events up to persist the object using the storage implementation." OR be implemented by a domain service long-term.

My initial thoughts are to solve only the storage problem and implement additional wiring on top of that after the fact. So, I'm thinking that storage options would implement the following interface:

interface PetrinetRepositoryInterface {
    /**
    * Loads a Petrinet.
    *
    * @param mixed $resource The resource to load
    *
    * @return \Petrinet\PetrinetInterface The Petrinet
    */
    public function load($resource);

    /**
    * Persist a Petrinet.
    *
    * @param mixed $resource The resource to load
    *
    * @return \Petrinet\PetrinetInterface The Petrinet
    */
    public function save(PetrinetInterface $petrinet);
}

This would mean the XML loader would transition to this repository architecture as well as possibly the Graphvis dumper. My only additional thoughts on type-hinting PetrinetInterface on the save method is that Doctrine2 ORM may be finicky because it's expecting a concrete implementation of an Entity with member variables, setters, and getters; I'm not sure though, can give it a go.

Another thought is to separate into two interfaces (similar to what you have now; one for saving and loading) and then have an abstract base repository implement those two and extend storage implementations off the abstract base.

Seems like splitting hairs, haha.

I'm going to start wiring things up and see if it feels right.

florianv commented 10 years ago

Hello, I would use the terminology Manager instead of Repository (which is specific to Doctrine ORM).

<?php

interface PlaceManagerInterface
{
    public function update(PlaceInterface $interface);
}

I would provide an event subscriber whose role is to update the marking after a token is removed from a place or after a token is added to a place. People can register it to the engine or not (if they want simulation only they don't register the listener).

<?php

use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class UpdatingListener implements EventSubscriberInterface
{
    private $placeManager;

    public function __construct(PlaceManagerInterface $placeManager)
    {
        $this->placeManager = $placeManager;
    }

    static public function getSubscribedEvents()
    {
        return array(
            'after.token.insert' => array('onAfterTokenInsert', 10),
            'after.token.consume' => array('onAfterTokenConsume', 10),
        );
    }

    public function onAfterTokenConsume(PlaceEvent $event)
    {
        $this->placeManager->update($event->getPlace();
    }

    public function onAfterTokenInsert(PlaceEvent $event)
    {
       $this->placeManager->update($event->getPlace());
    }
}
mdwheele commented 10 years ago

I think we're coming at this from two angles, which is great, haha.

Your comment handles updates related to engine events while an already-defined Petrinet is running in the engine and is already assumed to have its definition in storage. There is still the problem of handling how the definition itself is stored, modified, and removed. The way I see it, we have at least 2 choices:

The overall use case for me is going to be a workflow management interface where administrators can add, modify, and remove workflows, hooking application-specific event subscribers up to different parts of the workflow. The current event dispatcher will work well towards hooking app event subscribers into the engine over the interface you've proposed but I'm not sure it's entirely appropriate for the story of "add, modifying, and removing workflows". Honestly, that's probably more a call for you to make! :smile:

mdwheele commented 10 years ago

Workflow management is a topic for another day, but that's another good question for the future of this package. Do you think this package should remain as close as possible to only providing services to simulate a Petrinet, or do you anticipate eventually including some of the more Workflow-oriented user stories (timed triggers, automatic triggers, asserted guards, etc). I'd personally like to see it stay as close to "Petrinet-only" with an eventual separate package implementing the requirements of an activity-based workflow, if only for clarity.

florianv commented 10 years ago

Intitally this library was only a library to create, execute and visualize (basic) Petrinets. I did it with the idea of creating a workflow engine but I didn't have the time to do it when I pushed the library. I agree with adding a persistence layer to store Petrinet definitions, but yes Workflow related things should be done in an other library built on top of this one (which is purely a Petrinet library).

Petrinets can be created using the PetrinetBuilder class or manually by creating all objects and then persisted using the PetrinetRepository class you mentionned. I don't think the definition will be modified at runtime so yes the listeners won't be helpful in this case.

The token disposition is stored in a separate table for each workflow case. To execute the workflow: the definition is loaded from the database, then the token disposition for the workflow case. The Petrinet is marked with the tokens for this case, and then executed by the engine. Event listeners can be used to update the new token disposition for this workflow case.

florianv commented 10 years ago

Ps: You might be interested by this discussion https://github.com/florianv/petrinet/pull/18#issuecomment-32096762 . Gilles is also interested to build a workflow engine using Petrinets.

florianv commented 10 years ago

This has been fixed in the latest version with an example about how to map the model classes using Doctrine2 ORM.