I use zend framework and I would like to have your advice to modelize my classes.
I have 3 classes Patrimony.php Project.php and Version.php.
Version extends Project extends Patrmimony.
In other hand I have a folder structure like this /data/patrimonies/projects/versions/
I don't know if I have to use a Design Pattern or something like that, if patrimony have a variable $_project which contains an instance of project, how do I instanciate my class (__construct() params). In brief I am very confusing with OOP.
Please light me :-)
class Admin_Model_Patrimony
{
}
class Admin_Model_Project extends Admin_Model_Patrimony
{
}
class Admin_Model_Version extends Admin_Model_Project
{
}
I have look to the composition pattern and it seems it is interessant when you want to compose with different number of leaves. In my case There are always one patrimony one project and one version for any entities.
My goal is sometimes I just use informations about a patrimony othertimes I only needs information about project.
I show you how I have design my classes, see the last lines for execution process and expected values.
I think this code doesn't work so how do I do to set variable of the parents when I instanciate a child?
// Version
class Version extends Project
private $this->_patId;
private $this->_proId;
private $this->_verId;
{
public function __contruct($patrimonyId, $projectId, $versionId)
{
$this->_patId = $patrimonyId;
$this->_proId = $projectId;
$this->_verId = $versionId;
}
public function getVersionId()
{
return $this->_verId;
}
// Project
class Project extends Patrimony
private $this->_patId;
private $this->_proId;
{
public function __contruct($patrimonyId, $projectId)
{
$this->_patId = $patrimonyId;
$this->_proId = $projectId;
}
public function getProjectId()
{
return $this->_proId;
}
// Patrimony
class Patrimony
private $this->_patId;
{
public function __contruct($patrimonyId)
{
$this->_patId = $patrimonyId;
}
public function getPatrimonyId()
{
return $this->_patId;
}
// Execution
$version = new Version(1,2,3);
$version->getVersionId(); // should return 1
$version->getProjectId(); // should return 2
$version->getPatrimonyId(); // should return 3
Related
I have the following code (simplified and details changed for this question):
class model_to_be_tested {
// an array that holds a collection of thing A
public $array_of_thing_A;
// already doing constructor injection for the data object
public __construct($data_object) {
// details here
}
public function add_new_thing_A($has_relationship) {
$thing_A = new Thing_A();
$thing_A->is_thing = true;
$thing_A->has_relationship_with_thing_B = $has_relationship;
if ($has_relationship) {
$thing_B = new Thing_B();
$thing_A->relationship_with = $thing_B;
}
$this->array_of_thing_A[] = $thing_A;
}
}
In the above example, I have to decouple the instantiation of Thing_A and Thing_B from the add_new_thing method. However, a simple constructor injection will not do for these two classes. This is because I need fresh instances of Thing_A and Thing_B every time add_new_thing is called so that Thing_A can be added to the array_of_thing_A.
How can I make this function unit testable? And more specifically for me to use mocks of Thing_A and Thing_B in testing this function in PHPUnit?
Any suggestions with code example will be appreciated.
Additionally, I would like to mention that Thing_A and Thing_B are used elsewhere in the codebase that I am working with and the code using these classes will eventually need to be unit tested. Solutions that are too localized and would cause repeated code elsewhere will not be too ideal in my situation. Thank you.
As commenter xmike mentioned, you could use the factory pattern. You would inject a factory object through the ctor as well. Then you could have a factory that provides simplified instances of your Thing_A and Thing_B.
class ThingFactory {
public function buildThingA() {
return new Thing_A(); // or MockThing_A if you go the ducktyping route
}
public function buildThingB() {
return new Thing_B();
}
}
class model_to_be_tested {
// an array that holds a collection of thing A
public $array_of_thing_A;
// you could go the typed route and have an interface for this
private $factory;
// already doing constructor injection for the data object
public __construct($data_object, $factory) {
// details here
$this->factory = $factory;
}
public function add_new_thing_A($has_relationship) {
$thing_A = $this->factory->buildThingA();
$thing_A->is_thing = true;
$thing_A->has_relationship_with_thing_B = $has_relationship;
if ($has_relationship) {
$thing_B = $this->factory->buildThingB();
$thing_A->relationship_with = $thing_B;
}
$this->array_of_thing_A[] = $thing_A;
}
}
PHP is such a strange language, you can't assign a class to a variable. But you can do it as a string. Inject ThingA and ThingB on the constructor as strings. You can call new on the string member.
class ThingA {};
class ThingB{};
class model_to_be_tested {
// an array that holds a collection of thing A
public $array_of_thing_A;
private $_thingA;
private $_thingB;
public function __construct($data_object, $thingA, $thingB) {
$this->_thingA = $thingA;
$this->_thingB = $thingB;
}
public function add_new_thing_A($has_relationship) {
$thing_A = new $this->_thingA();
if ($has_relationship) {
$thing_B = new $this->_thingB();
}
$this->array_of_thing_A[] = $thing_A;
}
}
$model = new model_to_be_tested('foo', 'ThingA', 'ThingB');
$model->add_new_thing_A(true);
There's a live version here: https://repl.it/#rmoskal/InconsequentialAnotherGermanshorthairedpointer
Or provide a static constructor for the class.
Trying to get a grasp on MVC, I wrote the following:
//Model/Model.php
class Model
{
private $dbh;
public function __construct()
{
$this->dbh = dbConnect();
}
private function dbConnect()
{
//return database connection
}
public function crudMethod()
{
//interact with databse using $this->dbh
}
}
//Controller/Controller.php
class Controller
{
public $modelConnection;
public function __construct()
{
$this->modelConnection = new Model();
}
}
//View/index.php
$obj = new Controller;
$obj->modelConnection->crudMethod();
?>
Looking at my code, I feel like I'm completely missing the point here.
The controller doesn't serve real value, I might as well instantiate the Model class directly.
Should a Model class ever be instantiated? Inside or outside the controller? Or is it bad practice altogether?
How would I improve this structure to support the MVC paradigm?
Your problem is that you are putting information in your layers that should not be there. Your Data Access layer does not need to know HOW it will be connected, they just need to know that there is a properly configured driver to get data from.
In the same way, you are giving your controllers responsibilities they doesn't need: creating models.
Why this is bad?
Imagine that for some reason, you change the dependencies of your model classes. Like if you now have to store data from the model in two different databases at the same time for redundancy.
How would you do this? Change all of your controllers and alter the way you instantiate models inside them?
That's a lot of work! And bug prone.
So, how can I solve this?
I like to do this using Dependency Injection Principle and the Factory Pattern.
Why a factory?
To decouple the creation of my Model from who uses it. My default model factory creates Models that requires only one database connection to work:
namespace MyApplication\Model\Factory;
use MyApplication\Storage\StorageInterface;
interface FactoryInterface {
public function create($modelClassName);
}
class WithStorage implements FactoryInterface {
const MODEL_NAMESPACE = '\\MyApplication\\Model\\';
private $storage;
public function __construct(StorageInterface $storage) {
$this->storage = $storage;
}
public function create($modelClassName) {
$refl = new \ReflectionClass(self::MODEL_NAMESPACE . $modelClassName);
try {
return $refl->newInstance($this->storage);
} catch (\ReflectionException $e) {
throw new RuntimeException("Model {$modelClassName} not found");
}
}
}
Now, for your model classes that requires a storage, you can create a structure like this:
namespace MyApplication\Storage;
interface StorageInterface {
public function insert($container, array $data);
public function update($container, array $data, $condition);
// etc..
}
class PdoStorage implements StorageInterface {
private $dbh;
public function __construct(\PDO $dbh) {
$this->dbh = $dbh;
}
public function insert($container, array $data) {
// impl. omitted
}
public function update($container, array $data, $condition) {
// impl. omitted
}
}
So, if you have the following class:
namespace MyApplication\Model;
use MyApplication\Storage\StorageInterface;
// Entity impl. will be omitted for brevity.
use MyApplication\Entity\EntityInterface;
abstract class AbstractApplicationModel {
private $containerName;
private $storage;
protected function __construct($name, StorageInterface $storage) {
$this->containerName = (string) $name;
$this->storage = $storage;
}
public function save(EntityInterface $entity) {
// impl. omitted
}
public function delete(EntityInterface $entity) {
// impl. omitted
}
}
class UserModel extends AbstractApplicationModel {
public function __construct(StorageInterface $storage) {
parent::__construct('users', $storage);
}
}
With this, we solve our problem with the coupling within the model. Ok.
So, with all this, how can I get a Model component that is ready to store my data from my controller?
Directly:
namespace MyApplication\Controller;
use MyApplication\Model\UserModel;
use MyApplication\Storage\PDOStorage;
use MyApplication\Entity\User;
class UserController {
public function onCreate() {
$model = new UserModel(new Storage(new \PDO(...))); // Here's our problem
$entity = User::createFromArray([
'name' => 'John',
'surname' => 'Doe',
]);
try {
$model->save($entity);
} catch (Exception $e) {
echo 'Oops, something is wrong: ' . $e;
}
}
}
If you have just one model to deal in your whole application, you are ready to go. But you have several of them, then you will have problems.
What if I no longer want to use PDO as my storage driver and work with MySQLi? What if I don't want to use a RDBMS anymore, instead I want to store my data in plain text files?
This way, you'd have to change the implementation of all controllers (which violates the OCP). Lots of repetitive work to do. I hate this!
Oh wait! We have the freaking factory to create models for us! So, let's use it!
namespace MyApplication\Controller;
use MyApplication\Model\Factory\FactoryInterface;
use MyApplication\Entity\User;
abstract class AbstractController {
private $modelFactory;
public function __construct(ModelFactory $factory) {
$this->modelFactory = $factory;
}
public function getModelFactory() {
return $this->modelFactory;
}
}
class UserController {
public function onCreate() {
$model = $this->getModelFactory()->create('UserModel'); // now it's better
$entity = User::createFromArray([
'name' => 'John',
'surname' => 'Doe',
]);
try {
$model->save($entity);
} catch (Exception $e) {
echo 'Oops, something is wrong: ' . $e;
}
}
}
Now, to have all this working together, you have to do some setup on your bootstrap/front controller:
use MyApplication\Storage\PDOStorage;
use MyApplication\Model\Factory\WithStorage as ModelFactory;
$defaultPDODriver = new \PDO(...);
$defaultStorage = new PdoStorage($defaultPDODriver);
$defaultModelFactory = new ModelFactory($defaultStorage);
$controller = new UserController($defaultModelFactory);
Now, what do I do if I want to change my storage engine to plain text files?
$defaultStorage = new PlainFileStorage('/path/to/file'); // just this
Now, what do I do if I want to change my storage engine to one custom implementation that have 2 distinct database to save the same data?
$master = new PdoStorage(new \PDO(...));
$slave = new PdoStorage(new \PDO(.......));
$defaultStorage = new RedundancyStorage($master, $slave);
See? Now the way you store information is irrelevant to your models.
The same way, if you have some crazy business logic that changes the way your models do things based on the setup, you can also change your model factory:
$defaultModelFactory = new SomeCrazyModelFactory(...);
Your controllers are not even aware that your models changed (of course, you'd have to respect the same interface to be allowed to do this interchangeably).
That's a possible way, it's more or less how I do, but there are a few other possibilities.
You are mixing concerns in your interpretation of a Model. In Model-View-Controller, a Model is some type of knowledge, which could be a single class representing a domain concept or a more complex structure e.g. a Page or Document in the context of a CMS.
However in your example, your Model class is really just a Singleton holder for your database connection, that is not its purpose.
A database connection is a service that is provided across the MVC tiers, which should preferably be wired using dependency injection. You should not instantiate services in Controller code.
Whether instantiating Model objects within a controller makes sense depends largely on the context, but there is no rule that forbids it.
I am looking to incorporate a testing framework into a project I am building and came across Enhance PHP which I like but I am having some difficulty finding relevant information on-line since "enhance php" is such a commonly used phrase.
Has anyone worked with this framework that might be able to point me toward some helpful guide? Have you worked with a unit test framework that you think is amazingly better?
Thanks in advance.
In response to Gotzofter, this is the class to be tested:
<?php
include_once('EnhanceTestFramework.php');
class ExampleClass
{
private $OtherClass;
function __construct($mock = null)
{
if ($mock == null)
$this->OtherClass = new OtherExampleClass();
else
$this->OtherClass = $mock;
}
public function doSomething()
{
return $this->OtherClass->getSomething(1, 'Arg2');
}
}
class OtherExampleClass
{
public function getSomething()
{
return "Something";
}
}
class ExampleClassTests extends \Enhance\TestFixture
{
public function setUp()
{
}
public function tearDown()
{
}
public function verifyWithAMock()
{
$mock = \Enhance\MockFactory::createMock('OtherExampleClass');
$mock->addExpectation(
\Enhance\Expect::method('getSomething')
->with(1, 'Arg2')
->returns('Something')
->times(1)
);
$target = new ExampleClass($mock);
$result = $target->doSomething();
\Enhance\Assert::areIdentical("Something", $result);
$mock->verifyExpectations();
}
}
\Enhance\Core::runTests();
look at my constructor for ExampleClass.
Because enhance-php's site example injects the $mock object by calling new ExampleClass($mock), I am forced to change my ExampleClass constructor to handle a $mock as an input parameter.
Do I have to handle this for all classes that I want to subject to unit testing with the framework?
Thanks.
This:
function __construct()
{
$this->OtherClass = new OtherExampleClass;
}
Should be:
function __construct($otherClass)
{
$this->OtherClass = $otherClass;
}
Your mock is never injected at this point in your test:
$target = new ExampleClass($mock);
One thing I would recommend no matter what testing framework you are using is type-hinting against the expected class, or interface.
<?php
class ExampleClass
{
private $OtherClass; // OtherClass instance
public function __construct(OtherClass $OtherClass=null)
{
// ...
}
}
I'm no di expert, but I don't see the problem in letting each class call new if an instance isn't provided for a particular dependency. You could also of course take the approach where you use setter methods to configure dependencies.
<?php
class class ExampleClass
{
private $OtherClass; // OtherClass instance
public function setOtherClass(OtherClass $OtherClass)
{
$this->OtherClass = $OtherClass;
}
}
It is lame that the ExampleClass in the sample code doesn't even define the doSomething method from the ExampleDependencyClassTests, but if I understand correctly it looks like Enhance PHP is not forcing you to take a particular style of dependency injection. You can write the test class however you want, so for example if you took the setter method approach I mentioned above, you could change the example mock code to
<?php
class ExampleDependencyClassTests extends \Enhance\TestFixture
{
public function verifyWithAMock()
{
$mock = \Enhance\MockFactory::createMock('ExampleDependencyClass');
$mock->addExpectation(
\Enhance\Expect::method('getSomething')
->with(1, 'Arg2')
->returns('Something')
->times(1)
);
$target = new ExampleClass();
$target->setExampleDependencyClass($mock);
$result = $target->doSomething();
$mock->verifyExpectations();
}
}
Of course it would probly make sense to make the appropriate revisions to the ExampleClass!
<?php
class ExampleClass
{
private $ExampleDependencyClass;
public function addTwoNumbers($a, $b)
{
return $a + $b;
}
public function setExampleDependencyClass(
ExampleDependencyClass $ExampleDependecyClass
) {
$this->ExampleDependecyClass = $ExampleDependecyClass;
}
public function doSomething($someArg)
{
return 'Something';
}
}
I've worked with PHPUnit quite a bit, and honestly you'll have to face the same challenges with Mocks there. My 2 cents, try to model your tests without Mocks if possible ;)
There is a tutorial on NetTuts titled Testing Your PHP Codebase With Enhance PHP, which will definitely help you to get started.
And there is a Quick Start Guide on Enhance PHP.
I'm wondering if anyone could give me a suggestion for to best handle this situation:
I have several systems from which to pull data to display on a single PHP-driven website. The type of information will be the same across systems (contacts, addresses, etc) but the way I pull data (MS-SQL, XML, REST) will not.
I want to create a class, or set of classes, for each of the connection types and use simple methods such as getContact(), getAddress(), etc. I am wondering how best to structure this.
The most obvious way that comes to mind means creating classes for each connection type, like:
class.sys_mysql.php. class.sys_xml.php, etc
But then won't I be duplicating the methods in each class? Maybe that's OK, but I'm curious if there's a better way, as far as future maintenance goes.
Maybe I should simply isolate the queries/data extraction methods, into separate class files? Classes within classes? Extended classes? I'm less familiar with these.
Any advice would be greatly appreciated.
DC
--------- more info ----------
Hi all. I really appreciate all the great advice. Not to belabor this thread but I'm still a bit confused on how I should break things down. I will try and be a bit more specific:
Basically, I have 3 (more in the future) offices, from which one PHP website pulls information. Each office uses a different CRM, and a different system for interfacing with that CRM. One uses MSSQL, another XML requests, etc.
Each office wants to display information similarly on the website, but there are minor differences. There may be more differences in the future. However, there are by far more similarities, and so I want to capitalize on higher level functions like getContacts($id) which are shared between them.
I am trying to write these classes so I can:
1) use higher level methods to pull data easily
2) account for different ways of pulling data (xml,sql,etc)
3) account for differences between how data is displayed on the website (office 1, office 2, office 3)
4) manage the connection credentials for each office and allow for expandability_
5) I should also mention that I will be creating separate classes for reporting, sending out automated e-mails, calculating finances...separate modules that will need to use existing classes to pull data.
I realize that some of the examples here see to cover 1 and 2, but I am confused as to how to get 3, 4 and 5 working with 1 and 2.
I really appreciate the help.
DC
This is what Interfaces are for.
You define the methods required to interact with the data in an Interface, and then you create classes that implement that Interface
If some of the systems have similar access models (i.e. perhaps two different DB Servers, but both are accessed using PDO) you could abstract it further and put the "low level" functionality into service-specific classes (which implement an Interface) and then a higher-level class which defines the actual methods you use.
Another option is that you could put the "common" methods (those that are identical or can be made idetntical with service-type checks) into a base class, which all others extend.
Example for option one:
interface DataModel {
public function findContacts($search);
public function getContact($id);
public function findAddresses($search);
public function getAddress($id);
}
class XMLDataModel implements DataModel {
public function findContacts($search) {
...
}
public function getContact($id) {
...
}
public function findAddresses($search) {
...
}
public function getAddress($id) {
...
}
}
class RESTDataModel implements DataModel {
public function findContacts($search) {
...
}
public function getContact($id) {
...
}
public function findAddresses($search) {
...
}
public function getAddress($id) {
...
}
}
As you can see, you simply define an Interface, which specifies which methods a class must implement.
If you had two very similar classes, perhaps one for MySQL and one for PostreSQL, and you can't/don't want to combine them into a single PDO class, you could do the following:
class PDODataModel implements DataModel {
private $model;
public function __construct ($serverType) {
if ($serverType === 'mysql') {
$this->model = new MySQLPDODataModel();
}
elseif ($serverType === 'postgresql') {
$this->model = new PostgresQLPDODataModel();
}
}
public function findContacts($search) {
// common logic about $search, perhaps checking it's a valid search?
$result = $this->model->searchForContacts($search);
// more common logic, maybe higher level filtering..
return $result;
}
public function getContact($id) {
...
}
public function findAddresses($search) {
...
}
public function getAddress($id) {
...
}
}
interface PDODataModelDriver {
public function searchForContacts($search);
}
class MySQLPDODataModel extends PDODataModel implements PDODataModelDriver {
public function searchForContacts($search) {
// MySQL-specific query to search for contacts
}
}
class PostgresSQLPDODataModel extends PDODataModel implements PDODataModelDriver {
public function searchForContacts($search) {
// PostgreSQL-specific query to search for contacts
}
}
The other option I mentioned was to work in the opposite direction:
abstract class PDODataModel implements DataModel {
protected $pdo;
protected $dsn;
public function __construct () {
$this->pdo = new PDO($this->dsn);
}
public function findContacts($search) {
// common logic about $search, perhaps checking it's a valid search?
$result = $this->searchForContacts($search);
// more common logic, maybe higher level filtering..
return $result;
}
public function getContact($id) {
...
}
public function findAddresses($search) {
...
}
public function getAddress($id) {
...
}
}
class MySQLPDODataModel extends PDODataModel {
protected $dsn = 'mysql:dbname=testdb;host=127.0.0.1';
protected function searchForContacts($search) {
// MySQL-specific query to search for contacts
}
}
class PostgresSQLPDODataModel extends PDODataModel {
protected $dsn = 'pgsql:host=localhost;port=5432;dbname=testdb';
protected function searchForContacts($search) {
// PostgreSQL-specific query to search for contacts
}
}
This is a classical example of a strategy design patter. Your first mind was absolutely fine, but if you're repeating yourself in each class you should consider creation of a abstract class that will handle the common code.
So it could look like this:
$myService = new MyService(new XMLReader('/path/to/file'));
echo $myService->getContanct('abc')->getName();
And skeleton of your classes:
class MyService {
private $reader;
public function __construct(ReaderInterface $reader) {
$this->reader = $reader;
}
// ...
public function getContacnt($id) {
$contact = $this->reader->getContact($id);
// do some extra stuff here
return $contact;
}
}
interface ReaderInterface {
public function getContanct($id);
public function getAddress($id);
}
abstract class AbstractReader implements ReaderInterface {
protected $loaded = false;
protected $data = array();
abstract protected function load();
public function getContanct($id) {
if ($this->loaded == false) {
$this->load();
$this->loaded = true;
}
return $this->data['contact'][$id];
}
}
class XMLReader extends AbstractReader {
public function __construct($filepath) {
...
}
protected function load() {
...
foreach (...) {
$this->data[...] = ...;
}
}
}
class MSSQLReader extends AbstractReader {
public function __construct(PDO $dbh) {
...
}
protected function load() {
...
while ($row = $stmt->fetchRow()) {
$this->data[...] = ...;
}
}
}
EDIT (2011-03-07) - According to your comment.
PHP supports variable variables (new $type()) but never use this! It's a horrible, and if overused make code really crappy.
This is a yet another example of a "classical issue". Use a factory pattern (depending on the complexion of the creation you might want to use more abstract variety of this pattern - abstract factory
When you need to dynamically determine class name (eg. from variable) use reflection API to instate an object.
You should create an object-storage mapping layer for each data source, which instantiates the objects into storage agnostic model objects. See http://martinfowler.com/eaaCatalog/dataMapper.html
If you have control over the structure of your data formats, I suggest you serialize your data in a consistent way (especially in XML) and provide drivers for each data format.
For instance, every driver will have 'findAll', 'getOne', 'count', etc. methods. The driver can be given a model to populate with the retrieved data.
abstract class DataDriver {
function __construct($model) {}
abstract public function findAll();
abstract public function getOne();
abstract public function count();
// ...
}
class XMLDriver extends DataDriver {
// implements all the methods
}
class SQLDriver extends DataDriver {
// implements all the methods
}
class Contact {
public var $firstName;
public var $lastName;
function getFullName() {
return trim($this->firstName . ' ' . $this->lastName);
}
}
$accessor = new SQLDriver('Contact');
$contacts = $accessor->findAll();
If your data will be serialized in an uncontrolled manner, the approach you suggest is the best. Just make sure to separate your models (e.g. Address book, Contact) from the method of retrieval (eg. get_address_book_xml, get_address_book_sql, etc.)
Of course there are many ways of separating your models from your data-mapping driver. The importance is you find the solution that works best for you given that you're using such different formats.
I'm working on creating a domain layer in Zend Framework that is separate from the data access layer. The Data Access Layer is composed to two main objects, a Table Data Gateway and a Row Data Gateway. As per Bill Karwin's reply to this earlier question I now have the following code for my domain Person object:
class Model_Row_Person
{
protected $_gateway;
public function __construct(Zend_Db_Table_Row $gateway)
{
$this->_gateway = $gateway;
}
public function login($userName, $password)
{
}
public function setPassword($password)
{
}
}
However, this only works with an individual row. I also need to create a domain object that can represent the entire table and (presumably) can be used to iterate through all of the Person's in the table and return the appropriate type of person (admin, buyer, etc) object for use. Basically, I envision something like the following:
class Model_Table_Person implements SeekableIterator, Countable, ArrayAccess
{
protected $_gateway;
public function __construct(Model_DbTable_Person $gateway)
{
$this->_gateway = $gateway;
}
public function current()
{
$current = $this->_gateway->fetchRow($this->_pointer);
return $this->_getUser($current);
}
private function _getUser(Zend_Db_Table_Row $current)
{
switch($current->userType)
{
case 'admin':
return new Model_Row_Administrator($current);
break;
case 'associate':
return new Model_Row_Associate($current);
break;
}
}
}
Is this is good/bad way to handle this particular problem? What improvements or adjustments should I make to the overall design?
Thanks in advance for your comments and criticisms.
I had in mind that you would use the Domain Model class to completely hide the fact that you're using a database table for persistence. So passing a Table object or a Row object should be completely under the covers:
<?php
require_once 'Zend/Loader.php';
Zend_Loader::registerAutoload();
$db = Zend_Db::factory('mysqli', array('dbname'=>'test',
'username'=>'root', 'password'=>'xxxx'));
Zend_Db_Table_Abstract::setDefaultAdapter($db);
class Table_Person extends Zend_Db_Table_Abstract
{
protected $_name = 'person';
}
class Model_Person
{
/** #var Zend_Db_Table */
protected static $table = null;
/** #var Zend_Db_Table_Row */
protected $person;
public static function init() {
if (self::$table == null) {
self::$table = new Table_Person();
}
}
protected static function factory(Zend_Db_Table_Row $personRow) {
$personClass = 'Model_Person_' . ucfirst($personRow->person_type);
return new $personClass($personRow);
}
public static function get($id) {
self::init();
$personRow = self::$table->find($id)->current();
return self::factory($personRow);
}
public static function getCollection() {
self::init();
$personRowset = self::$table->fetchAll();
$personArray = array();
foreach ($personRowset as $person) {
$personArray[] = self::factory($person);
}
return $personArray;
}
// protected constructor can only be called from this class, e.g. factory()
protected function __construct(Zend_Db_Table_Row $personRow) {
$this->person = $personRow;
}
public function login($password) {
if ($this->person->password_hash ==
hash('sha256', $this->person->password_salt . $password)) {
return true;
} else {
return false;
}
}
public function setPassword($newPassword) {
$this->person->password_hash = hash('sha256',
$this->person->password_salt . $newPassword);
$this->person->save();
}
}
class Model_Person_Admin extends Model_Person { }
class Model_Person_Associate extends Model_Person { }
$person = Model_Person::get(1);
print "Got object of type ".get_class($person)."\n";
$person->setPassword('potrzebie');
$people = Model_Person::getCollection();
print "Got ".count($people)." people objects:\n";
foreach ($people as $i => $person) {
print "\t$i: ".get_class($person)."\n";
}
"I thought static methods were bad
which is why I was trying to create
the table level methods as instance
methods."
I don't buy into any blanket statement that static is always bad, or singletons are always bad, or goto is always bad, or what have you. People who make such unequivocal statements are looking to oversimplify the issues. Use the language tools appropriately and they'll be good to you.
That said, there's often a tradeoff when you choose one language construct, it makes it easier to do some things while it's harder to do other things. People often point to static making it difficult to write unit test code, and also PHP has some annoying deficiencies related to static and subclassing. But there are also advantages, as we see in this code. You have to judge for yourself whether the advantages outweigh the disadvantages, on a case by case basis.
"Would Zend Framework support a Finder
class?"
I don't think that's necessary.
"Is there a particular reason that you
renamed the find method to be get in
the model class?"
I named the method get() just to be distinct from find(). The "getter" paradigm is associated with OO interfaces, while "finders" are traditionally associated with database stuff. We're trying to design the Domain Model to pretend there's no database involved.
"And would you use continue to use the
same logic to implement specific getBy
and getCollectionBy methods?"
I'd resist creating a generic getBy() method, because it's tempting to make it accept a generic SQL expression, and then pass it on to the data access objects verbatim. This couples the usage of our Domain Model to the underlying database representation.