Let's say I have Vehicle model (it's Eloquent model) that stores different types of vehicles (in vehicles table). Of course, there are many different types of vehicles, so I have for example:
class Car extends Vehicle {
}
class Bicycle extends Vehicle {
}
and so on.
now I need to find object based on vehicle and here's the problem. I've added the following method in Vehicle model:
public function getClass()
{
return __NAMESPACE__ . '\\' . ucfirst($this->type)
}
so I can find the class name I should use.
But the only way to get valid class is like this:
$vehicle = Vehicle::findOrFail($vehicleId);
$vehicle = ($vehicle->getClass())::find($vehicleId);
which is not the best solution because I need to run 2 exact same queries to get valid final class object.
Is there any way to achieve same without duplicating the query?
An alternative to #jedrzej.kurylo's method would be to just override one method in your Vehicle class:
public static function hydrate(array $items, $connection = null)
{
$models = parent::hydrate($items, $connection);
return $models->map(function ($model) {
$class = $model->getClass();
$new = (new $class())->setRawAttributes($model->getOriginal(), true);
$new->exists = true;
return $new;
});
}
Hope this helps!
In order for Eloquent to correctly return objects of a class determined by the type column, you'll need to override 2 methods in your Vehicle model class:
public function newInstance($attributes = array(), $exists = false)
{
if (!isset($attributes['type'])) {
return parent::newInstance($attributes, $exists);
}
$class = __NAMESPACE__ . '\\' . ucfirst($attributes['type']);
$model = new $class((array)$attributes);
$model->exists = $exists;
return $model;
}
public function newFromBuilder($attributes = array(), $connection = null)
{
if (!isset($attributes->type)) {
return parent::newFromBuilder($attributes, $connection);
}
$instance = $this->newInstance(array_only((array)$attributes, ['type']), true);
$instance->setRawAttributes((array)$attributes, true);
return $instance;
}
For anybody else that comes across this page, this is what worked for me. I copied the newInstance and newFromBuilder from the source code, and put them in my parent class, in this case it would be Vehicle.
I think the newInstance method is ran twice when building up a query builder instance. In the newInstance method I would check if the type is set in the attributes, and if so then get the namespace based off the type (I used PHP Enums). On the second pass $attributes gets converted to an object rather than array, not sure why but don't worry about your IDE complaining.
In the newFromBuilder method I had to pass $attributes in to the newInstance method, as before it was just passing an empty array.
$model = $this->newInstance([], true);
to:
$model = $this->newInstance($attributes, true);
Vehicle.php
/**
* Create a new instance of the given model.
*
* #param array $attributes
* #param bool $exists
* #return static
*/
public function newInstance($attributes = [], $exists = false)
{
// This method just provides a convenient way for us to generate fresh model
// instances of this current model. It is particularly useful during the
// hydration of new objects via the Eloquent query builder instances.
$model = new static;
if (isset($attributes->type)) {
$class = // Logic for getting namespace
$model = new $class;
}
$model->exists = $exists;
$model->setConnection(
$this->getConnectionName()
);
$model->setTable($this->getTable());
$model->mergeCasts($this->casts);
$model->fill((array) $attributes);
return $model;
}
/**
* Create a new model instance that is existing.
*
* #param array $attributes
* #param string|null $connection
* #return static
*/
public function newFromBuilder($attributes = [], $connection = null)
{
// I had to pass $attributes in to newInstance
$model = $this->newInstance($attributes, true);
$model->setRawAttributes((array) $attributes, true);
$model->setConnection($connection ?: $this->getConnectionName());
$model->fireModelEvent('retrieved', false);
return $model;
}
By making these changes I could do Vehicle::all() and get a collection containing both Car and Bicycle classes.
Related
I have this set of entities that we call nomenclators, which basically have an id field and a text-based field. The CRUD operations for these entities are virtually the same, just that in some of them the text field is called state while in others is area... and so on.
Given that, I created this base Controller
class NomenclatorsController extends Controller
{
use ValidatorTrait;
protected function deleteENTITYAction(Request $req, $entityName)
{
$id = $req->request->get('id');
$spService = $this->get('spam_helper');
$resp = $spService->deleteEntitySpam("AplicacionBaseBundle:$entityName", $id);
if ($resp == false)
return new JsonResponse("error.$entityName.stillreferenced", Response::HTTP_FORBIDDEN);
return new JsonResponse('', Response::HTTP_ACCEPTED);
}
protected function listENTITYAction(Request $req, $entityName)
{
$size = $req->query->get('limit');
$page = $req->query->get('page');
$spService = $this->get('spam_helper');
$objectResp = $spService->allSpam("AplicacionBaseBundle:$entityName", $size, $page);
$arrayResp = $spService->spamsToArray($objectResp);
return new JsonResponse($arrayResp, Response::HTTP_ACCEPTED);
}
protected function updateENTITYAction(Request $req, $entityName)
{
$id = $req->request->get('id');
$entity = null;
if (is_numeric($id)) {
$entity = $this->getDoctrine()->getRepository("AplicacionBaseBundle:$entityName")->find($id);
} else if (!is_numeric($id) || $id == null) {
//here comes the evil
eval('$entity=new \\AplicacionBaseBundle\\Entity\\' . $entityName . '();');
$entity->setEliminado(false);
$entity->setEmpresa($this->getUser()->getEmpresa());
}
$this->populateEntity($req->request, $entity);
$errors = $this->validate($entity);
if ($errors)
return new Response(json_encode($errors), Response::HTTP_BAD_REQUEST);
$spamService = $this->get('spam_helper');
$spamService->saveEntitySpam($entity);
}
//Override in children
protected function populateEntity($req, $entity)
{
}
}
So, each time I need to write a controller for one of these nomenclators I extend this NomenclatorsController and works like a charm.
The thing is in the updateENTITYAction I use eval for dynamic instantiation as you can see, but given all I have readed about how bad is eval I am confused now, and even when there is no user interaction in my case I want to know if there is a better way of doing this than eval and if there is any noticiable performance issue when using eval like this.
By the way I am working in a web json api with symfony and extend.js, which means no view is generated in the server,my controllers match a route and receive a sort of request params and do the work.
I've done something similar in the past. Since you are extending a base class using specific classes for each entity you can instance your entity from the controller that extends NomenclatorsController.
If one of your entities is called Foo you will have a FooController that extends NomenclatorsController. Just overwrite updateENTITYAction and pass back needed variables.
An example:
<?php
use AplicacionBaseBundle\Entity\Foo as Item;
class FooController extends NomenclatorsController
{
/**
* Displays a form to edit an existing item entity.
*
* #Route("/{id}/edit")
* #Method({"GET", "POST"})
* #Template()
* #param Request $request
* #param Item $item
* #return array|bool|\Symfony\Component\HttpFoundation\RedirectResponse
*/
public function updateENTITYAction(Request $request, Item $item)
{
return parent::updateENTITYAction($request, $item);
}
}
This way you are sending directly the entity to NomenclatorController and you don't even need to know the entityName.
Humm I'll me too advise you to avoid the eval function. It's slow and a bad practice.
What you want here is the factory pattern,
You could define a service to create the entites for you
#app/config/services.yml
app.factory.nomenclators:
class: YourNamespace\To\NomenclatorsFactory
And your factory might be like this
namespace YourNamespace\To;
use YourNamespace\To\Entity as Entites;
class NomenclatorsFactory {
// Populate this array with all your Nomenclators class names with constants OR with reflection if you have many
private $allowedNomemclators = [];
/**
* #param $entityName
* #return NomenclatorsInterface|false
*/
public function getEntity($entityName)
{
if(!is_string($entityName) || !in_array($entityName, $this->allowedNomemclators)) {
// Throw exception or exit false
return false;
}
return new $entityName;
}
}
Then you have to create the NomenclatorsInterface and define in it all the common methods between all your entities. Moreover define one more method getSomeGoodName, the job of this method is to return the good property (area or state)
With this structure your controller can only instances the Nomenclators entities and don't use anymore the eval evil method haha
Moreover you don't have to worry about about the state and area property
Ask if something isn't clear :D
I hope it help !
Looking for "AutoMapper"-like feature/framework/pattern for taking data from a Doctrine 2 domain entity / DTO and mapping the protected properties on that entity to matching public properties on a View Model.
$userEntity = $this-em->find(User::class, 1);
$userViewModel = AutoMapper->Map($userEntity, new UserViewModel());
Where the only significant difference between User and UserViewModel is that User contains get/set accessors with protected backing fields (per doctrine's instructions), whereas UserViewModel contains public properties that match in name [a subset of] the protected backing fields on User.
An thoughts on how to accomplish this? (preferably without reflection)
Note that the domain entity has public get accessor, so the solution can leverage those accessors.
Came up with my own crude, yet effective, bare-bones implementation of AutoMapper for PHP to solve this problem for me. This method will map from public properties or public getters (convention based naming) to public properties on the target entity.
Hope this helps someone out:
class Mapper
{
/**
* This method will attempt to source all public property values on $target from $source.
*
* By convention, it'll look for properties on source with the same name,
* .. and will fallback camel-cased get/set accessors to use.
*
* Note that underscores in properties will be translated to capital letters in camel-cased getters.
*
* #param $source object
* #param $target object
* #return object
* #throws Exception
*/
public static function Map($source, $target)
{
$targetProperties = get_object_vars($target);
$sourceProperties = get_object_vars($source);
foreach ($targetProperties as $name => $value)
{
//
// match properties
//
$matchingSourcePropertyExists = array_key_exists($name, $sourceProperties);
if ($matchingSourcePropertyExists)
{
$target->{$name} = $source->{$name};
continue;
}
//
// fall back on matching by convention-based get accessors
//
$sourceMethods = get_class_methods(get_class($source));
$getterName = "get" . self::convertToPascalCase($name);
$matchingGetAccessorExists = in_array($getterName, $sourceMethods);
if ($matchingGetAccessorExists)
{
$target->{$name} = $source->{$getterName}();
continue;
}
//
// if we ever fail to map an entity on the target, throw
//
$className = get_class($target);
throw new Exception("Could not auto-map property $name on $className.");
}
return $target;
}
/**
* Converts this_kind_of_string into ThisKindOfString.
* #param $value string
* #return string
*/
private static function convertToPascalCase($value)
{
$value[0] = strtoupper($value[0]);
$func = create_function('$c', 'return strtoupper($c[1]);');
return preg_replace_callback('/_([a-z])/', $func, $value);
}
}
My implementation of automapping using Symfony's components
<?php
declare(strict_types=1);
namespace App\ApiResource\Utils;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
class GenericMapper
{
public static function map(object $source, object $target): void
{
$reflectionExtractor = new ReflectionExtractor();
$sourceProperties = $reflectionExtractor->getProperties($source::class);
$propertyAccessor = PropertyAccess::createPropertyAccessor();
foreach ($sourceProperties as $propertyName) {
if ($propertyAccessor->isWritable($target, $propertyName) && $propertyAccessor->isReadable($source, $propertyName)) {
$propertyAccessor->setValue($target, $propertyName, $propertyAccessor->getValue($source, $propertyName));
}
}
}
}
I am new to PHPUnit and am trying to set up my first test. It is failing with the following error message
Failed asserting that ReflectionProperty Object (...) is an instance of class "Illuminate\Database\Eloquent\Model".
Essentially, I am trying to mock the creation of a model which I am injecting into a constructor and testing it is the right type through reflection. I am following the advice laid out in this post/answer:
How do I test this class using phpunit?
If anyone can give me some guidance, that would greatly appreciated.
The class being tested is here:
<?php
namespace PlaneSaleing\Repo\Listing;
use Illuminate\Database\Eloquent\Model;
class EloquentListing implements ListingInterface {
protected $advert;
public function __construct(Model $advert)
{
$this->advert = $advert;
}
/**
* Get paginated listings
*
* #param int Current page
* #param int Number of listings per page
* #return StdClass object with $items and $totalItems for pagination
*/
public function byPage($page=1, $limit=10)
{
$result = new \StdClass;
$result->page = $page;
$result->limit = $limit;
$result->totalItems = 0;
$result->items = array();
$listings = $this->advert
->orderBy('created_at')
->skip( $limit * ($page-1) )
->take($limit)
->get();
// Create object to return data useful for pagination
$result->items = $listings->all();
$result->totalItems = $this->totalListings();
return $result;
}
The service provider is here:
<?php
namespace PlaneSaleing\Repo;
use Illuminate\Support\ServiceProvider;
use PlaneSaleing\Repo\Listing\EloquentListing as Listing;
use \Advert;
class RepoServiceProvider extends ServiceProvider {
public function register()
{
$this->app->bind('PlaneSaleing\Repo\Listing\ListingInterface', function($app) {
$app->make(Listing(new Advert));
} );
}
}
My Test is here:
<?php
use \Mockery;
use \ReflectionClass;
use PlaneSaleing\Repo\Listing\EloquentListing;
class EloquentListingTest extends \TestCase
{
/**
* Testing if __constructor is setting up property
*/
public function testModelSetsUp()
{
$mock1 = Mockery::mock(Illuminate\Database\Eloquent\Model::class);
$listing = new EloquentListing($mock1);
$reflection = new ReflectionClass($listing);
// Making your attribute accessible
$property1 = $reflection->getProperty('advert');
$property1->setAccessible(true);
$this->assertInstanceOf(Illuminate\Database\Eloquent\Model::class, $property1);
}
When you assert, you should use the actual value of the reflection property, not the property itself:
public function testModelSetsUp()
{
$mock1 = Mockery::mock(Illuminate\Database\Eloquent\Model::class);
$listing = new EloquentListing($mock1);
$reflection = new ReflectionClass($listing);
// Making your attribute accessible
$property1 = $reflection->getProperty('advert');
$property1->setAccessible(true);
$this->assertInstanceOf(
Illuminate\Database\Eloquent\Model::class,
$property1->getValue($listing)
);
}
However, you can simplify the test a lot:
public function testModelSetsUp()
{
$mock1 = Mockery::mock(Illuminate\Database\Eloquent\Model::class);
$listing = new EloquentListing($mock1);
$this->assertAttributeInstanceOf(
Illuminate\Database\Eloquent\Model::class,
'advert',
$listing
);
}
Even better, you could use meaningful variable names, use a meaningful test method name, and not just assert that the $advert property is an instance of Illuminate\Database\Eloquent\Model, but actually the same instance as passed into the constructor:
public function testConstructorSetsAdvert()
{
$advert = Mockery::mock(Illuminate\Database\Eloquent\Model::class);
$listing = new EloquentListing($advert);
$this->assertAttributeSame(
$advert,
'advert',
$listing
);
}
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();
}
}
}
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;
}