I have an model with a relation, and I want to instantiate a new object of the relations type.
Example: A person has a company, and I have a person-object: now I
want to create a company-object.
The class of the companyobject is defined in the relation, so I don't think I should need to 'know' that class, but I should be able to ask the person-object to provide me with a new instance of type company? But I don't know how.
This is -I think- the same question as New model object through an association , but I'm using PHPActiveRecord, and not the ruby one.
Reason behind this: I have an abstract superclass person, and two children have their own relation with a type of company object. I need to be able to instantiate the correct class in the abstract person.
A workaround is to get it directly from the static $has_one array:
$class = $this::$has_one[1]['class_name'];
$company = new $class;
the hardcoded number can of course be eliminated by searching for the association-name in the array, but that's still quite ugly.
If there is anyone who knows how this is implemented in Ruby, and how the phpactiverecord implementation differs, I might get some Ideas from there?
Some testing has revealed that although the "search my classname in an array" looks kinda weird, it does not have any impact on performance, and in use it is functional enough.
You can also use build_association() in the relationship classes.
Simplest way to use it is through the Model's __call, i.e. if your relation is something like $person->company, then you could instantiate the company with $company = $person->build_company()
Note that this will NOT also make the "connection" between your objects ($person->company will not be set).
Alternatively, instead of build_company(), you can use create_company(), which will save a new record and link it to $person
In PHPActiveRecord, you have access to the relations array. The relation should have a name an you NEED TO KNOW THE NAME OF THE RELATIONSHIP/ASSOCIATION YOU WANT. It doesn't need to be the classname, but the classname of the Model you're relating to should be explicitly indicated in the relation. Just a basic example without error checking or gritty relationship db details like linking table or foreign key column name:
class Person extends ActiveRecord\Model {
static $belongs_to = array(
array('company',
'class_name' => 'SomeCompanyClass')
);
//general function get a classname from a relationship
public static function getClassNameFromRelationship($relationshipName)
foreach(self::$belongs_to as $relationship){
//the first element in all relationships is it's name
if($relationship[0] == $relationshipName){
$className = null;
if(isset($relationship['class_name'])){
$className = $relationship['class_name'];
}else{
// if no classname specified explicitly,
// assume the clasename is the relationship name
// with first letter capitalized
$className = ucfirst($relationship);
}
return $className
}
}
return null;
}
}
To with this function, if you have a person object and want an object defined by the 'company' relationship use:
$className = $person::getClassNameFromRelationship('company');
$company = new $className();
I'm currently using below solution. It's an actual solution, instead
of the $has_one[1] hack I mentioned in the question. If there is a
method in phpactiverecord I'm going to feel very silly exposing
msyelf. But please, prove me silly so I don't need to use this
solution :D
I am silly. Below functionality is implemented by the create_associationname call, as answered by #Bogdan_D
Two functions are added. You should probably add them in the \ActiveRecord\Model class. In my case there is a class between our classes and that model that contains extra functionality like this, so I put it there.
These are the 2 functions:
public function findClassByAssociation($associationName)
Called with the name of the association you are looking for.
Checks three static vars (has_many,belongs_to and has_one) for the association
calls findClassFromArray if an association is found.
from the person/company example: $person->findClassByAssociation('company');
private function findClassFromArray($associationName,$associationArray)
Just a worker-function that tries to match the name.
Source:
/**
* Find the classname of an explicitly defined
* association (has_one, has_many, belongs_to).
* Unsure if this works for standard associations
* without specific mention of the class_name, but I suppose it doesn't!
* #todo Check if works without an explicitly set 'class_name', if not: is this even possible (namespacing?)
* #todo Support for 'through' associations.
* #param String $associationName the association you want to find the class for
* #return mixed String|false if an association is found, return the class name (with namespace!), else return false
* #see findClassFromArray
*/
public function findClassByAssociation($associationName){
//$class = $this::$has_one[1]['class_name'];
$that = get_called_class();
if(isset($that::$has_many)){
$cl = $this->findClassFromArray($associationName,$that::$has_many);
if($cl){return $cl;}
}
if(isset($that::$belongs_to)){
$cl = $this->findClassFromArray($associationName,$that::$belongs_to);
if($cl){return $cl;}
}
if(isset($that::$has_one)){
$cl = $this->findClassFromArray($associationName,$that::$has_one);
if($cl){return $cl;}
}
return false;
}
/**
* Find a class in a php-activerecord "association-array". It probably should have a specifically defined class name!
* #todo check if works without explicitly set 'class_name', and if not find it like standard
* #param String $associationName
* #param Array[] $associationArray phpactiverecord array with associations (like has_many)
* #return mixed String|false if an association is found, return the class name, else return false
* #see findClassFromArray
*/
private function findClassFromArray($associationName,$associationArray){
if(is_array($associationArray)){
foreach($associationArray as $association){
if($association['0'] === $associationName){
return $association['class_name'];
}
}
}
return false;
}
Related
I'm new to OOP and MVC with PHP, and I'm currently learning by making my own custom framework from scratch, for testing purposes. I have set up my controllers, models and views and everything works fine.
My app has the following architecture :
It’s a small blog that follows the rules of the MVC pattern. To summarize, it works like this :
The called Controller will fetch the data using the right models
Models return objects of the class \Classes\{MyObject}
Controller call the right template to render the view, and passes it the data and objects to display
The problem
In some views, I need to display related data. For example, in the article view, I need to display the author's first name. In the database, an article contains only the author’s ID, not his first name : this is the same thing in my class \Classes\Article.
What I've tried
To display the author’s first name in my view, I've updated the model Find method to use a LEFT JOIN in the SQL query. Then, I've updated my \Classes\Article class to have a user_firstname property :
class Article
{
private $pk_id;
private $title;
private $excerpt;
private $content;
private $created_at;
private $fk_user_id;
private $updated_at;
private $user_firstname; // <-- I've added this property to retrieve author's firstname
// (...)
}
What I did works well, but my teacher tells me it’s not the right way to do it because the author’s firstname is not part of the definition of an article.
In this case, my teacher tells me to use a DTO (Data Transfert Object) between Article and User classes.
Questions
What is the right way to set up a DTO in this case?
Do I need to create a new ArticleUserDTO class in a new namespace ?
How to use it ?
I think I understood the problem : the Article class should only contain what defines an article. But I can’t understand the logic of setting up a DTO. I’ve done some research on it, I understand the usefulness of the DTO but I can’t set up into my app.
Setting up a DTO in my app was easy ! As mentioned by #daremachine, the diagram below helped me to understand what a DTO is for.
Diagram source : martinfowler.com
We can see DTOs as an object assembler, in which we place all the elements we need on the view side.
For example, in the post view of an article, I needed to display other items, such as the author and posted comments. So I have created a Post class that groups all these items.
Setting up a DTO
In my \Classes\ namespace, I've created a new Post class. First, we define the properties we will need. Then we add the getters and setters for each of them. Finally, we set up the constructor, which will call each of the classes we need in the view.
namespace Classes;
use DateTime;
class Post
{
private int $pk_id;
private string $title;
private string $excerpt;
private string $content;
private DateTime $created_at;
private DateTime $updated_at;
private int $author_id;
private string $author_firstname;
private array $comments;
public function __construct(Article $article, User $author, array $comments)
{
$this->setPkId($article->getId());
$this->setTitle($article->getTitle());
$this->setExcerpt($article->getExcerpt());
$this->setContent($article->getContent());
$this->setCreatedAt($article->getCreatedAt());
$this->setUpdatedAt($article->getUpdatedAt());
$this->setAuthorId($article->getAuthorId());
$this->setAuthorFirstname($author->getFirstname());
$this->setComments($comments);
}
/**
* #param int $pk_id
*/
public function setPkId(int $pk_id): void
{
$this->pk_id = $pk_id;
}
/**
* #return int
*/
public function getPkId(): int
{
return $this->pk_id;
}
// (etc)
}
We now need to update the ArticleController, which should no longer pass the Article, Comment and User objects, but only the new Post object.
namespace Controllers;
class ArticleController extends Controller
{
// (...)
/**
* Get an article and display it
*
* #return void
*/
public function show(): void
{
// (...)
// Find Article :
$article = $this->articleModel->find($article_id);
if (!$article) {
Http::error404();
}
// Find Comments :
$commentaires = $this->commentModel->findAllByArticle($article_id);
// Find User (author)
$user = $this->userModel->find($article->getAuthorId());
// Data Transfert Object instance :
$post = new Post($article, $user, $commentaires);
$pageTitle = $post->getTitle();
// Pass DTO to view :
Renderer::render('articles/show', compact('pageTitle', 'post'));
}
}
We just need to update our view to use the new Post object and it's done ! Thanks to #daremachine for his help :)
I've been reading this post : Doctrine - how to check if a collection contains an entity
But I actually don't like the solution, as, doctrine already provide the contains() method, which have the advantage to keep logic directly into the object, and then to not load EXTRA_LAZY collections entirely.
So here a Cart Entity own a CartProduct collection as is :
/**
* ...
* #ORM\Entity(repositoryClass="App\Repository\CartRepository")
*/
abstract class Cart implements InheritanceInterface{
...
/**
* #ORM\OneToMany(targetEntity="CartProduct", mappedBy="cart", fetch="EXTRA_LAZY", cascade={"persist"})
*/
private Collection $cartProducts;
...
public function __construct()
{
$this->cartProducts = new ArrayCollection();
}
...
}
(CartProduct have to be an Entity look at this simplify EA model. That's a standard way to proceed for related entity holding extra fields)
Now I want to add a new ProductCart Entity to my Cart class.
So I'm adding this method (generated by Symfony make:entity) :
abstract class Cart implements InheritanceInterface{
...
public function addCartProduct(CartProduct $cartProduct): self
{
if(!$this->getCartProducts()->contains($cartProduct)) {
$this->cartProducts->add($cartProduct);
$cartProduct->setCart($this);
}
return $this;
}
...
And then I test this code :
public function testAddCartProduct()
{
$cart = new ShoppingCart($this->createMock(ShoppingCartState::class));
$cart_product = new CartProduct();
$cart_product->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product);
$cart_product2 = new CartProduct();
$cart_product2->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product2);
$this->assertCount(1, $cart->getCartProducts());
}
But when I run this test, it fail :
Failed asserting that actual size 2 matches expected size 1.
So I check, and the Cart.cartProducts Collection have two product which are exactly the same objects.
As it's an ArrayCollection, I suppose that it just use this method :
namespace Doctrine\Common\Collections;
class ArrayCollection implements Collection, Selectable {
...
public function contains($element)
{
return in_array($element, $this->elements, true);
}
So well, of course in this case it is just return false, And the objects are considered to be different.
So now, I wish I could use PersistentCollection instead of ArrayCollection when implementing the Collection object , because the PersistentCollection.contains() method looks better.
abstract class Cart implements InheritanceInterface{
...
public function __construct()
{
-- $this->cartProducts = new ArrayCollection();
++ $this->cartProducts = new PersistentCollection(...);
}
}
But this require an EntityManager as a parameter, so, seams a little bit overkill to give an EntityManager to an Entity object...
So I finally, I don't know what is the better way to check for a dupplicate entity inside a collection.
Of course, I could implement myself a thing like :
abstract class Cart implements InheritanceInterface{
...
public function addCartProduct(CartProduct $cartProduct): self
{
if(!$this->getCartProducts()->filter(
function (CartProduct $cp)use($cartProduct){
return $cp->getId() === $cartProduct->getId();
})->count()) {
$this->cartProducts->add($cartProduct);
$cartProduct->setCart($this);
}
return $this;
}
...
But it'll require to load every Entity and I really don't like the idea.
Personally I agree with your comment, I don't think the entity itself should have the responsibility to ensure there is no duplicate.
The entity cannot make a request like a repository could, and I don't see how you can be sure there is no duplicate in the database without querying it.
Calling contains will not trigger a fetch in your case, this means the collection will stay as is, which is not what you want anyway because you could have a previously persisted duplicate that will not be part of the collection because you marked it as EXTRA_LAZY.
You also don't want to fetch all the entities of the collection (and transform the results into objects) just to check if you have a collision.
So IMHO you should create a method in the repository of the entity to check for duplicates, a simple SELECT COUNT(id).
Then there is your real problem.
The way you make your test will never find a collision. When you do:
$cart = new ShoppingCart($this->createMock(ShoppingCartState::class));
$cart_product = new CartProduct();
$cart_product->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product);
$cart_product2 = new CartProduct();
$cart_product2->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product2);
$this->assertCount(1, $cart->getCartProducts());
You are creating two instances of CartProduct, that's why the call to contains doesn't find anything.
Because contains checks for the object reference, not the content, like you can see in its implementation:
public function contains($element)
{
return in_array($element, $this->elements, true);
}
So in your test case what you're really testing is:
in_array(new CartProduct(), [new CartProduct()], true);
which will always return false.
I wrote a vcard class with Phalcon in PHP. The vCard Model is initialized like this.
// Inside the BS_VCard class
public function initialize(){
$this->hasMany("id","BS_VCardElement","vCardId",array(
"alias" => "elements",
'foreignKey' => array(
'action' => Phalcon\Mvc\Model\Relation::ACTION_CASCADE
)
));
}
Its elements are initialized like this
// Inside the BS_VCardElement class
public function initialize(){
$this->belongsTo("vCardId","BS_VCard","id",array("alias" => "vCard"));
...
}
If a user reads a vCard and adds another element, it doesn't work as expected. To simplify the use I added some fascade methods like this
public function addDateOfBirth($date){
$element = new BS_VCardElement();
$element->setName("BDAY");
$element->addValue($date);
// This doesn't work
$this->elements[] = $element;
}
The Docs/Storing related records do not explain how to append fresh data like this to the related table.
I also tried this
$this->elements[] = array_merge($this->elements,array($element));
But the save method seems to ignore the added element. Save() returns true.
This question has been asked a couple of months ago but since I ran into a similar issue I decided to share my results anyway.
And here's what I found. Lower case aliases ('elements') don't seem to work whereas upper case aliases ('Elements') do.
To add one element you can do this;
$this->Elements = $element;
To add multiple elements you can do this;
$elements = array($element1, $element2);
$this->Elements = $elements;
After that you have to save the vcard before accessing the elements again. If you don't, phalcon will just return a result set with only the elements already in the database. (Not sure if this can be changed somehow.)
And here's the documentation (where all this is not mentioned): http://docs.phalconphp.com/en/latest/reference/models.html#storing-related-records
According to the Phalcon source code, the Resultset object is immutible.
/**
* Resultsets cannot be changed. It has only been implemented to
* meet the definition of the ArrayAccess interface
*
* #param int index
* #param \Phalcon\Mvc\ModelInterface value
*/
public function offsetSet(var index, var value)
{
throw new Exception("Cursor is an immutable ArrayAccess object");
}
It appears that replacing the element with an array is the only way to implement an "append" or modification of the resultset (other than delete which IS supported).
Of course this breaks the \Phalcon\Mvc\Model::_preSaveRelatedRecords() because the function ignores the class properties and refetches the related from the Model Manager (and resets the model::$element attribute at the end).
I feel frustrated by this because appending objects to a collection seems like a very common task and not having a clear method in which to add new items to a parent seems like a design flaw.
I think related elements might have some magic functionality invoked when you set the properties, so simply using $this->elements[] (evidently) doesn't work. Perhaps try re-setting the entire variable:
public function addDateOfBirth($date){
$element = new BS_VCardElement();
$element->setName("BDAY");
$element->addValue($date);
$elements = $this->elements;
$elements[] = $element;
$this->elements = $elements;
}
I'm writing some code that allows users to read reports on a site, using AJAX calls to dynamically load only what is requested, instead of the entire 15+MB report.
I'm writing a Model to access all the report data from the database, and I don't want to use the Active Record pattern. I'm following the idea of "A Model HAS a table, instead of IS-A table", since this model will be accessing 5 different tables, and there are some complex MySQL JOIN's between these tables.
What is a good design pattern to follow in Zend Framework for this, examples?
UPDATED on 2012-12-05 # 12:14PM EST
I'm currently working for a Market Research Report company. Without using actual function names, or revealing any meaningful details of the code, here are the basics:
readreportAction() does:
get the report meta data
get the report "table of contents"
readsectionAction() does:
get the report text, only a part of it
get the embedded tabular data
get the figures / images
get the footnotes
format the report text
reportpdfAction() does the exact same thing as readreportAction() and readsectionAction(), except all at one time. I'm trying to conceptualize a way to NOT copy + paste this code / programming logic. A data mapper seems to solve this.
I would recommend the Data Mapper pattern.
Everything you said makes sense and this pattern fits. Your model should not know or care how it is persisted. Instead the mapper does what it suggests - maps your model to your database. One of the things I like about this approach is it encourages people to think about the model in terms of an object, not a relational database table, as often happens with active record patterns and table row gateways.
Your object, unless very simple, typically will not reflect the structure of a database table. This lets you write good objects and then worry about the persistence aspects afterward. Sometimes more manual in that your mapper will need to deal with the complex joins, probably requiring writing some code or SQL, but the end result is it does just what you want and nothing more. No magic or conventions required if you don't want to leverage them.
I've always though these articles do a good job of explaining some of the design patterns that can be used well in ZF: http://survivethedeepend.com/zendframeworkbook/en/1.0/implementing.the.domain.model.entries.and.authors#zfbook.implementing.the.domain.model.entries.and.authors.exploring.the.entry.data.mapper
UPDATE:
Well you mapper might extend from an interface similar to this:
<?php
interface Mapper_Interface
{
/**
* Sets the name of the entity object used by the mapper.
*/
public function setObjectClass($class);
/**
* Sets the name of the list class used by the mapper.
*/
public function setObjectListClass($listClass);
/**
* Get the name of the object class used by the mapper.
*
*/
public function getObjectClass();
/**
* Get the name of the object list class used by the mapper.
*
* #return string
*/
public function getObjectListClass();
/**
* Fetch one row.
*
* #param array $where Criteria for the selection.
* #param array [$order = array()] Optionally the order of results
* #return Object_Abstract
* #throws Mapper_Exception
*/
public function fetchRow($where, $order = array());
/**
* Fetch all records. If there is no underlying change in the persisted data this should
* return a consistant result.
*
* #param string|array|Zend_Db_Table_Select $where OPTIONAL An SQL WHERE clause or Zend_Db_Table_Select object.
* #param string|array $order OPTIONAL An SQL ORDER clause.
* #param int $count OPTIONAL An SQL LIMIT count.
* #param int $offset OPTIONAL An SQL LIMIT offset.
* #return Object_List_Abstract
* #throws Mapper_Exception
*/
public function fetchAll($where = null, $order = null, $count = null, $offset = null);
/**
* Deletes one or more object.
*
* #param array|string $where Criteria for row deletion.
* #return integer $affectedRows
* #throws Mapper_Exception
*/
public function delete($where);
/**
* Saves a record. Either updates or inserts, as required.
*
* #param $object Object_Abstract
* #return integer $lastInsertId
* #throws Mapper_Exception
*/
public function save($object);
}
And you would interact with the mapper like:
$fooObjectMapper = new Foo_Mapper;
$fooObjectList = $fooObjectMapper->fetchAll();
var_dump($fooObjectList->first());
or
$fooObjectMapper = new Foo_Mapper;
$fooObject = $fooObject->fetch(array('id = ?' => 1));
$fooObject->setActive(false);
$fooObjectMapper->save($fooObject);
I usually write a mapper abstract for any 'PDO' enabled databases. One of the attributes of that concrete mapper is then the Zend_Db_Adapter to issue commands against. Makes for a flexible solution, easy to use mock data sources in testing.
First it looks like you need to make a little bit more of a conceptual leap. With the data mapper pattern it helps to think in terms of objects instead of database tables. I found these two articles helpful when I needed to make the leap.
http://phpmaster.com/building-a-domain-model/
http://phpmaster.com/integrating-the-data-mappers/
That being said ZF 1 has some very useful tools for building a data mapper/domain model.
The convention in ZF 1 is for each table you are working with to be accessible through the Zend_Db_Table api. The simplest way I've found is to just use the DbTable resource for each table. You could also use the Zend_Db::factory or new Zend_Db_Table('tableName') or any other method that appeals to you.
This example is based on a mp3 song track.
//in effect this is the database adapter for database table 'track', This is $tableGateway used later.
<?php
class Application_Model_DbTable_Track extends Zend_Db_Table_Abstract
{
//name of database table, required to be set if name of class does not match name of table
protected $_name = 'track';
//optional, column name of primary key
protected $_primary = 'id';
}
there are several ways to attach a table to the Db adapter and the Zend_Db_Table api, I just find this method simple to implement and it makes setting up a mapper simple as well.
The mapper class is the bridge between the data source and your object (domain entity). The mapper interacts with the api for Zend_Db_Table in this example.
A really important point to understand: when using classes that extend Zend_Db_Table_Abstract you have all the basic functionality of the Zend_Db component at your disposal. (find(),fetchall(), fetchRow(), select() ...)
<?php
class Music_Model_Mapper_Track extends Model_Mapper_Abstract
{
//the mapper to access the songs artist object
protected $artistMapper;
//the mapper to access to songs album object
protected $albumMapper;
/**
* accepts instance of Zend_Db_Table_Abstract
*
* #param Zend_Db_Table_Abstract $tableGateway
*/
public function __construct(Zend_Db_Table_Abstract $tableGateway = null)
{
//at this point I tend to hardcode $tablegateway but I don't have to
$tableGateway = new Application_Model_DbTable_Track();
parent::__construct($tableGateway);
//parent sets the $tablegateway variable and provides an abstract requirement
//for createEntity(), which is the point of this class
}
/**
* Creates concrete object of Music_Model_Track
*
* #param object $row
* #return Music_Model_Track
*/
public function createEntity($row)
{
$data = array(
'id' => $row->id,
'filename' => $row->filename,
'format' => $row->format,
'genre' => $row->genre,
'hash' => $row->hash,
'path' => $row->path,
'playtime' => $row->playtime,
'title' => $row->title,
'track_number' => $row->track_number,
'album' => $row->album_id,//foriegn key
'artist' => $row->artist_id//foriegn key
);
//instantiate new entity object
return new Music_Model_Track($data);
}
/**
* findById() is proxy for find() method and returns
* an entity object.
*
* #param type $id
* #return object Model_Entity_Abstract
*/
public function findById($id)
{
//instantiate the Zend_Db_Select object
$select = $this->getGateway()->select();
$select->where('id = ?', $id);
//retrieve one database table row
$row = $this->getGateway()->fetchRow($select);
//create one entity object Music_Model_Track
$entity = $this->createEntity($row);
//return one entity object Music_Model_Track
return $entity;
}
//truncated
}
All that has gone before is for the express purpose of building the following object:
<?php
class Music_Model_Track extends Model_Entity_Abstract
{
/**
* $id, __set, __get and toArray() are implemented in the parent
*/
protected $album;
protected $artist;
protected $filename;
protected $format;
protected $genre;
protected $hash;
protected $path;
protected $playtime;
protected $title;
protected $track_number;
//artist and album mappers
protected $albumMapper = null;
protected $artistMapper = null;
//these are the important accessors/mutators because they convert a foreign key
//in the database table to an entity object.
public function getAlbum()
{
//if the album object is already set, use it.
if(!is_null($this->album) && $this->album instanceof Music_Model_Album) {
return $this->album;
} else {
//else we make a new album object
if(!$this->albumMapper) {
$this->albumMapper = new Music_Model_Mapper_Album();
}
//This is the album object we get from the id in our reference array.
return $this->albumMapper->findById($this->getReferenceId('album'));
}
}
//same as above only with the artist object.
public function getArtist()
{
if(!is_null($this->artist) && $this->artist instanceof Music_Model_Artist) {
return $this->artist;
} else {
if(!$this->artistMapper) {
$this->artistMapper = new Music_Model_Mapper_Artist();
}
return $this->artistMapper->findById($this->getReferenceId('artist'));
}
}
//the setters record the foriegn keys recorded in the table row to an array,
//this allows the album and artist objects to be loaded only when needed.
public function setAlbum($album)
{
$this->setReferenceId('album', $album);
return $this;
}
public function setArtist($artist)
{
$this->setReferenceId('artist', $artist);
return $this;
}
//standard setter and getters truncated...
}
so when using the track object you would get album or artist info like:
//this would be used in a controller most likely.
$mapper = new Music_Model_Mapper_Track();
$track = $mapper->findById('1');
//all of the information contained in the album or artist object is
//available to the track object.
//echo album title, year or artist. This album object also contains the artist object
//so the artist object would be available in two ways.
echo $track->album->title; //or
echo $track->artist->name;
echo $track->album->artist->name;
echo $track->getAlbum()->getArtist()->getName();
So what you really need to decide is how you want to structure your application. What I see as obvious may not be an option you wish to implement. A lot of the answers to your questions depend on exactly how these resources are to be used.
I hope this helps you at least a little bit.
You could consider using Doctrine 2. It's an ORM that does not use the ActiveRecord pattern.
In Doctrine, your models (entities) are all just normal PHP objects with zero knowledge of the database. You use mapping (xml, yaml or annotations) to tell Doctrine how they appear in the database, and the Entity Manager and repositories are used as a gateway for persisting entities or doing other database actions.
I'm using Zend Framework and following the design pattern of separating the Data layer from the Domain layer
the problem raises when implementing the methods for the Data mapper
so i implemented the save() which insert & update based on whether domain model contains id property and find() which return the records domain object based on id parameter
but what if i need to
search all/some rows in a table and return all the columns
search the same rows and return a mysql COUNT value
should i just directly use the class the inherited the Zend_Db_Table_Abstract for these needs or
should i implement method for every need ?
i'm a little confused on how to divide the functionality of the Data Mapper that will fit my needs and my future needs
You can add individual finder Methods, e.g.
class PersonMapper
{
… // other code
public function findByLastName()
{
// … fetch rowset and map them
}
public function countByLastName()
{
…
However, that will quickly get out of hand when you need to query multiple columns or want to handle CRUD by arbitrary criteria. You don't want methods like
public function findByLastNameAndBirthdayAndMaritalStatus()
The easy solution would be to use Zend_Db_Table_Select to create the queries and then pass those to the Data Mapper to execute and map them, e.g. in your DataMapper
public function getSelect()
{
return $this->personTable->select();
}
public function findBy(Zend_Db_Table_Select $select)
{
$people = $this->personTable->fetchAll($select);
// map people to People objects
}
You could abstract this further with the Mapper returning and accepting PersonQueryBuilder instead, which hides the SQL Semantics inside and let's you specify against your Domain Objects instead. It's more effort though.
Also have a look at the Repository and Specification Pattern.
As much as Gordon very likely has the correct answer, I find it overly complex for my tastes and needs at the moment.
I use a base mapper class for all of my domain mappers and I put as much functionality into the base class as I can.
I use a find by column method that works fairly well in all of my mappers:
//from abstract class Model_Mapper_Abstract
//The constructor of my base class accepts either a dbtable model
// or the name of a table stored in the concrete mapper tablename property.
public function __construct(Zend_Db_Table_Abstract $tableGateway = null)
{
if (is_null($tableGateway)) {
$this->tableGateway = new Zend_Db_Table($this->tableName);
} else {
$this->tableGateway = $tableGateway;
}
}
/**
* findByColumn() returns an array of entity objects
* filtered by column name and column value.
* Optional orderBy value.
*
* #param string $column
* #param string $value
* #param string $order optional
* #return array of entity objects
*/
public function findByColumn($column, $value, $order = null)
{
//create select object
$select = $this->getGateway()->select();
$select->where("$column = ?", $value);
//handle order option
if (!is_null($order)) {
$select->order($order);
}
//get result set from DB
$result = $this->getGateway()->fetchAll($select);
//turn DB result into domain objects (entity objects)
$entities = array();
foreach ($result as $row) {
//create entity, handled by concrete mapper classes
$entity = $this->createEntity($row);
//assign this entity to identity map for reuse if needed
$this->setMap($row->id, $entity);
$entities[] = $entity;
}
//return an array of entity objects
return $entities;
}
I hope you find this useful as an idea generator at the least. Also if you wish to implement a SQL Count() statement in a method similar to this it will go easier if you use Zend_Db_Expr() when you build that select().