SitePoint Sponsor

User Tag List

Page 1 of 2 12 LastLast
Results 1 to 25 of 49

Hybrid View

  1. #1
    SitePoint Enthusiast
    Join Date
    Feb 2005
    Posts
    27
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)

    New PHP Data Mapper Library

    Been playing around recently with a PHP 5.3 data mapper library, would be interested to hear any feedback.

    http://github.com/lox/pheasant

    Still very early days, relationship support is still in proof-of-concept phase, but I'd be interested to hear what you guys think of the usage examples.

    I'm well aware that there are two other excellent PHP 5.3 ORMs, both very similar to their Ruby counterparts (and both excellently engineered).

    Basically where mine differs is in that I've tried to keep simple the declarations (which are in PHP), the database dependancy (which is Mysql and Innodb specific) and a build for a very light memory footprint.

  2. #2
    ********* Victim lastcraft's Avatar
    Join Date
    Apr 2003
    Location
    London
    Posts
    2,423
    Mentioned
    2 Post(s)
    Tagged
    0 Thread(s)
    Hi...

    It looks more like ActiveRecord to me. You have each persistent object extending DomainObject and you have to call save() on each one. Or have I missed something?

    yours, Marcus
    Marcus Baker
    Testing: SimpleTest, Cgreen, Fakemail
    Other: Phemto dependency injector
    Books: PHP in Action, 97 things

  3. #3
    SitePoint Enthusiast
    Join Date
    Feb 2005
    Posts
    27
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Basically the save() methods are syntactic sugar that delegate to the registered mapper.

    Whether that makes it activerecord, I'm not sure.

  4. #4
    SitePoint Wizard silver trophy kyberfabrikken's Avatar
    Join Date
    Jun 2004
    Location
    Copenhagen, Denmark
    Posts
    6,157
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Having the domain objects extend from a common base class pretty much disqualifies it as an object mapper.

  5. #5
    SitePoint Zealot
    Join Date
    Feb 2009
    Location
    Bristol
    Posts
    116
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by kyberfabrikken View Post
    Having the domain objects extend from a common base class pretty much disqualifies it as an object mapper.
    Could you elaborate on that please?

    Quote Originally Posted by TomB;
    $user = $userMapper->findById(1);
    echo $user->orders[0]->items[0]->product->manufacturer->name
    Forgive me for being dense Tom - but how would you achieve this?
    Do you load the entire object graph on the initial query?
    Are you using some sort of magic method (i.e. __get) to instantiate mappers when data that points to relations is requested?

    Chaining (as I understand it) would involve returning an object of some sort - be it the original root object, or I guess you're drilling down to each object in some way?

  6. #6
    SitePoint Guru bronze trophy TomB's Avatar
    Join Date
    Oct 2005
    Location
    Milton Keynes, UK
    Posts
    988
    Mentioned
    9 Post(s)
    Tagged
    2 Thread(s)
    Quote Originally Posted by sunwukung View Post
    Forgive me for being dense Tom - but how would you achieve this?
    Do you load the entire object graph on the initial query?
    Are you using some sort of magic method (i.e. __get) to instantiate mappers when data that points to relations is requested?

    Chaining (as I understand it) would involve returning an object of some sort - be it the original root object, or I guess you're drilling down to each object in some way?
    I posted a full code solution :P

    Yes it uses __get.

    Essentially:

    -Each object knows of the mapper it was created by.
    -Each mapper knows how it is related to other mappers
    -When you call __get it queries the related (via its own) mapper to find the related record(s).
    -As each object is created, it is passed the mapper it was created by. This allows for unlimited chaining


    It also means any object, at any level of the chain can be initiated through its mapper and used as the root object.

  7. #7
    SitePoint Enthusiast
    Join Date
    Feb 2005
    Posts
    27
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Pheasant uses __get to dynamically traverse relationships. I assume TomB is doing something similar.
    Last edited by Lachlan; Jul 13, 2010 at 17:05. Reason: Ooops! Missed Tom's response!

  8. #8
    SitePoint Enthusiast
    Join Date
    Feb 2005
    Posts
    27
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Not sure I entirely agree, I see the core of the data mapper pattern as separating the persistence into separate mapper objects which can be interchanged to support different data layers. I agree that it's somewhere in the middle, not sure it particularly matters. As soon as there is trait support I'll hopefully be able to move to that.

    It seemed like a reasonable middle ground to me. Any particular problems you see with the base class approach?

  9. #9
    ********* Victim lastcraft's Avatar
    Join Date
    Apr 2003
    Location
    London
    Posts
    2,423
    Mentioned
    2 Post(s)
    Tagged
    0 Thread(s)
    Hi...

    It makes the domain code depend on the persistence layer. This is very common for small web projects, and works fine in these cases. If you are building a complex domain model you have enough problems to worry about without mixing in the DB behaviour.

    A data mapper proper just reads the data out of a normal domain object. The domain object then knows nothing about the persistence layer, although it is obliged to make enough of itself public that the mapper can extract the data. Means you can test the model without the persistence at all.

    Building a full on data mapper is a huge task. Usually smaller ones are build customised for a specific project. As these are often proprietary as well, you don't see them in the open too much.

    You may have a mapper internally, but I wouldn't describe your library as such. Looks neat though.

    Why do you have that Singleton in your code? Whether or not the main persistence class is a Singleton will be up to the application developer, no? Are you telling me how to do my job? Hmph. You come in here...just 'cos your better looking doesn't give you the right...I mean I've been working on this project for years...young upstart, telling us what should be a Singleton when we know the code...I mean we know our dependencies and we can handle it...library coders telling us what to do when they no nothing about our problems...messing up our tests...I mean...

    Ahem...sorry, Er, forgot what I was saying. Oh, yeah. No Singletons please. I might want to do something else like DI.

    yours, Marcus
    Marcus Baker
    Testing: SimpleTest, Cgreen, Fakemail
    Other: Phemto dependency injector
    Books: PHP in Action, 97 things

  10. #10
    SitePoint Enthusiast
    Join Date
    Feb 2005
    Posts
    27
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Enlighten a newbie Marcus, which part is a singleton particularly? Any suggestions on how I could improve that particular part to make it more DI friendly?

    Also, I agree with TomB, I don't know it's so bad a thing that the object knows it CAN be persisted, it just shouldn't know HOW it's persisted.

    I had hoped to use this library to replace an the ageing domain model layer on 99designs, so I do have some experience with large complex domain models. My experience on them has generally been the more complex they are, the less magic you are better of relying on. Hence, pheasant.

  11. #11
    ********* Victim lastcraft's Avatar
    Join Date
    Apr 2003
    Location
    London
    Posts
    2,423
    Mentioned
    2 Post(s)
    Tagged
    0 Thread(s)
    Hi...

    Quote Originally Posted by Lachlan View Post
    Enlighten a newbie
    Funny. We both know you are anything but .

    Quote Originally Posted by Lachlan View Post
    Marcus, which part is a singleton particularly?
    The top level Pheasant class. Can I not just instantiate this once myself?

    Quote Originally Posted by Lachlan View Post
    Any suggestions on how I could improve that particular part to make it more DI friendly?
    Just make it a normal class - job done.

    Say I'm getting my objects from some kind of repository/facade thingy...?
    PHP Code:
    class PheasantPoweredRepository implements Repository {
        function 
    __construct(Pheasant $pheasant) { ... }
        function 
    findStuff() { ... }

    When I use a DI tool I'll go...
    PHP Code:
    $repository $injector->create('Repository'); 
    I don't need Pheasant to tell me it's a Singleton, as I can get the DI tool to do that. I can save a bit of complexity by not bothering if I'm only going to have one Repository instance anyway.

    Quote Originally Posted by Lachlan View Post
    Also, I agree with TomB, I don't know it's so bad a thing that the object knows it CAN be persisted, it just shouldn't know HOW it's persisted.
    Oh, please don't misunderstand me here. I'm definitely *not* saying it's wrong. It looks like your ORM is exceptionally well designed. I even like your opening statement:

    "Basically where mine differs is in that I've tried to keep simple the declarations (which are in PHP), the database dependancy (which is Mysql and Innodb specific) and a build for a very light memory footprint."

    Stating the features left out is just the coolest advert for a tool ever.

    I was being pedantic .

    More pedantry...

    To be a DataMapper the object being mapped must know nothing. The mapper can see the domain object and some kind of schema (so it knows how to handle collections, etc). The domain object sees neither. The mapping of fields and relationships is in the schema which could be code or metadata.

    On a scale of ActiveRecord to DataMapper you are probably 80% DataMapper. Which is fine. Active record is fine. I'm just quibbling with naming.

    Suppose I've already written the domain objects and look around for a DataMapper (fat chance in PHP land, but bear with me), i'm going to be mildly disappointed to find I have to go back and edit my code to add these "extends" keywords.

    Just call it an ORM and I've nothing to complain about. Well, except the Singleton .

    Quote Originally Posted by Lachlan View Post
    I had hoped to use this library to replace an the ageing domain model layer on 99designs, so I do have some experience with large complex domain models.
    Your solution is a good one. When I said complex, I meant Java scale complex. I've only used a DataMapper once (for a recruitment site, sadly abandoned) and that wasn't even in PHP.

    The place you would get immediate friction is if the domain model itself involves inheritance. You've already played the inheritance card.

    Quote Originally Posted by Lachlan View Post
    My experience on them has generally been the more complex they are, the less magic you are better of relying on.
    Yes!

    yours, Marcus
    Marcus Baker
    Testing: SimpleTest, Cgreen, Fakemail
    Other: Phemto dependency injector
    Books: PHP in Action, 97 things

  12. #12
    SitePoint Enthusiast
    Join Date
    Feb 2005
    Posts
    27
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Marcus,

    Point taken on the fact that using Pheasant with existing objects is difficult. It's an interesting problem, one to which I am not sure I can find a lot of good solutions without introducing a lot more complexity. After some consideration, I think you and kyberfabrikken are right, it's probably not strictly a data mapper, however, I am ok with that Looking forward to what Traits brings to the table in PHP5.4.

    You're right the Pheasant class is a singleton. As a general rule I hate Singletons with a firey passion, mainly because of what they do to testability. In Pheasant, I was trying to leave the domain object constructors alone (even if it's a bit hacky), which precluded DI for the pheasant object into the domain object constructors. At the minute the static pheasant class is basically a Locator for a PheasantInstance, which can be replaced, mocked, etc as needed. Point taken though that it's got a lot of downsides. Will ponder this.

    I haven't had a lot to do with DI in PHP, I tend to get by with registries of pluggable factories. Something I should look into.

    - Lox

  13. #13
    SitePoint Guru bronze trophy TomB's Avatar
    Join Date
    Oct 2005
    Location
    Milton Keynes, UK
    Posts
    988
    Mentioned
    9 Post(s)
    Tagged
    2 Thread(s)
    I'm not sure I wholly agree. I think Lachlan's approach has far more benefits than drawbacks.


    Consider this:

    PHP Code:
    class DomainObject {
        protected 
    $mapper;
        
        public function 
    __construct(DataMapper $mapper) {
            
    $this->mapper $mapper;    
        }
        
        public function 
    save() {
            
    $this->mapper->save($this);    
        }
    }



    class 
    DataMapper {
        public function 
    findById($id) {
        
    //get data from the source    
            
    $result = array(); //query the database, read xml file, get the data from somewhere
            
    $obj = new DomainObject($this);
            foreach (
    $result as $key => $value$obj->$key $value;
            return 
    $obj;    
        }
        
        public function 
    save(DomainObject $obj) {
            
    //put $obj back into the database, or wherever it came from
        
    }

    The domain object knows it can be persistent (is this a really a bad thing?). What it doesn't know, is anything about how the mapper works.


    Pointless? In this example yes.. but introduce realations and it becomes incredibly powerful. By defining relations in the mapper (how a mapper relates to another mapper) it makes this:

    PHP Code:
    $user $userMapper->findById('123');
    echo 
    $user->orders[0]->items[0]->product->manufacturer->name
    Both possible and easy. This is how my own ORM works

    The alternative is messy and involves initiating a whole load of mappers manually with code that is clearly wrapping a relational schema or fetching the entire database into the $user object.

    Any of these mappers could be database/xml/flat file/web service. It doesn't matter.


    Should the domain object know it might be persistent? It does not cause any problems. You mention testing, in this example you'd use a mock DataMapper.

    Should the domain object know the method of persistence or any implementation details of it? No.

  14. #14
    Twitter: @AnthonySterling silver trophy AnthonySterling's Avatar
    Join Date
    Apr 2008
    Location
    North-East, UK.
    Posts
    6,111
    Mentioned
    3 Post(s)
    Tagged
    0 Thread(s)
    Maybe I'm a bit well, thick - but wouldn't something like the following be a little more obvious in declaring dependencies and focusing responsibilities?

    I'd love to know why I'm wrong here.

    PHP Code:
    class Mapper
    {
        public function 
    find(MapperCriteria $criteria);
        
        public function 
    save(MapperSavable $object);
    }

    class 
    UserMapper extends Mapper
    {
        public function 
    save(User $user){
            try{
                
    $object = new MapperSaveable($user);
                
    parent::save($object);
                return 
    true;
            }catch(
    MapperSaveable $ex){
                
    #thrown if cannot save this type
                
    return false;
            }
        }
    }

    class 
    User
    {
        

    It's rough, and knocked up during my Biscuit break so please forgive the roughness.

    The way I see it:-

    • MapperSavable checks to see if the object is compatible and maybe formats it for persistence, the latter should probably be left up to the Mapper though.
    • User no longer knows or cares if can be saved, thus left to do User stuff.
    • UserMapper allows some business logic to creep in and decide what to do if User cannot be saved etc..

    Maybe Mapper should be passed into UserMapper to allow different persistence methods?

    Sure this means *Mapper objects, but at least each is actually doing something obvious, and all these objects are on the same level of abstraction (application pov).

    @AnthonySterling: I'm a PHP developer, a consultant for oopnorth.com and the organiser of @phpne, a PHP User Group covering the North-East of England.

  15. #15
    SitePoint Guru bronze trophy TomB's Avatar
    Join Date
    Oct 2005
    Location
    Milton Keynes, UK
    Posts
    988
    Mentioned
    9 Post(s)
    Tagged
    2 Thread(s)
    As I said previously the issue is relations. With your method I need to do:

    PHP Code:
    $user $userMapper->findById(1);
    $orders $orderMapper->findWhere('userId = '.  $user->id);
    $orderItems $orderItemMapper->findWhere('orderId = ' $orders[0]->id);
    $product $productMapper->findById($items[0]->productId);
    $manufacturer $manufacturerMapper->findById($product->manufacturerId);
    echo 
    $manufacturer->name
    instead of

    PHP Code:
    $user $userMapper->findById(1);
    echo 
    $user->orders[0]->items[0]->product->manufacturer->name 
    The former is verbose, has reduced readability is not reusable and if the relations change, this code needs to be updated everywhere!

  16. #16
    Spirit Coder allspiritseve's Avatar
    Join Date
    Dec 2002
    Location
    Ann Arbor, MI (USA)
    Posts
    648
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by TomB View Post
    As I said previously the issue is relations.
    I could see the example you gave trimmed down to this:
    PHP Code:
    $user $userMapper->findbyId(1);
    $orders $orderMapper->findByUser($user);
    echo 
    $orders[0]->getItem(0)->getProductManufacturerName(); 
    Behind the scenes, I'd have the order mapper batch lazy load items, products, and manufacturers (possibly eager loading them depending on how often I used them when working with orders).

    In doing so, I keep knowledge of the database relationships in the mapper layer where they belong, and each object can remain blissfully unaware of the db.
    Last edited by allspiritseve; Jul 7, 2010 at 12:26. Reason: Edited based on TomB's post

  17. #17
    Twitter: @AnthonySterling silver trophy AnthonySterling's Avatar
    Join Date
    Apr 2008
    Location
    North-East, UK.
    Posts
    6,111
    Mentioned
    3 Post(s)
    Tagged
    0 Thread(s)
    Cheers Tom.

    Few questions, in your first example you actually show how you end up at the Manufacturers name, could you show how you would do this in your latter please?

    You state in your earlier post that the mapper would be aware of the relations, could this 'magic' just not be added to my example too?

    Thanks!

    Anthony.
    @AnthonySterling: I'm a PHP developer, a consultant for oopnorth.com and the organiser of @phpne, a PHP User Group covering the North-East of England.

  18. #18
    SitePoint Guru bronze trophy TomB's Avatar
    Join Date
    Oct 2005
    Location
    Milton Keynes, UK
    Posts
    988
    Mentioned
    9 Post(s)
    Tagged
    2 Thread(s)
    Sorry this example got a bit big, it shows how to get from $user to $user->orders

    PHP Code:
    abstract class DataMapper {
        public 
    $relations = array();
        
        public abstract function 
    findWhere($criteria);
        public abstract function 
    findById($id);
        
        public function 
    getMapper($name) {
            
    //Initiate a new mapper or get existing one, shout probably really go through a factory.
            
    return new $name;
        }

        public function 
    getRelated($relationName,  DataObject $object) {
            
    $relation $this->relations[$relationName];
            
    $mapper $this->getMapper($relation['mapper'] . 'Mapper');
            return 
    $mapper->findWhere($relation['foreignField'] . ' = ' $object->{$relation['localField']});
        }
        
        public function 
    hasRelation($relationName) {
            return isset(
    $relationName);
        }

    }

    class 
    UserMapper extends DataMapper {
        public 
    $relations = array(
            
    'orders' => array('mapper' => 'order''foreignField' => 'userId''localField' => 'id')
        );

        public function 
    findWhere($criteria) {
            
    //Query the database and return a set of User objects        
        
    }
        
        public function 
    findById($id) {
            
    //Query the database and return a User object
        
    }
    }

    class 
    OrderMapper {
        public function 
    findWhere($criteria) {
            
    //Query the database and return a set of Order objects        
        
    }
        
        public function 
    findById($id) {
            
    //Query the database and return a Order object
        
    }    
    }

    class 
    DataObject {
         protected 
    $mapper;
        
        public function 
    __construct(DataMapper $mapper) {
            
    $this->mapper $mapper;    
         }
         
        public function 
    __get($name) {
            if (
    $this->mapper->hasRelation($name)) return $this->mapper->getRelated($name$this);
        }

    }

    class 
    User extends DataObject {
        
        

    It cant be added to your example as you need some way of getting back from the domain object to the mapper.

    To get to manufacturer you'd create all the extra mappers, setting up the relations you go.

  19. #19
    SitePoint Enthusiast
    Join Date
    Feb 2005
    Posts
    27
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Tom, interesting approach. I'm still not sure about the fact that in Pheasant properties are lowercase and relationships are uppercase (Doctrine does this too). Perhaps slightly too magic.

    How do you distinguish between them in your code?

  20. #20
    SitePoint Guru bronze trophy TomB's Avatar
    Join Date
    Oct 2005
    Location
    Milton Keynes, UK
    Posts
    988
    Mentioned
    9 Post(s)
    Tagged
    2 Thread(s)
    Short answer: I don't.

    Why would you?

    $user->orders is just as valid and meaningful as $user->firstname. They just store different data types. One is a string, the other is an array. Imho, There does not need to be any difference in the way these are handled by the code using the object. How it works behind the scenes is less important than how it is used by client code, in my opinion.

    if you didnt want the magic you could rename __get to getRelated and call $user->getRelated('orders');

  21. #21
    SitePoint Enthusiast
    Join Date
    Feb 2005
    Posts
    27
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Yup, very fair point and a good simplification.

    Have you run into the N+1 problem, where you are iterating over relationships? E.g:

    PHP Code:

    foreach($user->groups as $group)
    {
       foreach(
    $group->members as $member)
       {
         echo 
    "member name: {$member->fullname}\n";
       }

    Any clever techniques for optimizing this pattern of access?

  22. #22
    SitePoint Guru bronze trophy TomB's Avatar
    Join Date
    Oct 2005
    Location
    Milton Keynes, UK
    Posts
    988
    Mentioned
    9 Post(s)
    Tagged
    2 Thread(s)
    I had the same reservations in this topic. Czaries pointed me in the right direction I won't reiterate what he told me. His posts there are useful.

    edit: By the way, the code I posted above was my solution to the problems I presented in that topic

  23. #23
    SitePoint Guru bronze trophy TomB's Avatar
    Join Date
    Oct 2005
    Location
    Milton Keynes, UK
    Posts
    988
    Mentioned
    9 Post(s)
    Tagged
    2 Thread(s)
    Not that simple, because you need to get the correct product from the order item, from the order.

    Either way, imho, coding specific get*() functions in the mapper seems like overkill for getting something which could be any depth down the chain, your mappers could get very large.

    I wanted a way that supports unlimited chaining without having to write new functions in specific mappers every time I want a different object which may be any depth down the chain from the current mapper. It just allows for much faster/easier/consistent development.

    In my example, only the mappers do have knowledge of the relationships That's where they're stored. The domain objects essentially only have knowledge of the mapper they were created by.

  24. #24
    Spirit Coder allspiritseve's Avatar
    Join Date
    Dec 2002
    Location
    Ann Arbor, MI (USA)
    Posts
    648
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by TomB View Post
    Not that simple, because you need to get the correct product from the order item, from the order.
    You're right. I amended my post.

    Quote Originally Posted by TomB View Post
    Either way, imho, coding specific get*() functions in the mapper seems like overkill.
    Maybe... but I think it depends on the complexity of the query because you're exposing implementation details.

    Quote Originally Posted by TomB View Post
    I wanted a way that supports unlimited chaining without having to write new functions in specific mappers every time I want a different object which may be any depth down the chain from the current mapper.
    You shouldn't have to write a new function for each related object. You just need some way to specify immediate object relations. It's probably not much different from how you're currently specifying relations, you are just working with relationships between objects instead of between tables.

    Quote Originally Posted by TomB View Post
    It just allows for much faster/easier/consistent development.
    Yeah I understand that. It's convenience with the cost of your domain having a dependency on the database. That may or may not be an issue, depending on the complexity of your domain.

    Quote Originally Posted by TomB View Post
    In my example, only the mappers do have knowledge of the relationships That's where they're stored. The domain objects essentially only have knowledge of the mapper they were created by.
    And in my example, the domain objects don't need to have knowledge of any mappers. Maybe that's not important to you, but it has its uses.

  25. #25
    SitePoint Guru bronze trophy TomB's Avatar
    Join Date
    Oct 2005
    Location
    Milton Keynes, UK
    Posts
    988
    Mentioned
    9 Post(s)
    Tagged
    2 Thread(s)
    Quote Originally Posted by allspiritseve View Post
    Yeah I understand that. It's convenience with the cost of your domain having a dependency on the database.
    Well, does it? It has a dependency on the mapper. Which can be referencing anything, the file system, a web server, etc. The domain object knows it contains data and that that data comes from somewhere (its non-descript mapper)

    And in my example, the domain objects don't need to have knowledge of any mappers. Maybe that's not important to you, but it has its uses.
    I'm probably missing something here but I cant see how the domain object knowing it contains data that comes from somewhere is an issue.


Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •