I know it's possible to define accessors and mutators for individual fields, like so:
public function setSomeAttribute($value) {
// set the attribute
}
public function getSomeAttribute() {
// return the attribute}
}
But is it possible to define a fallback method that will be used for all attribute gets and sets?
The reason being that I want to convert any empty values to null values on the fly, to keep my database clean and allow nullable fields to be null instead of an empty string (if there's a better way to do this let me know!)
I'm looking for something like
public function setAttribute($property,$value) {
$this->$property = empty($value) ? null : $value;
}
UPDATE:
Thanks to Chris Goosey I've found a solution that works for me. I extended the Eloquent model method setAttribute, and I set the value to the column default if it's empty. That's usually null, zero or an empty string in my databases so works for me!
public function setAttribute($key, $value)
{
// Convert empty values to their default values (e.g. null, zero)
if(empty($value) && $this->getSchema()->hasColumn($key)) {
$value = $this->getSchema()->getColumn($key)->getDefault();
}
parent::setAttribute($key,$value);
}
Best way is probably to extend the Eloquent class overwriting the setAttribute and getAttribute methods.
For all your models to inherit these overwritten methods you would want to create a class that extends eloquent e.g.
<?php
class BaseModel extends eloquent {
public function setAttribute($property,$value) {
$this->$property = empty($value) ? null : $value;
}
public function getAttribute($key) {
// Do Stuff
}
}
and then all your models should extend from this new class, e.g.
<?php
class User extends BaseModel {
protected $table = 'users';
}
It is also worth mentioning your new methods should have all the functionality of the old method plus your new functionality, this is what the getAttribute() looks like (Illuminate\Database\Eloquent line 2212):
/**
* Get an attribute from the model.
*
* #param string $key
* #return mixed
*/
public function getAttribute($key)
{
$inAttributes = array_key_exists($key, $this->attributes);
// If the key references an attribute, we can just go ahead and return the
// plain attribute value from the model. This allows every attribute to
// be dynamically accessed through the _get method without accessors.
if ($inAttributes || $this->hasGetMutator($key))
{
return $this->getAttributeValue($key);
}
// If the key already exists in the relationships array, it just means the
// relationship has already been loaded, so we'll just return it out of
// here because there is no need to query within the relations twice.
if (array_key_exists($key, $this->relations))
{
return $this->relations[$key];
}
// If the "attribute" exists as a method on the model, we will just assume
// it is a relationship and will load and return results from the query
// and hydrate the relationship's value on the "relationships" array.
$camelKey = camel_case($key);
if (method_exists($this, $camelKey))
{
return $this->getRelationshipFromMethod($key, $camelKey);
}
}
and the setAttribute in the same file looks like this (line 2338):
/**
* Set a given attribute on the model.
*
* #param string $key
* #param mixed $value
* #return void
*/
public function setAttribute($key, $value)
{
// First we will check for the presence of a mutator for the set operation
// which simply lets the developers tweak the attribute as it is set on
// the model, such as "json_encoding" an listing of data for storage.
if ($this->hasSetMutator($key))
{
$method = 'set'.studly_case($key).'Attribute';
return $this->{$method}($value);
}
// If an attribute is listed as a "date", we'll convert it from a DateTime
// instance into a form proper for storage on the database tables using
// the connection grammar's date format. We will auto set the values.
elseif (in_array($key, $this->getDates()))
{
if ($value)
{
$value = $this->fromDateTime($value);
}
}
$this->attributes[$key] = $value;
}
Hope this helps!
Related
I am looking for a clean solution for querying and modifying my Eloquent model's dynamic properties, stored in another table.
My main model is User. A User may have multiple UserVariables. When loading the model through a UserRepository, I may or may not eager-load the variables.
What I want to achieve is that the UserVariables can be modified in-memory, and be automatically saved when, and only when the User is saved.
Here is my solution (stripped out the non-relevant parts), which works, but is nor elegant, nor scalable:
/**
* #property boolean $isCommentingEnabled
*/
class User extends \Illuminate\Database\Eloquent\Model
{
public function variables()
{
return $this->hasMany('UserVariable', 'var_userid', 'user_id');
}
public function getIsCommentingEnabledAttribute()
{
foreach ($this->variables as $variable) {
if ($variable->name == UserVariable::IS_COMMENTING_ENABLED) {
return (boolean) $variable->value;
}
}
return false;
}
public function setIsCommentingEnabledAttribute($enabled)
{
foreach ($this->variables as $variable) {
if ($variable->name == UserVariable::IS_COMMENTING_ENABLED) {
$variable->value = $enabled ? 1 : 0;
return;
}
}
$this->variables()->add(new UserVariable([
'name' => UserVariable::IS_COMMENTING_ENABLED,
'value' => $enabled ? 1 : 0,
]));
}
}
/**
* #property-read int $id Unique ID of the record.
* #property string $name Must be one of the constants in this class.
* #property int $userId
* #property string $value
*/
class UserVariable extends EloquentModel {}
class UserRepository
{
public function findById($id)
{
return User::with('variables')->find($id);
}
public function saveUser(User $user)
{
return $user->push();
}
}
This solution clearly doesn't scale. If the user had 5+ variables, the code would be abundant, even if I extracted the loops.
I suspect there must be a short and clean solution in Laravel to just pop out a User's UserVariable by name, or get a new one if it does not exist, modify its value and put it back to the model. When a User::push() is called, it's auto-saved. Done.
I'm looking for something like
$user->variables()->where('name', UserVariable::IS_COMMENTING_ENABLED)->first()->value
= $enabled ? 1 : 0;
But the above does not work properly, because it uses the DB, not the model. Any help is appreciated.
Note: I'm working on a large legacy code base, so changing the DB structure is out of the question for now.
Unfortunately, there's no built-in way to do this. Fortunately, it's not too bad to just implement.
We have our model with its ->variables relation.
class User extends \Illuminate\Database\Eloquent\Model {
public function variables() {
return $this->hasMany('UserVariable', 'var_userid', 'user_id');
}
We then need to build a method which tells us whether or not we've already loaded said relation, and one to load them if we haven't.
protected function hasLoadedVariables() {
$relations = $this->getRelations();
return array_key_exists('variables', $relations);
}
protected function loadVariablesIfNotLoaded() {
if(!$this->hasLoadedVariables()) {
$this->load('variables');
}
}
These methods allow us to build generic getters and setters for UserVariables.
The getter makes sure that the variables relation is loaded, then returns the first value with a matching key.
protected function getVariable($key) {
$this->loadVariablesIfNotLoaded();
foreach($this->variables as $variable) {
if($variable->key === $key) {
return $variable->value;
}
}
return null;
}
The setter is a little more complicated because we want to update an existing UserVariable model if one exists and create one if it doesn't, and in either case save the change in the ->variables relation but not the DB. ($this->variables->push($variable) saves to the relation but not to the database).
protected function setVariable($key, $value) {
$this->loadVariablesIfNotLoaded();
foreach($this->variables as $k => $variable) {
if($variable->key === $key) {
$variable->value = $value;
return;
}
}
// We didn't find an existing UserVariable so we create one.
$variable = new UserVariable;
$variable->user_id = $this->id;
$variable->key = $key;
$variable->value = $value;
$this->variables->push($variable);
}
Laravel's ->push() method does not save records which do not exist to the database by default, but we can override it to do so.
public function push(array $options = []) {
if($this->hasLoadedVariables()) {
foreach($this->variables as $variable) {
if(!$variable->exists) {
$this->variables()->save($variable);
} else if($variable->isDirty()) {
$variable->save();
}
}
}
parent::push();
}
Finally, with all that in place, we can add specific getters and setters for single attributes.
protected function setIsCommentingEnabledAttribute($enabled) {
$this->setVariable(UserVariable::IS_COMMENTING_ENABLED, $enabled);
}
protected function getIsCommentingEnabledAttribute($enabled) {
$this->getVariable(UserVariable::IS_COMMENTING_ENABLED);
}
}
To use this, just write code the way you normally would.
$user = User::find(123);
$user->is_commenting_enabled; // Calls the getter, no DB calls
$user->is_commenting_enabled = 1; // Calls the setter, no DB calls
$user->push(); // Saves the user and any variables which have been added or changed.
I've been developing an application utilizing ZendFramework 1.1 for the better part of two years now, and as-so it has seen a few different stages of refactoring from me learning or trying something new. At its current state, I feel that my structure is pretty good in that I can get stuff done quickly, but could certainly use some improvements in certain areas- where I feel like there is a lot of bloat and awkward dependencies.
Bear with me here as I lay down some example code from my application. I will use an example of an Order object which has OrderItem instances which also must be saved. I will explain all necessary parts of instantiation and saving.
As far as my understanding goes, what I've got going on here is more-so in line with the ActiveRecord design pattern than with Domain Models, though I think I have practices from both...
class Order extends BaseObject {
/** #var OrderItem array of items on the order */
public $items = array();
public function __construct($data = array()){
// Define the attributes for this model
$schema = array(
"id" => "int", // primary key
"order_number" => "string", // user defined
"order_total" => "float", // computed
// etc...
);
// Get datamapper and validator classes
$mf = MapperFactory::getInstance();
$mapper = $mf->get("Order");
$validator = new Order_Validator();
$table = new Application_DbTable_Order();
// Construct parent
parent::__construct($schema, $mapper, $validator, $table);
// If data was provided then parse it
if(count($data)){
$this->parseData($data);
}
// return the instance
return $this;
}
// Runs before a new instance is saved, does some checks
public function addPrehook(){
$orderNumber = $this->getOrderNumber();
if($this->mapper->lookupByOrderNumber($orderNumber)){
// This order number already exists!
$this->addError("An order with the number $orderNumber already exists!");
return false;
}
// all good!
return true;
}
// Runs after the primary data is saved, saves any other associated objects e.g., items
public function addPosthook(){
// save order items
if($this->commitItems() === false){
return false;
}
// all good!
return true;
}
// saves items on the order
private function commitItems($editing = false){
if($editing === true){
// delete any items that have been removed from the order
$existingOrder = Order::getById($this->getId());
$this->deleteRemovedItems($existingOrder);
}
// Iterate over items
foreach($this->items as $idx => $orderItem){
// Ensure the item's order_id is set!
$orderItem->setOrderId($this->getId());
// save the order item
$saved = $orderItem->save();
if($saved === false){
// add errors from the order item to this instance
$this->addError($orderItem->getErrors());
// return false
return false;
}
// update the order item on this instance
$this->items[$idx] = $saved;
}
// done saving items!
return true;
}
/** #return Order|boolean The order matching provided ID or FALSE if not found */
public static function getById($id){
// Get the Order Datamapper
$mf = MapperFactory::getInstance();
$mapper = $mf->get("Order");
// Look for the primary key in the order table
if($mapper->lookup($id)){
return new self($mapper->fetchObjectData($id)->toArray());
}else{
// no order exists with this id
return false;
}
}
}
The parsing of data, saving, and pretty much anything else that applies to all models (a more appropriate term may be Entity) exists in the BaseObject, as so:
class BaseObject {
/** #var array Array of parsed data */
public $data;
public $schema; // valid properties names and types
public $mapper; // datamapper instance
public $validator; // validator instance
public $table; // table gateway instance
public function __construct($schema, $mapper, $validator, $table){
// raise an error if any of the properties of this method are missing
$this->schema = $schema;
$this->mapper = $mapper;
$this->validator = $validator;
$this->table = $table;
}
// parses and validates $data to the instance
public function parseData($data){
foreach($data as $key => $value){
// If this property isn't in schema then skip it
if(!array_key_exists($key, $this->schema)){
continue;
}
// Get the data type of this
switch($this->schema[$key]){
case "int": $setValue = (int)$value; break;
case "string": $setValue = (string)$value; break;
// etc...
default: throw new InvalidException("Invalid data type provided ...");
}
// Does our validator have a handler for this property?
if($this->validator->hasProperty($key) && !$this->validator->isValid($key, $setValue)){
$this->addError($this->validator->getErrors());
return false;
}
// Finally, set property on model
$this->data[$key] = $setValue;
}
}
/**
* Save the instance - Inserts or Updates based on presence of ID
* #return BaseObject|boolean The saved object or FALSE if save fails
*/
public function save(){
// Are we editing an existing instance, or adding a new one?
$action = ($this->getId()) ? "edit" : "add";
$prehook = $action . "Prehook";
$posthook = $action . "Posthook";
// Execute prehook if its there
if(is_callable(array($this, $prehook), true) && $this->$prehook() === FALSE){
// some failure occured and errors are already on the object
return false;
}
// do the actual save
try{
// mapper returns a saved instance with ID if creating
$saved = $this->mapper->save($this);
}catch(Exception $e){
// error occured saving
$this->addError($e->getMessage());
return false;
}
// run the posthook if necessary
if(is_callable(array($this, $posthook), true) && $this->$posthook() === FALSE){
// some failure occured and errors are already on the object
return false;
}
// Save complete!
return $saved;
}
}
The base DataMapper class has very simple implementations for save, insert and update, which are never overloaded because of the $schema being defined per-object. I feel like this is a bit wonky, but it works I guess? Child classes of BaseMapper essentially just provide domain-specific finder functions e.g., lookupOrderByNumber or findUsersWithLastName and other stuff like that.
class BaseMapper {
public function save(BaseObject $obj){
if($obj->getId()){
return $this->update($obj);
}else{
return $this->insert($obj);
}
}
private function insert(BaseObject $obj){
// Get the table where the object should be saved
$table = $obj->getTable();
// Get data to save
$saveData = $obj->getData();
// Do the insert
$table->insert($saveData);
// Set the object's ID
$obj->setId($table->getAdapter()->getLastInsertId());
// Return the object
return $obj;
}
}
I feel like what I have isn't necessarily horrible, but I also feel like there are some not-so-great designs in place here. My concerns are primarily:
Models have a very rigid structure which is tightly coupled to the database table schema, making adding/removing properties from the model or database table a total pain in the butt! I feel like giving all of my objects which save to the database a $table and $mapper in the constructor is a bad idea... How can I avoid this? What can I do to avoid defining $schema?
Validation seems a bit quirky as it is tied very tightly to the property names on the model which also correspond to column names in the database. This further complicates making any database or model changes! Is there a more appropriate place for validation?
DataMappers don't really do much besides provide some complicated finder functions. Saving complex objects is handled entirely by the object class itself (e.g., Order class in my example. Also is there an appropriate term for this type of object, other than 'complex object'? I say that my Order object is "complex" because it has OrderItem objects that it must also save. Should a DataMapper handle the saving logic that currently exists in the Order class?
Many thanks for your time and input!
It's a good practice to separate the concerns between objects as much as possible. Have one responsible for Input Validation, other to perform the business logic, DB operations, etc. In order to keep the 2 objects loosely coupled they should not know anything about each other’s implementation only what they can do. This is defined thru an interface.
I recommend reading this article http://www.javaworld.com/article/2072302/core-java/more-on-getters-and-setters.html and other ones from this guy. He's got a book as well worth reading http://www.amazon.com/Holub-Patterns-Learning-Looking-Professionals/dp/159059388X.
I would separate if possible order and items, I don’t know much about your app but if you need to show a list of 20 orders only with their order numbers then those DB calls and processing regarding order items would be a waste if not separated. This is of course not the only way.
So first you need to know what the order attributes are and encapsulate a way to feed those into an order and also have an order expose that data to other objects.
interface OrderImporter {
public function getId();
public function getOrderNumber();
public function getTotal();
}
interface OrderExporter {
public function setData($id, $number, $total);
}
In order to keep the business logic separate from the database we need to encapsulate that behavior as well like so
interface Mapper {
public function insert();
public function update();
public function delete();
}
Also I would define a specific mapper whose duty is to handle DB operations regarding orders.
interface OrderMapper extends Mapper {
/**
* Returns an object that captures data from an order
* #return OrderExporter
*/
public function getExporter();
/**
* #param string $id
* #return OrderImporter
*/
public function findById($id);
}
Finally an order needs to be able to communicate with all those objects through some messages.
interface Order {
public function __construct(OrderImporter $importer);
public function export(OrderExporter $exporter);
public function save(OrderMapper $orderRow);
}
So far we have a way to provide data to the Order, a way to extract data from the order and a way to interact with the db.
Below I've provided a pretty simple example implementation which is far from perfect.
class OrderController extends Zend_Controller_Action {
public function addAction() {
$requestData = $this->getRequest()->getParams();
$orderForm = new OrderForm();
if ($orderForm->isValid($requestData)) {
$orderForm->populate($requestData);
$order = new ConcreteOrder($orderForm);
$mapper = new ZendOrderMapper(new Zend_Db_Table(array('name' => 'order')));
$order->save($mapper);
}
}
public function readAction() {
//if we need to read an order by id
$mapper = new ZendOrderMapper(new Zend_Db_Table(array('name' => 'order')));
$order = new ConcreteOrder($mapper->findById($this->getRequest()->getParam('orderId')));
}
}
/**
* Order form can be used to perform validation and as a data provider
*/
class OrderForm extends Zend_Form implements OrderImporter {
public function init() {
//TODO setup order input validators
}
public function getId() {
return $this->getElement('orderID')->getValue();
}
public function getOrderNumber() {
return $this->getElement('orderNo')->getValue();
}
public function getTotal() {
return $this->getElement('orderTotal')->getValue();
}
}
/**
* This mapper also serves as an importer and an exporter
* but clients don't know that :)
*/
class ZendOrderMapper implements OrderMapper, OrderImporter, OrderExporter {
/**
* #var Zend_Db_Table_Abstract
*/
private $table;
private $data;
public function __construct(Zend_Db_Table_Abstract $table) {
$this->table = $table;
}
public function setData($id, $number, $total) {
$this->data['idColumn'] = $id;
$this->data['numberColumn'] = $number;
$this->data['total'] = $total;
}
public function delete() {
return $this->table->delete(array('id' => $this->data['id']));
}
public function insert() {
return $this->table->insert($this->data);
}
public function update() {
return $this->table->update($this->data, array('id' => $this->data['id']));
}
public function findById($id) {
$this->data = $this->table->fetchRow(array('id' => $id));
return $this;
}
public function getId() {
return $this->data['idColumn'];
}
public function getOrderNumber() {
return $this->data['numberColumn'];
}
public function getTotal() {
return $this->data['total'];
}
public function getExporter() {
return $this;
}
}
class ConcreteOrder implements Order {
private $id;
private $number;
private $total;
public function __construct(OrderImporter $importer) {
//initialize this object
$this->id = $importer->getId();
$this->number = $importer->getOrderNumber();
$this->total = $importer->getTotal();
}
public function export(\OrderExporter $exporter) {
$exporter->setData($this->id, $this->number, $this->total);
}
public function save(\OrderMapper $mapper) {
$this->export($mapper->getExporter());
if ($this->id === null) {
$this->id = $mapper->insert();
} else {
$mapper->update();
}
}
}
The mysql's SET data type is commonly used for representing options whereby they can be combined using bitwise operations.
eg.
SELECT set_column, set_column + 0 FROM table
The first would return a comma-separated list of options (eg. 'option1, option2')
The second would return 3 (1+2)
I'm using Doctrine2 and I'm only working with objects (not using partial objects or arrays). So my question is how can I retrieve the second format when querying the entity's repository?
If I map the column to be string it returns the first format. If mapped to integer php will convert to 0 (intval('options, ...')).
A dirty hack would be adding this to the mapped attribute:
#ORM\Column(name="set_column+0", type="integer")
... however it will cause problems when INSERTING or UPDATING.
Any ideas?
You need to create a custom type for Doctrine, as it doesn't provide built-in support for SET type. Luckly, it's quite simple:
Define the type class:
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
class MyCustomSetType extends Type
{
const TYPE = 'my_custom_set';
private $possibleValues = array('option1', 'opton2', ..., 'optionN');
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return sprintf("SET('%s')", implode(',', $this->possibleValues));
}
public function convertToPHPValue($values, AbstractPlatform $platform)
{
return explode(',', $values);
}
public function convertToDatabaseValue($values, AbstractPlatform $platform)
{
if (!is_array($values)) {
throw new \InvalidArgumentException('...');
}
foreach ($values as $value) {
if (!in_array($value, $this->possibleValues, true)) {
throw new \InvalidArgumentException('...');
}
}
return implode(',', $values);
}
public function getName()
{
return self::TYPE;
}
}
Register your newly created type:
\Doctrine\DBAL\Types\Type::addType('my_custom_set', '\My\Type\MyCustomSet');
Define a appropriate mapping for your entity:
/**
* #ORM\Column(type="my_custom_set")
*/
protected $options = array();
If you want to find more about custom types read the documentation: Custom Mapping Types
I have a model defined as follows:
class User extends ActiveRecord\Model {
function get_name() {
return $this->first_name . " " . $this->surname;
}
}
however when I show $item->attributes(); then name doesn't appear. Am I being an idiot here? If so, how do I get my custom attributes into the model?
Thanks,
Gareth
Here's my simple solution. I've overriding the attributes method and adding any methods that start with "get_attribute_" so I can use them during serialization:
class BaseModel extends ActiveRecord\Model
{
public function attributes()
{
$attrs = parent::attributes();
$modelReflector = new ReflectionClass(get_class($this));
$methods = $modelReflector->getMethods(~ReflectionMethod::IS_STATIC & ReflectionMethod::IS_PUBLIC);
foreach ($methods as $method)
{
if (preg_match("/^get_attribute_/", $method->getName()))
{
$attrs[str_replace('get_attribute_', '', $method->getName())] = $method->invoke($this);
}
}
return $attrs;
}
}
The resulting models that use this would look like this:
class User extends BaseModel
{
public $loginSessionId;
function get_attribute_loginSessionId(){
return $this->loginSessionId;
}
}
This way I can manually tack on the loginSessionId (or whatever else I want) and have it show up in the serialized values.
The attributes() method will indeed return only the values for your model's table columns (not aliased).
But $item->name should give you the expected result. You can also add the setter.
To get an array of all the attributes, you can add this method to your model:
public function all_attributes() {
$custom_attr = [];
foreach (static::$getters as $getter) {
$key = substr($getter, 4);
$custom_attr[$key] = $this->$key;
}
return $custom_attr + $this->attributes();
}
(don't forget to add your getters to the $getters array, the ActiveRecord model will use it)
You have to check what the attributes function does.
You might need to override it to take into account your custom attributes, or you might have to add your custom attributes to some _properties property in the ancestor
Can you show us how you created $item? PHPActiveRecord needs you to set the attributes in the constructor call (new User($attributes)) or directly on the properties ($user->first_name = 'Gareth').
EDIT
I'm not sure the attributes() method would pick up the custom getter. It seems that it just returns the attributes property of the model.
https://github.com/kla/php-activerecord/blob/master/lib/Model.php#L566
I solved this as follows:
Create a class that derives from ActiveRecord\Model and include this function:
public function properties()
{
$attrs = $this->attributes();
$modelReflector = new ReflectionClass(get_class($this));
$methods = $modelReflector->getMethods(~ReflectionMethod::IS_STATIC & ReflectionMethod::IS_PUBLIC);
foreach ($methods as $method)
{
if (preg_match("/^get_/", $method->getName()))
{
$attrs[str_replace('get_', '', $method->getName())] = $method->invoke($this);
}
}
return $attrs;
}
This returns all of the attributes, custom or otherwise, as long as I derive my models from the right class (not ActiveRecord\Model).
Actually, it is easily implemented like this # add this to https://github.com/kla/php-activerecord/blob/master/lib/Model.php#L520:
// check for attribute as getter
if ( method_exists( $this, "get_{$name}" ) ){
$method = "get_{$name}";
$var = $this->$method();
return $var;
}
But I prefer to do it like this (cleaner/optimized code) :
SomeModel.php:
/**
* gets templatefile of outputplugin, wrapper function for template comfortability
*
* NOTE 2: never implement functions in model, try redirecting
* to manager class (=lowmemory footprint, imagine xxxxx models with xxxx similar functions)
*
* #param string $varname variable description
* #return string
*/
public function get( $varname )
{
switch( $varname ){
case "foo" : return SomeModel::getManager()->getFoo(); break;
default: return "";
}
}
add this to https://github.com/kla/php-activerecord/blob/master/lib/Model.php#L520:
// check for attribute as getter
if ( method_exists( $this, "get" ) && $this->get( $name ) ){
$var = $this->get( $name );
return $var;
}
I'm using CodeIgniter to build a php web application, and I'm trying to use good OO practices - of which there appears to be many schools of thought. I specifically have a class biography_model to interact with a MySQL table. This data model has some class properties representing the columns in the table, but it also has some properties not in the table such as $image_url. The class constructor function accepts an optional record ID parameter which then fetches that record from the table and sets all object properties by calling the get_biography() method, including the $image_url property not in the table. This way I can instantiate a new biography_model object in the controller with all useful properties ready to go: $bio = new biography_model($id);
But, what is the best approach when we are returning a multi-row result set of records from the table? For each record I need to also set the $image_url. I could do this in the controller, by querying the list of records in the table and then passing each id into the new biography_model($id) object. But then I would have a situation where the controller is directly querying the database bypassing the model.
Instead, I choose to return an array of biography_model objects from within the biography_model.
Example:
class Biography_model extends Model
{
/**
* This model manages biography information in the 'biography_content' table.
* If a biography ID is passed in when instantiating a new object,
* then all class properties are set.
*/
protected $id;
protected $person_name;
protected $title;
protected $image_file_name;
protected $image_url;
protected $biography_text;
protected $active;
/**
* Constructor
*
* If an id is supplied when instantiating a new object, then
* all class variables are set for the record.
*/
public function __construct($person_id = NULL)
{
parent::Model();
if(isset($person_id))
{
$this->set_property('id',$person_id);
$this->get_biography();
}
}
/**
* Sets supplied property with supplied value.
*/
public function set_property($property, $value)
{
// Set image path if $value is the file name
if($property == 'image_file_name')
{
$this->set_property('image_url',$this->get_bio_img_url($value));
}
$this->$property = $value;
}
/**
* Gets requested property value.
*/
public function get_property($property)
{
return $this->$property;
}
/**
* Returns the biography thumbnail image URL
*/
public function get_bio_img_url($image_name)
{
return $this->config->item('parent_url').'assets/img/biography/'.$image_name;
}
/**
* Get one or more biography entries
*/
public function get_biography()
{
// If the ID is set then set model properties.
if($this->get_property('id'))
{
$this->db->where('id',$this->get_property('id'));
$query = $this->db->get('biography_content');
if($query->num_rows() == 1)
{
foreach($query->row() as $key => $value)
{
$this->set_property($key, $value);
}
}
}
// Otherwise return result set of all biographies
else
{
// Get the list of record ID's
$this->db->select('id');
$query = $this->db->get('biography_content');
if ($query->num_rows() > 0)
{
// New array to return result set
$biography_list = array();
// For each record, return a new biography_model object
foreach($query->result() as $value)
{
$biography_list[] = new biography_model($value->id);
}
}
return $biography_list;
}
}
}
// End of Biography_model Class
It works. But is it a reasonable approach? Are there other more accepted methods? I'm keenly aware that I am querying the database twice, but I was not sure of a better way to handle this. All suggestions are welcome!
Thanks, Wolf
Usually it's better for functions to have one job. Your get_biography() function has 2: get one biography and get all biographies. Consider splitting them up into 2 functions. Also there's no need for the multiple db access.
public function get_biography($id=null)
{
$this->db->where('id', $this->get_property($id))
$query = $this->db->get('biography_content');
foreach($query->row() as $key => $value)
{
$this->set_property($key, $value);
}
}
public function get_biographies()
{
$biography_list = array();
// don't limit this query to just id's - get everything
$query = $this->db->get('biography_content');
// For each record, return a new biography_model object
foreach($query->result() as $row)
{
$model = new biography_model();
// set the properties you already have straight onto the new model
// instead of querying again with just the id
foreach($row as $key => $value)
{
$model->set_property($key, $value);
}
$biography_list[] = $model;
}
return $biography_list;
}
Also you might want to take advantage of php's __get and __set magic methods:
public function __get($property)
{
if(!isset($this->$property))
return null;
return $this->$property;
}
public function __set($property, $value)
{
if(!property_exists($this, $property))
return;
if($property == 'image_file_name')
{
$this->image_url = $this->get_bio_img_url($value);
}
else
$this->$property = $value;
}
This will let you get properties on your model like this: $bio->title instead of $bio->get_property('title') while at the same time provide a place you can introduce new logic later.
Using an array to represent a set of records is a perfectly valid approach.
However the property image_url directly depends on the value of another property, so it doesn't make sense to store it as a separate field. Just calculate it on the fly, in your case you'd have to do that in the get_property method.
On the other hand should the model really be responsible for dealing with URLs? I don't think so. There should be a method outside the model that takes the Biography_model object and generates the URL of the image based on its image_file_name. If you already have some routing module responsible for mapping controllers to URLs, this code should probably land there.