Laravel Eloquent 5.1 update related model efficiently - php

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.

Related

How to access and manipulate data in Apigility before it is sent to the client?

I'm developing an Apigility driven application based on the Zend Framework 2.
Currently I'm sending the data retrieved in the database directly to the client: a request comes in, the MyResource#fetch(...) or MyResource#fetchAll(...) gets triggered and calls an appropriate method on MyService class, that calls MyMapper to retireve the data with its methods like findFooByBar(...).
Now I'd like to process the data, before the response is sent. How can I do that?
The Apigility ZF HAL documentation shows, how to access the entity data between it has been retrieved and sent to the client. Well I tried this out. It's ugly and to much code for such task. And... it doesn't work. I want however post here my attept:
namespace Portfolio;
...
class Module implements ApigilityProviderInterface {
private $serviceManager;
public function onBootstrap(MvcEvent $event) {
$application = $event->getTarget();
$this->serviceManager = $serviceManager = $application->getServiceManager();
$viewHelperManager = $serviceManager->get('ViewHelperManager');
$hal = $viewHelperManager->get('Hal');
$hal->getEventManager()->attach('renderEntity', array($this, 'onRenderEntity'));
$hal->getEventManager()->attach('renderCollection', array($this, 'onRenderCollection'));
}
public function onRenderEntity($event) {
$entity = $event->getParam('entity');
if ($entity->entity instanceof ProjectEntity) {
$projectEntity = $entity->entity;
$imageCollection = $this->tempCreateimagesForProject(
$event, $entity->entity->getId()
);
$projectEntity->setImages($imageCollection);
$event->setParam('entity', $projectEntity);
}
}
public function onRenderCollection($event) {
$collection = $event->getParam('collection');
$projectCollection = $collection->getCollection();
if ($projectCollection instanceof ProjectCollection) {
foreach ($projectCollection as $key => $projectItem) {
$tempProject = $projectCollection->getItem($key);
$tempProject->append(
['images' => $this->tempCreateimagesForProject($tempProject->offsetGet('id'))]
);
$projectCollection->getItem($key)->offsetSet($key, $tempProject);
}
}
}
private function tempCreateimagesForProject(Event $event, $projectId) {
$imageService = $this->serviceManager->get('Portfolio\V2\Rest\ImageService');
$imageCollection = $imageService->getImagesForProject($projectId);
return $imageCollection;
}
...
}
I think using the renderEntity and renderCollection events is not the correct spot to add this kind of resource specific logic. It is more suitable for more general changes or incidental customization.
You can add this logic to your resource listeners. So in your fetch and fetchAll methods in your MyResource class you can add the custom code you currently added in these onRenderEntity and onRenderCollection methods.
So something like this:
class MyResource extends AbstractResourceListener
{
/**
* Your image service dependency
*/
protected $imageService;
/* ... */
public function fetch($id)
{
$project = $this->projectMapper->fetch($id);
$imageCollection = $this->imageService->getImagesForProject($project);
$project->setImages($imageCollection);
return $project;
}
/* ... */
public function fetchAll($params = array())
{
$projects = $this->projectMapper->fetchAll();
foreach ($projects as $key => $project) {
$imageCollection = $this->imageService->getImagesForProject($project);
$project->setImages($imageCollection);
}
return $projects;
}
/* ... */
}
One possible solution is handling the data in the Hydrator. So we write a custom Hydrator class and enrich the items with nested objects and lists in it. It can look like this:
Portfolio\V2\Rest\Project\ProjectHydrator
...
class ProjectHydrator extends ClassMethods {
/**
* #var ImageService
*/
protected $imageService;
...
/*
* Doesn't need to be implemented:
* the ClassMethods#hydrate(...) handle the $data already as wished.
*/
/*
public function hydrate(array $data, $object) {
$object = parent::hydrate($data, $object);
if ($object->getId() !== null) {
$images = $this->imageService->getImagesForProject($object->getId());
$object->setImages($images);
}
return $object;
}
*/
/**
* #see \Zend\Stdlib\Hydrator\ClassMethods::extract()
*/
public function extract($object) {
$array = parent::extract($object);
if ($array['id'] !== null) {
$images = $this->imageService->getImagesForProject($array['id']);
$array['images'] = $images;
}
return $array;
}
}
It's not a nice solution, since then a part of the model / data retrieving logic gets moved to the hydrator. But it works. Here is shown an implementation of this approach and here is a discussion to this topic on GitHub.
If you are using the ClassMethods Hydrator and your Collection extends \Zend\Paginator\Paginator a good solution without losing the Collection's consistency and not changing anybody's code is to overwrite your getCurrentItems() method.
public class MyResourceCollection // most likely extends Paginator
{
public function getCurrentItems()
{
// getting the original items
$items = parent::getCurrentItems();
// as we work with objects $item will be an object
// when working with objects we use references to them not clones of objects
// so changes to $item are also present in the collection
foreach ($collection as $item) {
$oldSomething = $item->getSomething();
$item->setSomething('['.$oldSomething.']');
}
// $items are now changed, return them
return $items;
}
}
I have named the key something not to get confused with the getValue method from other places.
This makes the something value look like [something].

Cleaning up my Application Structure... Domain Objects and Data Mappers

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();
}
}
}

Universal accessors and mutators in Laravel 4

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!

Iterator on MySQL-using objects

I was thinking about such problem... Let's say we have a class Person:
class Person {
private $iPersonId;
private $sName;
private $sLastName;
private $rConn;
public function __construct($rConn, $iPersonId) {
$this->rConn = $rConn;
$this->iPersonId = $iPersonId;
}
public function load() {
// load name and last name using the $rConn object and $iPersonId
}
}
And now we want to perform some actions on many people so we write a new class:
class People {
private $aPeople = array();
public function addPerson(Person $oPerson) {
// ...
}
public function loadPeople() {
// PROBLEM HERE //
}
}
And now there are two problems:
1. Person and People have the same interface for loading (function load()) but if I wanted to iterate through $aPeople in People to load their data then this would result in maaaaany queries like:
SELECT * FROM people WHERE id = 1
SELECT * FROM people WHERE id = 2
SELECT ......
.....
....
And if wanted to load 1000 then something would go boom :) .
How do I design this code for loading all the users in one query? (IN)
I have to keep using Dependency Injection in every Person object I add into People. It's against the DRY rule and just doesn't look well.
So dear users, what is the better way to design this code?
I'd suggest a static method within People to load a bulk of people.
This would also require you to rewrite the constructor, or add another method to initialize the other data.
class Person {
protected $_data
protected $rConn;
public function __construct($rConn, $iPersonId) {
$this->rConn = $rConn;
$this->_data = array();
$this->_data['id'] = $iPersonId;
}
public function load() {
// load name and last name using the $rConn object and $iPersonId
}
// under the assumption, that $rConn is a mysqli connection
// if not rewrite the specific section
// also there is no injection protection or error handling in here
// this is just a workflow example, not good code!
public static function loadPeople($ids) {
$res = $rConn->query("select * from people where id in (" . implode(',', $ids) . ")");
$people = array();
while ($row = $res->fetch_assoc()) {
$p = new People($rConn, $row['id']);
$p->setData($row);
$people[] = $p;
}
$res->free();
return $people;
}
public function setData($data) {
foreach ($data as $key => $value {
$this->_data[key] = $value;
}
}
}
If you build a service as in Symfony2 (http://symfony.com/doc/2.0/book/service_container.html), you can just add methods. It doesn't sound right to have a "load()" on a "person". What does it load, Itself? It's also a bad practice to give your Object or Entity access to the database, this causes unwanted dependencies.
Your Entity or Object should never have a function to load itself, bad practice. Let something else manage the Entities or Objects.
Don't make dependencies that cause confusion, keep an object to its own purpose. A PersonEntity should never know anything about a Database Connection or EntityManager
Build your code so that you can move it into another project without things breaking Composer. http://getcomposer.org/
example as how I would do it in symfony2
class PeopleService
{
private $em;
/**
* #param EntityManager $em
*/
public function __construct(EntityManager $em)
{
$this->em = $em;
}
/**
* #param int $id
* #return Person
*/
public function loadPerson($id)
{
// do something and return 1 person
return $this->em->find('MyBundleNamspace:Person', $id);
}
/**
* #return array of Person objects
*/
public function loadPeople()
{
// do something and return an array with persons
}
}

PHP return self as array of objects from within the class?

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.

Categories