Flexible finder methods

Not for 'how-to' coding questions but PHP theory instead, this forum is here for those of us who wish to learn about design aspects of programming with PHP.

Moderator: General Moderators

matthijs
DevNet Master
Posts: 3360
Joined: Thu Oct 06, 2005 3:57 pm

Flexible finder methods

Post by matthijs »

As an application grows and model classes get more and more methods, they get messy.

Code: Select all

<?php
class Widget {
  public function findById($id){}
  public function findByName($name){}
  public function findByNameAndyearAndBrand($pars){}
}
 
etc, you get the point.

Without directly going into a full blown ORM kind of layer (I am not looking into abstracting away sql), what kind of finder classes and methods do you use? I was thinking about something like

Code: Select all

 
public function find($fields=null,$sort=null,$limit=null,$groupby=null){}
// or
public function findWhere($options){}
 
The problem of course is that methods like these are pretty easy to build for the most basic queries. But as soon as you throw in some more complicated queries (joins, data conversion, etc), you run against a wall. In that case I could add a general query($rawsql) method, which allows one to run any sql you want. But then you are in fact moving that database access to a layer above the current model class, maybe even to a controller, which I would think is not so good. Maybe unless I created a layer in between.

I understand it's a pretty broad topic, but maybe if we limit the discussion to
- assuming we don't go into a large ORM like style, what would be the next step in cleaning things up?
- how do you make these finder classes and methods?
wei
Forum Contributor
Posts: 140
Joined: Wed Jul 12, 2006 12:18 am

Re: Flexible finder methods

Post by wei »

Something like ibatis will work. In particular, store the sql statements in a separate file, e.g.

sql-mappings.php

Code: Select all

 
<?php
$sql['NameAndyearAndBrand'] = <<<SQL
.... name = :name, ... year = :year
SQL;
return $sql;
 
then a generic finder, pseudo-code

Code: Select all

 
function find($id, $params) {
    $sqls = include('sql-mappings.php');
    $stmt = $pdo->query($sqls[$id]);
    foreach($params as $name=>$value)
          $stmt->bind($name, $value);
   return $stmt->execute();
}
 
usage:

Code: Select all

 
$widgets = $mapper->find('NameAndyearAndBrand', array(':name'=>$name, ':year'=>$year, ':brand'=>$brand));
 
matthijs
DevNet Master
Posts: 3360
Joined: Thu Oct 06, 2005 3:57 pm

Re: Flexible finder methods

Post by matthijs »

Interesting idea.

But if I understand it correctly, with your method you still have a fixed set of parameters for each method (and query). So I still have to write a seperate sql statement in the sql-mappings.php file, for each find action. What your method does help in, is that I don't have to repeat the code to set and prepare the statement and bind all the parameters. certainly with more parameters, all those long lists of
$this->stmt->bindParam($name, $value);
clutter up the code a lot.

What I was also looking for, is ways in which you can have a flexible amount of parameters. So say you do a findWhere($options) in which you can set one or more optional parameters. So that it's easy to use one method for more kinds of queries.
Maybe something like

Code: Select all

 
function findWhere($options= array()){
foreach($options as $option){
  $this->where = implode(' and ',$options);
 // etc
}
}
 
I don't mind having some sql in my model classes (could also be defined as constants or variables), but certainly reducing the amount of methods being needed to create all the select statements would help.
wei
Forum Contributor
Posts: 140
Joined: Wed Jul 12, 2006 12:18 am

Re: Flexible finder methods

Post by wei »

If you are using php 5.3 you can do the following

Code: Select all

 
$sql['NameAndyearAndBrand'] = function ($params) { 
 return 'select * from table where '. implode(' and ', array_keys($params)); 
};
 
and in the finder

Code: Select all

 
function find($id, $params) {
    $sqls = include('sql-mappings.php');
    $sql = is_callable($sqls[$id]) ? $sqls[$id]($params) : $sqls[$id];
    ...
}
 
pre 5.3, the you need to define the function first and then use the function name as the sql statement value. e.g.

Code: Select all

 
function buildNameAndyearAndBrand ($params) { 
 return 'select * from table where '. implode(' and ', array_keys($params)); 
}
$sql['NameAndyearAndBrand'] = 'buildNameAndyearAndBrand';
 
This should move most of the sql statements into one place.
User avatar
inghamn
Forum Contributor
Posts: 174
Joined: Mon Apr 16, 2007 10:33 am
Location: Bloomington, IN, USA

Re: Flexible finder methods

Post by inghamn »

What mathhisj proposes is exactly what I've been doing in my own framework for the past couple years. Collections implement a find() method, but the extend a PDOResultIterator that actually does the assembling of the select and the iterating of the results.

As time goes on during development, this lets you easily add more support for more and more fields to search on, even fields from other tables. If you want to find based on fields from other tables, you just need to also add the join SQL.

The PDOResultIterator iterates over the results, and passes each result back the collection class. The collection class takes the result, loads an ActiveRecord object and returns the ActiveRecord object.

A little bit chatty with the database, but it makes iterative development a breeze.

Code: Select all

 
<?php
/**
 * A collection class for Subscription objects
 *
 * This class creates a select statement, only selecting the ID from each row
 * PDOResultIterator handles iterating and paginating those results.
 * As the results are iterated over, PDOResultIterator will pass each desired
 * ID back to this class's loadResult() which will be responsible for hydrating
 * each Subscription object
 *
 * Beyond the basic $fields handled, you will need to write your own handling
 * of whatever extra $fields you need
 *
 * The PDOResultIterator uses prepared queries; it is recommended to use bound
 * parameters for each of the options you handle
 *
 * @copyright 2009 City of Bloomington, Indiana
 * @license http://www.gnu.org/copyleft/gpl.html GNU/GPL, see LICENSE.txt
 * @author Cliff Ingham <inghamn@bloomington.in.gov>
 */
class SubscriptionList extends PDOResultIterator
{
    /**
     * Creates a basic select statement for the collection.
     * Populates the collection if you pass in $fields
     *
     * @param array $fields
     */
    public function __construct($fields=null)
    {
        $this->select = 'select subscriptions.id as id from subscriptions';
        if (is_array($fields)) {
            $this->find($fields);
        }
    }
 
    /**
     * Populates the collection from the database based on the $fields you handle
     *
     * @param array $fields
     * @param string $sort
     * @param int $limit
     * @param string $groupBy
     */
    public function find($fields=null,$sort='id',$limit=null,$groupBy=null)
    {
        $this->sort = $sort;
        $this->limit = $limit;
        $this->groupBy = $groupBy;
        $this->joins = '';
 
        $options = array();
        $parameters = array();
 
        if (isset($fields['id'])) {
            $options[] = 'id=:id';
            $parameters[':id'] = $fields['id'];
        }
 
        if (isset($fields['application_id'])) {
            $options[] = 'application_id=:application_id';
            $parameters[':application_id'] = $fields['application_id'];
        }
 
        if (isset($fields['person_id'])) {
            $options[] = 'person_id=:person_id';
            $parameters[':person_id'] = $fields['person_id'];
        }
 
        if (isset($fields['waitTime'])) {
            $options[] = 'waitTime=:waitTime';
            $parameters[':waitTime'] = $fields['waitTime'];
        }
 
 
        // Finding on fields from other tables required joining those tables.
        // You can add fields from other tables to $options by adding the join SQL
        // to $this->joins here
 
        $this->populateList($options,$parameters);
    }
 
    /**
     * Loads a single Subscription object for the key returned from PDOResultIterator
     * @param int $key
     */
    protected function loadResult($key)
    {
        return new Subscription($this->list[$key]);
    }
}
 

Code: Select all

 
<?php
/**
 * Base class for iterating over database results
 *
 * @copyright 2006-2009 City of Bloomington, Indiana
 * @license http://www.gnu.org/copyleft/gpl.html GNU/GPL, see LICENSE.txt
 * @author Cliff Ingham <inghamn@bloomington.in.gov>
 */
abstract class PDOResultIterator implements ArrayAccess,SeekableIterator,Countable
{
    protected $select = '';
    protected $joins = '';
    protected $where = '';
    protected $sort = '';
    protected $limit = '';
    protected $groupBy = '';
    protected $parameters;
    private $sql;
 
    protected $list = array();
    private $valid = false;
    private $cacheEnabled = true;
    private $cache = array();
    private $key;
 
    abstract public function find($fields=null,$sort='',$limit=null,$groupBy=null);
    abstract protected function loadResult($key);
 
    /**
     * Assembles the full SQL statement and populates the object with the results
     *
     * This class uses prepared queries and supports bound parameters.
     * In the $options array, specify fields and parameter placeholders.  Send the actual data
     * in the $parameters array.
     * Make sure the number of paramters matches what you've declared in the options.
     * Named parameters are the preferred way to be certain.
     * $options[] = 'fieldname=:val';
     * $parameters[':val'] = $val;
     *
     * @param array $options
     * @param array $parameters
     */
    protected function populateList(array $options=null,array $parameters=null)
    {
        $PDO = Database::getConnection();
 
        // Make sure to clear out any previous list that was created
        $this->list = array();
 
        if (count($options)) {
            $this->where = ' where '.implode(' and ',$options);
        }
        $orderBy = $this->sort ? "order by {$this->sort}" : '';
        $groupBy = $this->groupBy ? "group by {$this->groupBy}" : '';
        $limit = $this->limit ? 'limit '.$this->limit : '';
        $this->sql = "{$this->select} {$this->joins} {$this->where} $groupBy $orderBy $limit";
 
        $query = $PDO->prepare($this->sql);
        $this->parameters = $parameters;
        $query->execute($parameters);
 
        $result = $query->fetchAll();
        if ($result) {
            foreach ($result as $row) {
                $this->list[] = $row['id'];
            }
        }
    }
 
    /**
     * Splits the results up into pages of results
     *
     * @param int $pageSize The number of results per page
     * @return Paginator
     */
    public function getPagination($pageSize) {
        return new Paginator($this,$pageSize);
    }
 
 
    // Array Access section
    /**
     * @param int $offset
     * @return boolean
     */
    public function offsetExists($offset) {
        return array_key_exists($offset,$this->list);
    }
    /**
     * Unimplemented stub requried for interface compliance
     * @ignore
     */
    public function offsetSet($offset,$value) { } // Read-only for now
    /**
     * Unimplemented stub requried for interface compliance
     * @ignore
     */
    public function offsetUnset($offset) { } // Read-only for now
    /**
     * @param int $offset
     * @return mixed
     */
    public function offsetGet($offset)
    {
        if ($this->offsetExists($offset)) {
            if ($this->cacheEnabled) {
                if (!isset($this->cache[$offset])) {
                    $this->cache[$offset] = $this->loadResult($offset);
                }
                return $this->cache[$offset];
            }
            else {
                return $this->loadResult($offset);
            }
        }
        else {
            throw new OutOfBoundsException('Invalid seek position');
        }
    }
 
 
 
    // SPLIterator Section
    /**
     * Reset the pionter to the first element
     */
    public function rewind() {
        $this->key = 0;
    }
    /**
     * Advance to the next element
     */
    public function next() {
        $this->key++;
    }
    /**
     * Return the index of the current element
     * @return int
     */
    public function key() {
        return $this->key;
    }
    /**
     * @return boolean
     */
    public function valid() {
        return array_key_exists($this->key,$this->list);
    }
    /**
     * @return mixed
     */
    public function current()
    {
        if ($this->cacheEnabled) {
            if (!isset($this->cache[$this->key])) {
                $this->cache[$this->key] = $this->loadResult($this->key);
            }
            return $this->cache[$this->key];
        }
        return $this->loadResult($this->key);
    }
    /**
     * @param int $index
     */
    public function seek($index)
    {
        if (isset($this->list[$index])) {
            $this->key = $index;
        }
        else {
            throw new OutOfBoundsException('Invalid seek position');
        }
    }
 
    /**
     * @return Iterator
     */
    public function getIterator()
    {
        return $this;
    }
 
    // Countable Section
    /**
     * @return int
     */
    public function count()
    {
        return count($this->list);
    }
 
    // Getters
    /**
     * @return string
     */
    public function getSelect()
    {
        return $this->select;
    }
    /**
     * @return string
     */
    public function getJoins()
    {
        return $this->joins;
    }
    /**
     * @return string
     */
    public function getWhere()
    {
        return $this->where;
    }
    /**
     * @return string
     */
    public function getGroupBy()
    {
        return $this->groupBy;
    }
    /**
     * @return string
     */
    public function getSort()
    {
        return $this->sort;
    }
    /**
     * @return int
     */
    public function getLimit()
    {
        return $this->limit;
    }
    /**
     * @return array
     */
    public function getParameters()
    {
        return $this->parameters;
    }
    /**
     * @return string
     */
    public function getSQL()
    {
        return $this->sql;
    }
 
    // Cache Enable/Disable functions
    /**
     * Turns on the caching of loaded objects
     *
     * This iterator does one database call to get all the ID from the table,
     * then, it does a database call for each of the rows returned, in order to load the object
     * With caching on, the loaded objects will be saved in case you want to iterate over the
     * result set again
     */
    public function enableCache()
    {
        $this->cacheEnabled = true;
    }
    /**
     * Turns off the caching of loaded object
     *
     * This iterator does one database call to get all the ID from the table,
     * then, it does a database call for each of the rows returned, in order to load the object
     * With caching off you can save a bit of memory by not storing the objects
     * for future iterations
     */
    public function disableCache()
    {
        $this->cacheEnabled = false;
    }
}
 
matthijs
DevNet Master
Posts: 3360
Joined: Thu Oct 06, 2005 3:57 pm

Re: Flexible finder methods

Post by matthijs »

That's a interesting way of doing things. I'll have to study your code some more, it definitely gave me some good ideas.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Re: Flexible finder methods

Post by Christopher »

inghamn, I have seen systems like that. But I wonder if, for all the work, you every really use rewind(), seek() or getOffset() -- rather than just iterating through the result set? I guess the Lazy Loading is nice, but generally if I want random access I can just load all rows and get an array. Otherwise I would be paginating and would still load all rows in the range.

I think it makes more sense to have a straight iterator result set, and for the rare occasion you want seeking you can build on top of that.
(#10850)
User avatar
inghamn
Forum Contributor
Posts: 174
Joined: Mon Apr 16, 2007 10:33 am
Location: Bloomington, IN, USA

Re: Flexible finder methods

Post by inghamn »

Does it really hurt to implement these interfaces in addition to the Iterator? Since the PDOResultIterator is storing the results internally as an array, it was trivial to add support for ArrayAccess and Count.

The seeking and counting were added to the PDOResultIterator after using it for some time. In particular, the seek interface was added to support paginating the results. Truth be told, outside of paginating, the only item I ever seek is item [0]

I do, however, iterate over the same lists multiple times. Having the caching saves me two database calls per item in the result during subsequent iterations. This made a noticeable difference for large result sets referenced in multiple places on a webpage.

I freely admit, my pagination is still not something I consider elegant yet. It's functional, but I'm always trying to get my controllers to be less verbose. And I currently do pagingation all in the controller:

Code: Select all

 
$template = new Template();
$topics = new TopicList(array('committee'=>$committee,'year'=>$_GET['year']));
 
$template->blocks[] = new Block('topics/tagCloud.inc',array('topicList'=>$topics);
 
// Paginate the topics if there's alot of them
if (count($topics) > 15) {
    $pages = new Paginator($topics,15);
    $page = (isset($_GET['page']) && $_GET['page'])
            ? (int)$_GET['page']
            : 0;
    if (!$pages->offsetExists($page)) {
        $page = 0;
    }
    $topicList = new LimitIterator($topics,$pages[$page],$pages->getPageSize());
}
else {
    $topicList = $topics;
}
$template->blocks[] = new Block('topics/topicList.inc',array('topicList'=>$topicList));
 
if (isset($pages)) {
    $pageNavigation = new Block('pageNavigation.inc');
    $pageNavigation->page = $page;
    $pageNavigation->pages = $pages;
    $pageNavigation->url = new URL($_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI']);
 
    $template->blocks[] = $pageNavigation;
}
 
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Re: Flexible finder methods

Post by Christopher »

inghamn wrote:Does it really hurt to implement these interfaces in addition to the Iterator? Since the PDOResultIterator is storing the results internally as an array, it was trivial to add support for ArrayAccess and Count.
I don't know if it hurts, but since an array of row arrays does the same thing without any additional code, it just seems unnecessary most of the time. I do see that there are cases where the Lazy Loading that your caching provides would be useful. But I guess I see that as a wrapper to the base functionality.
(#10850)
User avatar
inghamn
Forum Contributor
Posts: 174
Joined: Mon Apr 16, 2007 10:33 am
Location: Bloomington, IN, USA

Re: Flexible finder methods

Post by inghamn »

I can see it that way, too. I guess the PDOResultIterator, as I have it, is what makes it possible for me to keep the populating of my models' data inside their constructors.

Personally I've never been a fan of the style of models that use static factory functions.

Code: Select all

 
$topic = Topic::loadByXXX($stuff);
 
Doing the loading in the contructor has seemed the most straightforward way. Plus, you get to say that a Topic object is a Topic. Not that a Topic object is a Factory for Topics.

Code: Select all

 
$topic = new Topic($stuff);
 

Once I went to the constructor style, I really wanted to keep all the models consistent and to keep the number of parameters in the constructor down. I didn't mind parsing for a scaler in the constructor, but passing in an array of data to use to hydrate didn't sit right.

Code: Select all

 
$topic = new Topic($id);
$topic = new Topic($name);
 
Anyway, because my constructors we kept to only taking single, scalar parameters, I needed a way to get collections to load and return me these model objects. Thus was born, the PDOResultIterator which just selects and returns ID's to the collection class to be loaded.

Now that I type this you've got me thinking about maybe altering my models' constructors to also take an array of data to use to construct an object. I guess if I did that I could save a database call per element in the result. (D'oh, I also just realized I've left hard coded the $this->list[] = $row[id]. I'd have to modify that.)

But I think I'd stil end up keeping the PDOResultIterator almost exactly as it is. It really doesn't care what the collection classes select. It just makes sure to return whatever the collections choose to select.

You're probably correct in that it's a bit overblown. There's simpler solutions for handling database results. But I'm not sure I'm too crazy about the necessary changes to my models to support the simpleness of an array of rows. To me the PDOResultIterator has seemed a bit of complexity that bought me a lot of simpleness and consistency elsewhere.

It's always a tradeoff, I guess. You gotta put the complexity *somewhere*, right? I think this is really bumping up against the limit of what you can really do with ActiveRecord. If there's something in an application that this starts to be a problem, it may be time to start looking at a Table Data Gateway, or even just Data Table access. But so far, with all the applications I've been up to lately, I don't think I've hit that complexity limit just yet.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Re: Flexible finder methods

Post by Christopher »

I agree with you about not liking static finders. I like to have a real instance from a real class. And I see the trade-off you made for the simpler constructor. Yeah that makes sense. I just got the sense that there was some cruft -- which is why I wondered if you used some of that functionality much.

I tend toward Table Data Gateway instead of ActiveRecord. I always got the sendse when using ActiveRecord that is was (because if its design) pushing me away from the right direction. But it can be pretty handy if backed by an O/RM.
(#10850)
josh
DevNet Master
Posts: 4872
Joined: Wed Feb 11, 2004 3:23 pm
Location: Palm beach, Florida

Re: Flexible finder methods

Post by josh »

Here is a method from a "message" mapper in a system I'm working on:

Code: Select all

/**
    * Count new ( unread ) messages for a user
    * 
    * @param User the user to count unread messages for
    * @return int count of unread ( new ) messages
    */
    public function countNew( User $user )
    {
        return $this->listWhere( $this->filter->unread() . ' && `recipient_id` = ' . (int)$user->getId() )->count();
    }
This extends datashuffler, the listWhere method is a method defined on the message mapper:

Code: Select all

 
protected function listWhere( $where )
    {
        $query = $this->listSelect()
            ->where( $where );
        return $this->query( $query );
    }
 
My finders use this to build up the base query, it calls the listSelect helper method to do so:

Code: Select all

protected function listSelect()
    {
        return $this->select()->order( 'sent_datetime DESC');
    }
select() is provided on the superclass ( Data Shuffler ), I use the Zend_Select component which allows me to chain methods to build up the query. I introduced a helper class for building up where conditions as well for this particular mapper ( which is the $this->filter call from above ):

Code: Select all

<?php
class Message_Mapper_Filter
{
    public function notDraftNotTrash()
    {
        return '`draft` = 0 && `trash` = 0';   
    }
    
    public function draftAndNotTrash()
    {
        return '`draft` = 1 && `trash` = 0';   
    }
    
    public function unarchived()
    {
        return $this->notDraftNotTrash() . ' && `archived` = 0';
    }
    
    public function archived()
    {
        return $this->notDraftNotTrash() . ' && `archived` = 1';
    }
    
    public function unread()
    {
        return $this->unarchived() . ' && `read` = 0';
    }
}
 
User avatar
inghamn
Forum Contributor
Posts: 174
Joined: Mon Apr 16, 2007 10:33 am
Location: Bloomington, IN, USA

Re: Flexible finder methods

Post by inghamn »

The PDOResultIterator came from a need to get the collections to work a certain way. It's design has accrued over time as I've needed something to make one of the collections easier to work with. More harvested than designed, really.

I use the count() all the time for these. And most of the screens using these involve pagination, which requires the ArrayAccess interface. But neither of these would be needed if this were just an plain array of results.

I use the the SQL related functions a fair amount of time as I'm building stuff.
I use the ->getSQL() function from PDOResultIterator alot; not just during debugging, but as a way to build subqueries.

For instance, the Topics in my system get tagged with Tags. I use the ->getSQL() so I can get relevant tags from a TopicList.
(All code's been shortened to just show relevant portions)

Code: Select all

 
$topics = new TopicList(array('tag_id'=>'Crazy'));
$relatedTags = new TagList(array('topicList'=>$topics));
 
foreach ($relatedTags as $tags)
{
    echo $tag;
}
 

Code: Select all

 
class TopicList extends PDOResultIterator
{
    public function find($fields=null,$sort='topics.date desc',$limit=null,$groupBy=null)
    {
        $options = array();
        $parameters = array();
 
            // Finding on fields from other tables requires joining those tables.
            // You can add fields from other tables to $options by adding the join SQL
            // to $this->joins here
            if (isset($fields['tag_id'])) {
                $this->joins.= ' left join topic_tags t on topics.id=t.topic_id';
                $options[] = 'tag_id=:tag_id';
                $parameters[':tag_id'] = $fields['tag_id'];
            }
 
        $this->populateList($options,$parameters);
    }
}
 

Code: Select all

 
class TagList extends PDOResultIterator
{
    public function find($fields=null,$sort='name',$limit=null,$groupBy=null)
    {
        $options = array();
        $parameters = array();
 
        // Finding on fields from other tables required joining those tables.
        // You can add fields from other tables to $options by adding the join SQL
        // to $this->joins here
        if (isset($fields['topicList'])) {
            $topics = $fields['topicList']->getSQL();
 
            $this->joins.= ' join topic_tags linked on tags.id=linked.tag_id';
            $this->joins.= " join ($topics)related_topics on linked.topic_id=related_topics.id";
            $parameters = array_merge($parameters,$fields['topicList']->getParameters());
        }
 
        $this->populateList($options,$parameters);
    }
}
 
This is not the most optimized way from a performance standard. But the consitency gained for developers is wonderful. All the SQL for each table goes in the correspondingly named file. All collection classes in an application work the same. On the off chance that something really does bog down, you can add an optimized static function to the class and do whatever SQL you need to, directly. I've done this exactly three times in the past 3 years for performance problems. (Oh, how I love XDebug) For me, it ends up being less than 1% of the time that theres something I can't do this way, or something that is too slow and needs to be written out as plain SQL.

I'm not ready to abandon ActiveRecord style yet. The easy-to-understand concept has so far seemed well worth needed collection classes, even with some of the shenanigens. (BTW most of this code is generated directly from the database, there's not really a whole lot of typing)
User avatar
Jenk
DevNet Master
Posts: 3587
Joined: Mon Sep 19, 2005 6:24 am
Location: London

Re: Flexible finder methods

Post by Jenk »

What about an argument for type?

Code: Select all

class Foo
{
  static Id = 1;
  static Name = 2;
  // etc
 
  public __constructor ($value, $type)
  {
    switch ($type)
    {
      case Foo::Id:
        $this->findById($value);
        break;
      case Foo::Name:
        $this->findByName($value);
        break;
      // etc
    }
  }
}

Code: Select all

$foo = new Foo("bar", Foo.Id);
or array:

Code: Select all

$foo = new Foo(array("type" => "value"));
But personally, I'd go for static methods with descriptive names..

Code: Select all

$foo = Foo::newFromId($id);
User avatar
inghamn
Forum Contributor
Posts: 174
Joined: Mon Apr 16, 2007 10:33 am
Location: Bloomington, IN, USA

Re: Flexible finder methods

Post by inghamn »

If you're doing ActiveRecord, each object should hold data for only one row from the database. I've used the constructor to populate the object from the database by passing in a unique identifier. The possibile available unique keys on any table are almost always very few. In those objects, there's not much reason to bother trying to tell the constructor what you're passing in, since you can deduce it by looking at the value passed in:

Code: Select all

 
class User
{
    public function __construct($id = null)
    {
        if ($id) {
            $PDO = Database::getConnection();
 
            # Load an existing user
            if (ctype_digit($id)) {
                $sql = 'select * from users where id=?';
            }
            else {
                $sql = 'select * from users where username=?';
            }
 
            $query = $PDO->prepare($sql);
            $query->execute(array($id));
 
            $result = $query->fetchAll(PDO::FETCH_ASSOC);
            if (!count($result)) {
                throw new Exception('users/unknownUser');
            }
            foreach($result[0] as $field=>$value) {
                if ($value) $this->$field = $value;
            }
        }
        else {
            # This is where the code goes to generate a new, empty instance.
            # Set any default values for properties that need it here
        }
    }
}
 
Post Reply