In a PHP project I have three layers (Logical)
UI :
Place to store UI php pages. ex: ManageUsers.php
Code :
All Entity Classes Related to system. ex: User.php
DataAccess :
Data Access Classes for Entity Classes ex: UserDataAccess.php
So to add user from UI, I create a instances of User class set properties of it & call AddUser method (in user class). In AddUser method it creates a instance of UserDataAccess class & pass the same ($this) object to UserDataAccess Insert function. It gets the data from User object properties & updates the table. (returns status back to UI through Code layer)
I repeat this for other functions like Edit & Delete.
Is this the preferable OOP way to achive this CRUD functions or can I reduce one layer & combine Code & DataAccess layer to make it more simple?
Is it ok to instanciate User class more than once in ManageUser page (one instance for Add, one for Edit and so on) or should instanciate it only once for a single page?
Sorry about the length & complexity of the question.. Thanks in advance...
Is this the preferable OOP way to achive this CRUD functions or can I reduce one layer & combine Code & DataAccess layer to make it more simple?
The 3 layer method (Interface "U.I.", Business logic "Code" & Data Access), is the suggested way to do things.
You'll find that there are several business apps. that use the "2 layer" method that you suggested. If your think that application is going to "grow", you should use the 3 layer instead.
But, have in mind, that its complexity will also increase.
You may also want to get (download, develop, buy) a separate tool program that reads a database or data access, and generates your entity classes from it, instead of programming all by yourself.
If possible, do the same, for the "Code" or "Business logic" entities.
One more thing, avoid putting all you entities, in a single file.
Use a file per entity, instead. Make, unleast, a single "GUI" folder for your interface or "boundary" classes,
a folder for your "Code" classes, and a folder for your "Data Access" classes.
Your website or application may be small, and this suggestions may seen very complex nad unnecesary.
But, by experience, apps. tend to grow, even "pet" or "personal" websites, unexpectly may grow, and become complex.
Even, if its a personal, small project, you may try to use the suggestions, in order to be prepare for large websites or apps.
Additional Suggestion
One trick I learn from a coworker, to make code more maintainable, its to use a program that generates the entities code, automatically, and, later, not make instances of those class entities, as usually done in the 3 layer method.
Instead, a second class entity was code, usually manually, adding some members or features, that may be updated regularly.
So the first (parent) class have the members that can be obtained from the database (or XML file), and the second (child) class, has the members that may not be obtained from the database, and also require to do a lot of changes, that require to be "coded by hand".
Example:
(1) You have a database table (or XML file or local file) like this:
CREATE TABLE User
(
integer UserKey primary key,
varchar(20) UserAlias,
varchar(250) UserPasword,
integer UserIsActive, -- My D.B. doesn't support boolean
)
(2) You generate a Data-Access-Layer entity like this, wheter manually or tool-generated:
abstract class FieldsUserDataAccessEntityClass
{
/* integer */ protected $fieldUserKey = 0;
/* string */ protected $fieldUserAlias = "";
/* string */ protected $fieldUserPassword = "";
/* integer */ protected $fieldUserIsActive = 0;
/* integer */ public function CalculateNewKey();
/* void */ public function Insert();
/* void */ public function InsertWithKey(/* integer */ $newKey);
/* void */ public function Update();
/* void */ public function Delete();
} // class
This previous class is commonly used directly (without the "abstract" feature).
But, the suggestiuon is, that in other file, this, (I skip the methods code):
/* concrete */ class UserDataAccessEntityClass extends FieldsUserDataAccessEntityClass
{
public /* integer */ function getUserKey();
public function setUserKey(/* integer */ $newValue);
public /* string */ function getUserAlias();
public function setUserAlias(/* string */ $newValue);
public /* string */ function getUserPassword();
public function setUserPassword(/* string */ $newValue);
public /* string */ function getUserPassword();
public function setUserPassword(/* string */ $newValue);
public /* bool */ function getUserIsActive();
public function setUserIsActive(/* bool */ $newValue);
} // class
...
/* void */ function GlobalDoSomething()
{
/* UserDataAccessEntityClass */ $CurrentUser = new UserDataAccessEntityClass();
/* integer */ newKey = $CurrentUser->CalculateNewKey();
$CurrentUser->setUserKey(newKey);
$CurrentUser->setUserName("JohnDoe");
$CurrentUser->setUserPassword("a9b8c7d6e5f4");
$CurrentUser->setUserIsActive(true);
$CurrentUser->Insert();
$CurrentUser = null;
} //
...
I use the casting of the "IsActive" field from "integer" to "boolean", as an example,
could be other stuff like truncating a string that is too long.
Some developers will complain, and suggest to do this in the business logic entity.
(3) You generate a Business-Logic-Layer entity like this, wheter manually or tool-generated:
abstract class FieldsUserLogicAccessEntityClass
{
/* integer */ protected $fieldUserKey = 0;
/* string */ protected $fieldUserAlias = "";
/* string */ protected $fieldUserPassword = "";
/* integer */ protected $fieldUserIsActive = 0;
/* void */ public function DoSomeThing();
} // class
In other file, this, (I skip the methods code):
class UserLogicAccessEntityClass extends FieldsUserLogicAccessEntityClass
{
public /* integer */ function getUserKey();
public function setUserKey(/* integer */ $newValue);
public /* string */ function getUserAlias();
public function setUserAlias(/* string */ $newValue);
public /* string */ function getUserPassword();
public function setUserPassword(/* string */ $newValue);
public /* string */ function getUserPassword();
public function setUserPassword(/* string */ $newValue);
public /* bool */ function getUserIsActive();
public function setUserIsActive(/* bool */ $newValue);
/* void */ public function DoAnotherThing();
} // class
...
/* void */ function GlobalDoSomething()
{
/* UserLogicAccessEntityClass */ $CurrentUser = new UserLogicAccessEntityClass();
$CurrentUser->setUserName("JohnDoe");
$CurrentUser->setUserPassword("a9b8c7d6e5f4");
if ($CurrentUser->TryLogin())
{
echo "Welcome !!!";
}
else
{
echo "Security Alert !!!";
}
$CurrentUser = null;
} //
...
These method helps a lot when, you have to add or remove or update fields in the database, but, its more complex, so a entity generator is required.
The idea its that a tool generates the "abstract with fields" entity classes,
and you modify manually the "concrete child" entity classes, when required.
And in real world programming, modifying hte entities manually, happens a lot.
Is it ok to instanciate User class more than once in ManageUser page (one instance for Add, one for Edit and so on) or should instanciate it only once for a single page?
Its ok, to instantiate an entity class, several times, but, its more common, to have them as temporally objects int methods. In case of objects like the "currently logged user", you may use a "singleton" object.
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 am writing a Product Class, the job of the Class is to take in a product id and to output the corresponding product name.
For e.g.:
$Product = new Product;
$Product->id = "ff62";
$Product->readId();
echo $Product->name;
// returns a string with at least 5 characters.
My PHPUnit test method looks like:
$Product = new Product;
$Product->id = "ff62"; // needs to be a variable
$Product->readId();
$this->assertEquals(gettype($Product->name), 'string');
However, my aim is to check for a different product ID each time instead of ff62 which may or may not exist in database.
Ideally one should be able to define the id variable during testing.
What is the best way to test for dynamic variables as such?
Faker is one way to do it, but I would hesitate to say it is the "best way."
Your requirements are:
1. Test a set of different variables.
2. Those variables may or may not exist in the database.
But you have several problems with how you have designed this test:
You are using gettype() and comparing it to string. This is a bad idea. If product 54 is "foo", and your test is returning "bar" for 54, it will pass. This is Programming by Coincidence. I.e., it works, but not on purpose.
The way you're setting this up does not really deal with the problem. While Faker can create fake data, it cannot automatically create known good and known bad data for your specific system and business cases. I would assume that you want to test known good data + expected results as well as known bad data + expected exceptions.
The proper way to structure this test is using #dataProvider and database fixtures / testing.
Here's what that would look like:
<?php
namespace Foo\Bar;
use PHPUnit\DbUnit\TestCaseTrait;
use PHPUnit\Framework\TestCase;
use \PDO;
USE \Exception;
class ProductTest extends TestCase
{
use TestCaseTrait;
// only instantiate pdo once for test clean-up/fixture load
static private $pdo = null;
// only instantiate PHPUnit_Extensions_Database_DB_IDatabaseConnection once per test
private $conn = null;
final public function getConnection()
{
if ($this->conn === null) {
if (self::$pdo == null) {
self::$pdo = new PDO($GLOBALS['DB_DSN'], $GLOBALS['DB_USER'], $GLOBALS['DB_PASSWD']);
}
$this->conn = $this->createDefaultDBConnection(self::$pdo, $GLOBALS['DB_DBNAME']);
}
return $this->conn;
}
public function getDataSet()
{
return $this->createMySQLXMLDataSet('tests/unit/testdata/sampleproductdata.xml');
}
/**
* Tests products against known good data in the database fixture.
* #param $id
* #param $expectedName
* #dataProvider providerTestProduct
*/
public function testProduct($id, $expectedName) {
$Product = new Product;
$Product->id = $id;
$Product->readId();
$this->assertSame($expectedName, $Product->name);
}
/**
* Provides data that should appear in the database.
* #return array
*/
public function providerTestProduct() {
// id , expectedName
return [ [ "ff62" , "fooproduct"]
, [ "dd83" , "barproduct"]
, [ "ls98" , "bazproduct"]
];
}
/**
* Tests products against known-bad data to ensure proper exceptions are thrown.
* #param $id
* #param $expectedName
*/
public function testProductExceptions($id, $expectedName) {
$Product = new Product;
$Product->id = $id;
$this->expectException(Exception::class);
$Product->readId();
}
/**
* Provides test data that when queried against the database should produce an error.
* #return array
*/
public function providerTestProductExceptions() {
// id , expectedName
return [ [ "badtype" , "fooproduct"] //Wrong id type
, [ "aaaa" , "barproduct"] //Does not exist
, [ null , "bazproduct"] //null is a no-no.
];
}
}
Here's a breakdown:
Use namespaces. Because it's 2018, and it's the right thing to do.
Use use to declare what classes you're using in the test.
Use TestCaseTrait to properly setup your TestCase
The private $pdo variable will hold your database connection for your class / test.
getConnection() is required. This will use the database, username, and password you have configured in your phpunit.xml file. Reference
getDataSet() goes and reads your datasource (fixture), then, truncates your database on your workstation / dev box, imports all the data from the fixture to put the database in a known state. (Be sure to backup your data before you do this. It's lossy on purpose. Never execute on production).
Next, you have two pairs of methods for the test cases: a test and a data provider.
The data provider in each case provides an ID you want to test, and the expected result. In the case of testProduct and providerTestProduct, we are providing ID that should exist in the database (as ensured by the fixture above). We can then check that Product::readId() is not only returning a string, but is actually returning the correct string.
In the second case, testProductException() and providerTestProductException(), we are intentionally sending bad values to the class to trigger exceptions, and then checking to make sure those bad values actually produces the desired behavior: failure / thrown exceptions.
You can randomise your dataset using random number generation.
$value = dechex(random_int(0, 255)).dechex(random_int(0, 255));
$Product = new Product;
$Product->id = $value;
$Product->readId();
$this->assertEquals('string', gettype($Product->name));
$this->assertEquals($value, $Product->name);
One usually puts the expected value to the left, and the actual one to the right.
I found out the best way to do this is to use Faker.
https://github.com/fzaninotto/Faker
While I was trying to test against different instances of a Product, I could definitely use Faker to randomly generate a product and test if the Product was being retrieved properly from the database.
Although majorly used in Laravel, Symfony, etc. It's quite easy to use even in custom PHP frameworks.
It's the first time I run into this problem. I want to create a doctrine object and pass it along without having to flush it.
Right after it's creation, I can display some value in the object, but I can't access nested object:
$em->persist($filter);
print_r($filter->getDescription() . "\n");
print_r(count($filter->getAssetClasses()));
die;
I get:
filter description -- 0
(I should have 19 assetClass)
If I flush $filter, i still have the same issue (why oh why !)
The solution is to refresh it:
$em->persist($filter);
$em->flush();
$em->refresh($filter);
print_r($filter->getDescription() . " -- ");
print_r(count($filter->getAssetClasses()));
die;
I get:
filter description -- 19
unfortunately, you can't refresh without flushing.
On my entities, I've got the following:
in class Filter:
public function __construct()
{
$this->filterAssetClasses = new ArrayCollection();
$this->assetClasses = new ArrayCollection();
}
/**
* #var Collection
*
* #ORM\OneToMany(targetEntity="FilterAssetClass", mappedBy="filterAssetClasses", cascade={"persist"})
*/
private $filterAssetClasses;
public function addFilterAssetClass(\App\CoreBundle\Entity\FilterAssetClass $filterAssetClass)
{
$this->filterAssetClasses[] = $filterAssetClass;
$filterAssetClass->setFilter($this);
return $this;
}
in class FilterAssetClass:
/**
* #var Filter
*
* #ORM\ManyToOne(targetEntity="App\CoreBundle\Entity\Filter", inversedBy="filterAssetClasses")
*/
private $filter;
/**
* #var Filter
*
* #ORM\ManyToOne(targetEntity="AssetClass")
*/
private $assetClass;
public function setFilter(\App\CoreBundle\Entity\Filter $filter)
{
$this->filter = $filter;
return $this;
}
Someone else did write the code for the entities, and i'm a bit lost. I'm not a Doctrine expert, so if someone could point me in the good direction, that would be awesome.
Julien
but I can't access nested object
Did you set those assetClasses in the first place?
When you work with objects in memory (before persist), you can add and set all nested objects, and use those while still in memory.
My guess is that you believe that you need to store objects to database in order for them to get their IDs assigned.
IMHO, that is a bad practice and often causes problems. You can use ramsey/uuid library instead, and set IDs in Entity constructor:
public function __construct() {
$this->id = Uuid::uuid4();
}
A database should be used only as a means for storing data. No business logic should be there.
I would recommend this video on Doctrine good practices, and about the above mentioned stuff.
Your problem is not related to doctrine nor the persist/flush/refresh sequence; the problem you describe is only a symptom of bad code. As others have suggested, you should not be relying on the database to get at your data model. You should be able to get what you are after entirely without using the database; the database only stores the data when you are done with it.
Your Filter class should include some code that manages this:
// Filter
public function __contsruct()
{
$this->filterAssetClasses = new ArrayCollection();
}
/**
* #ORM\OneToMany(targetEntity="FilterAssetClass", mappedBy="filterAssetClasses", cascade={"persist"})
*/
private $filterAssetClasses;
public function addFilterAssetClass(FilterAssetClass $class)
{
// assuming you don't want duplicates...
if ($this->filterAssetClasses->contains($class) {
return;
}
$this->filterAssetClasses[] = $class;
// you also need to set the owning side of this relationship
// for later persistence in the db
// Of course you'll need to create the referenced function in your
// FilterAssetClass entity
$class->addFilter($this);
}
You may have all of this already, but you didn't show enough of your code to know. Note that you should probably not have a function setFilterAssetClass() in your Filter entity.
If i have a Car class like this:
class Car
{
/**
* #var string $model
*/
private $model;
/**
* Makes a new car based on a model
*
* #param string $model Initializes the model
*/
public function __construct($model) {
$this->model = $model;
}
/**
* Gets the car model
*
* #return string The model
*/
public function getModel() {
return $this->model;
}
}
And a CarValidator like this:
class CarValidator
{
public function isValidated(Car $car) {
if (empty($car->getModel())) {
return false;
}
return true;
}
}
And my usage is like this:
$bmw = new Car('bmw');
How do i validate my BMW car before instantiating it?
As I see it you have 2 choices.
You stop invalid cars being constructed, in which case the rules for a valid car need to be part of the car class
You use a factory to create the cars and the factory delegates to a CarValidator to validate the cars after construction. This means validation is separate (which seems to be something you value) but means that cars could be built in an invalid state. This might be ok if validation is contextual and your cars are reused and 'valid' means something different in one place or another.
Which of these is most applicable I think that fundamentally it comes down to what is 'valid' in your situation and whether validity is universally applicable. The truth probably means a bit of both.
For example it might be that having an empty model is invalid for all cars, but that some model colour combinations are invalid at the moment. In this case your cars constructor would throw an exception if someone tried to create a car which had an empty string for the model, as this is never valid.
Your validator might check that the car is valid after construction to ensure that the model/colour combination you have specified exists at the moment. These rules might change and so would be more applicable to external validation (you may have different acceptable model in certain countries or different validations rules based on some other factors). These can live in your CarValidator class(es).
Any fundamental invariants of your object that parts of your program will rely on (like the fact that the model is not empty, should be validated and enforced by the object itself, as these are not 'business rules' per-se but invariants of the system.
I'd likely go with:
public class CarFactory
{
private ICarValidator validator;
public CarFactory(ICarValidator validator){ this.validator=validator;}
public Car Create(string model)
{
Car car = new Car(model);
if (validator.IsValid(car))
return car;
throw new InvalidCarException(car);
}
}
public Car
{
private string model;
public Car(string model)
{
if (model=="")
throw new EmptyModelException();
this.model=model;
}
}
Which gives you separation of you invariants (enforced by Car) from your business logic (encapsulated in CarValidator) and disallows your users from getting hold of invalid car instances at all.
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.