CakePHP changing virtual fields at runtime - php

I have a Product model for a multi site application.
Depending on the domain(site) I want to load different data.
For example instead of having a name and description fields in my database I have posh_name, cheap_name, posh_description, and cheap_description.
if I set something up like this:
class Product extends AppModel
{
var $virtualFields = array(
'name' => 'posh_name',
'description' => 'posh_description'
);
}
Then it always works, whether accessed directly from the model or via association.
But I need the virtual fields to be different depending on the domain. So first I creating my 2 sets:
var $poshVirtualFields = array(
'name' => 'posh_name',
'description' => 'posh_description'
);
var $cheapVirtualFields = array(
'name' => 'cheap_name',
'description' => 'cheap_description'
);
So these are my 2 sets, but how do I assign the correct one based on domain? I do have a global function called isCheap() that lets me know if I am on the lower end domain or not.
so I tried this:
var $virtualFields = isCheap() ? $this->cheapVirtualFields : $this->poshVirtualFields;
This gives me an error. Apparently you cannot assign variables in a Class definition like this.
So I put this in my Product model instead:
function beforeFind($queryData)
{
$this->virtualFields = isCheap() ? $this->cheapVirtualFields : $this->poshVirtualFields;
return $queryData;
}
This works ONLY when the data is accessed directly from the model, DOES NOT work when the data is accessed via model association.
There has got to be a way to get this to work right. How?

Well if I put it in the constructor instead of the beforeFind callback it seems to work:
class Product extends AppModel
{
var $poshVirtualFields = array(
'name' => 'posh_name',
'description' => 'posh_description'
);
var $cheapVirtualFields = array(
'name' => 'cheap_name',
'description' => 'cheap_description'
);
function __construct($id = false, $table = null, $ds = null) {
parent::__construct($id, $table, $ds);
$this->virtualFields = isCheap() ? $this->cheapVirtualFields : $this->poshVirtualFields;
}
}
However, I am not sure if this is a CakePHP no no that can come back to bite me?

seems like the issue could be that the model association is a model that is built on the fly. eg AppModel
try and do pr(get_class($this->Relation)); in the code and see what the output is, it should be your models name and not AppModel.
also try and use:
var $poshVirtualFields = array(
'name' => 'Model.posh_name',
'description' => 'Model.posh_description'
);
var $cheapVirtualFields = array(
'name' => 'Model.cheap_name',
'description' => 'Model.cheap_description'
);

Related

CakePHP call to undefined method stdClass::read() error

I'm new to CakePHP and I'm still learning the basics, through working in a live project and taking help from the CakePHP documentations when necessary. Currently, I'm having the following problem : I've recently changed my database table name and structure, so I was forced to change my view, controller and model names. After changing names, whenever I run the index.ctp page, I get the following error:
Fatal error: Call to undefined method stdClass::read() in C:\wamp\www\sdb\app\controllers
\home_loan_distributions_details_controller.php on line 32
Previously, my view folder was named home_loan_distributions, now it's renamed to home_loan_distributions_details.
My previous controller name was home_loan_distributions_controller.php and current name is home_loan_distributions_details_controller.php. The codes:
class HomeLoanDistributionsDetailsController extends AppController {
var $name = 'HomeLoanDistributionsDetails';
function index() {
$user = $this->Session->read('User');
$user_role = $user['User']['user_role_id'];
$actions = $this->Session->read('actions');
$display_actions = array();
foreach ($actions as $action) {
array_push($display_actions, $action['pm_controller_actions']['name']);
}
$this->set('display_actions', $display_actions);
$this->set('user_role', $user_role);
$branch_id = 18;
$this->set('branch_id', $branch_id);
$conditions = array('branch_id' => $branch_id);
$this->set('HomeLoanDistributionsDetails', $this->paginate($conditions));
$this->HomeLoanDistributionDetail->Branch->recursive = 0;
$this->set('BranchDetailInformation', $this->HomeLoanDistributionDetail->Branch->read(array('Branch.id', 'Branch.name', 'RegionalOffice.name', 'DistrictOffice.name', 'SubDistrictOffice.name', 'ClusterOffice.name'), $branch_id));
}
My model was previously named home_loan_distribution.php and now it's named home_loan_distribution_detail.php. The codes:
class HomeLoanDistributionDetail extends AppModel {
var $name = 'HomeLoanDistributionDetail';
var $actsAs = array('Logable' => array(
'userModel' => 'User',
'userKey' => 'user_id',
'change' => 'list', // options are 'list' or 'full'
'description_ids' => TRUE // options are TRUE or FALSE
));
var $validate = array(
'entry_date' => array(
'rule' => 'date',
'message' => 'Enter a valid date',
'allowEmpty' => true
),
'branch_id' => array('numeric'),
'customer_id' => array('numeric'),
'loan_amount' => array('numeric'),
'service_charge' => array('numeric'),
'security' => array('numeric'),
'loan_taken_term' => array('numeric'),
'purpose_id' => array('numeric'),
'installment_amount' => array('numeric'),
'installment_service_charge' => array('numeric'),
);
//The Associations below have been created with all possible keys, those that are not needed can be removed
var $belongsTo = array(
'Branch' => array(
'className' => 'Branch',
'foreignKey' => 'branch_id',
'conditions' => '',
'fields' => 'id,name',
'order' => ''
)
);
function paginate($conditions, $fields, $order, $limit, $page = 1, $recursive = null, $extra = array()) {
$recursive = 0;
$group = $fields = array('branch_id', 'entry_date');
$order = array('entry_date DESC');
$limit = 4;
$this->paginateCount($conditions);
return $this->find('all', compact('conditions', 'fields', 'order', 'limit', 'recursive', 'group'));
}
function paginateCount($conditions = null, $recursive = 0, $extra = array()) {
$recursive = 0;
$group = $fields = array('branch_id', 'entry_date');
$order = array('entry_date DESC');
$results = $this->find('all', compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive', 'group'));
return count($results);
}
}
What my guess is: probably I messed up the naming conventions while renaming everything. The problem is definitely within this line in the controller:
$this->set('BranchDetailInformation',
$this->HomeLoanDistributionDetail->Branch->read(array('Branch.id', 'Branch.name',
'RegionalOffice.name', 'DistrictOffice.name', 'SubDistrictOffice.name', 'ClusterOffice.name'),
$branch_id));
Whenever I comment out this line, I stop getting the above mentioned error message and my view page loads (although that still have some data missing - because I need those Branch related data to be displayed in my view.)
I can't figure out what exactly my problem is, but at least I know where it is. I need someone to pinpoint it.
My CakePHP version is 1.2.5, PHP version - 5.2
There is no function read for model.If you want to find some model data then try with -
$this->set('BranchDetailInformation', $this->HomeLoanDistributionDetail->Branch->find('all', $consitions);
$conditions will be the array of all requirements you want to provide. See the docs for more info.
Apparently, the problem seemed to be related to my 'className' => 'Branch' element of the Branch array used in my model, since the stdClass::read() method is related to classes and not models. But I discovered that the problem was elsewhere. This error was part of that problem, but it itself is not the actual problem.
I figured out this morning that my Model name is HomeLoanDistributionDetail, but my table name is home_loan_distributions_details (because someone else has changed the table name). CakePHP convention requires corresponding table name to be plural and model class name to be singular and CamelCased.
Quoting from the CakePHP Cookbook:
Model class names are singular and CamelCased. Person, BigPerson, and ReallyBigPerson are all examples of conventional model names.
Table names corresponding to CakePHP models are plural and underscored. The underlying tables for the above mentioned models would be people, big_people, and really_big_people, respectively.
Considering the above convention, I just had to rename my model class name from HomeLoanDistributionDetail to HomeLoanDistributionsDetail, in order to match with the table name home_loan_distributions_details. Also, I had to change the model file name from home_loan_distribution_detail.php to home_loan_distributions_detail.php.
After doing that, I stopped getting the error and I was successful in retrieving data from table and viewing it.

ZF2 Form and Doctrine 2 modify the value_options

I am using Doctrine 2 in my Zend Framework 2 Project. I have now created a Form and create one of my Dropdowns with Values from the Database. My Problem now is that I want to change which values are used and not the one which I get back from my repository. Okay, here some Code for a better understanding:
$this->add(
array(
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'name' => 'county',
'options' => array(
'object_manager' => $this->getObjectManager(),
'label' => 'County',
'target_class' => 'Advert\Entity\Geolocation',
'property' => 'county',
'is_method' => true,
'empty_option' => '--- select county ---',
'value_options'=> function($targetEntity) {
$values = array($targetEntity->getCounty() => $targetEntity->getCounty());
return $values;
},
'find_method' => array(
'name' => 'getCounties',
),
),
'allow_empty' => true,
'required' => false,
'attributes' => array(
'id' => 'county',
'multiple' => false,
)
)
);
I want to set the value for my Select to be the County Name and not the ID. I thought that I would need the 'value_options' which needs an array. I tried it like above, but get the
Error Message: Argument 1 passed to Zend\Form\Element\Select::setValueOptions() must be of the type array, object given
Is this possible at all?
I was going to suggest modifying your code, although after checking the ObjectSelect code i'm surprised that (as far as I can tell) this isn't actually possible without extending the class. This is because the value is always generated from the id.
I create all form elements using factories (without the ObjectSelect), especially complex ones that require varied lists.
Alternative solution
First create a new method in the Repository that returns the correct array. This will allow you to reuse that same method should you need it anywhere else (not just for forms!).
class FooRepository extends Repository
{
public function getCounties()
{
// normal method unchanged, returns a collection
// of counties
}
public function getCountiesAsArrayKeyedByCountyName()
{
$counties = array();
foreach($this->getCounties() as $county) {
$counties[$county->getName()] = $county->getName();
}
return $counties;
}
}
Next create a custom select factory that will set the value options for you.
namespace MyModule\Form\Element;
use Zend\Form\Element\Select;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\FactoryInterface;
class CountiesByNameSelectFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $formElementManager)
{
$element = new Select;
$element->setValueOptions($this->loadValueOptions($formElementManager));
// set other select options etc
$element->setName('foo')
->setOptions(array('foo' => 'bar'));
return $element;
}
protected function loadValueOptions(ServiceLocatorInterface $formElementManager)
{
$serviceManager = $formElementManager->getServiceLocator();
$repository = $serviceManager->get('DoctrineObjectManager')->getRepository('Foo/Entity/Bar');
return $repository->getCountiesAsArrayKeyedByCountyName();
}
}
Register the new element with the service manager by adding a new entry in Module.php or module.config.php.
// Module.php
public function getFormElementConfig()
{
return array(
'factories' => array(
'MyModule\Form\Element\CountiesByNameSelect'
=> 'MyModule\Form\Element\CountiesByNameSelectFactory',
),
);
}
Lastly change the form and remove your current select element and add the new one (use the name that you registered with the service manager as the type key)
$this->add(array(
'name' => 'counties',
'type' => 'MyModule\Form\Element\CountiesByNameSelect',
));
It might seem like a lot more code (because it is) however you will benefit from it being a much clearer separation of concerns and you can now reuse the element on multiple forms and only need to configure it in one place.

has_many through using custom table and field names

I'm using php.activerecord, and I am trying to link tables together. I'm not using their structure, but php.activerecord assumes I am, so it doesn't always work. I'm trying to use it on an already made app, so I can't change the database.
I learned from my previous question - Model association with custom table and key names - that I need to be as explicit as possible with the primary_key and foreign_key fields.
I'm having issues now using has_many through. I keep getting NULL, and I have no idea why.
So, here's a scenario: I have 3 tables, contacts, contactPrefs, and preferences. Those tables are as follows
contacts
--------
contactID
name
status
contactPrefs
------------
contactID
prefID
prefValue
preferences
-----------
prefID
name
description
Each contact has multiple contactPrefs. Each contactPrefs has one preferences. I tried to use has_many to get this working, but it's not. Here are my models:
Contacts.php:
<?php
class Contact extends ActiveRecord\Model {
static $primary_key = 'contactID';
static $has_many = array(
array(
'prefs',
'foreign_key' => 'contactid',
'primary_key' => 'contactid',
'class_name' => 'ContactPref'
),
array(
'preferences',
'foreign_key' => 'prefid',
'primary_key' => 'prefid',
'through' => 'prefs',
'class_name' => 'Preference'
)
);
}
ContactPref.php:
<?php
class ContactPref extends ActiveRecord\Model {
static $table_name = 'contactPrefs';
static $belongs_to = array(
array(
'contact',
'foreign_key' => 'contactid',
'primary_key' => 'contactid'
),
array(
'preference',
'foreign_key' => 'prefid',
'primary_key' => 'prefid'
)
);
}
Preference.php:
<?php
class Preference extends ActiveRecord\Model {
static $primary_key = 'prefID';
static $has_many = array(
array(
'prefs',
'foreign_key' => 'prefid',
'primary_key' => 'prefid',
'class_name' => 'ContactPref'
)
);
}
According to the docs, I now should be able to the following:
<?php
var_dump(Contact::find(1234)->preference);
I cannot. I get NULL. Oddly, I can do this:
<?php
var_dump(Contact::find(1234)->prefs[0]->preference);
That works correctly. But, shouldn't I be able to access the preference object directly through the contact object? Am I misunderstanding the docs (they aren't the greatest, in my opinion)? Am I doing something wrong?
First you are reading the docs with a small flaw. In the docs you are shown:
$order = Order::first();
# direct access to users
print_r($order->users); # will print an array of User object
Which you are already doing via Contact::find(1234)->prefs. Let me boil it down a bit
$contact = Contact::find(1234);
# direct access to prefs
print_r($contact->prefs); # will print an array of ContactPref object
Second, what you actually want is undefined. What should Contact::find(1234)->preference actually do? Return the preference of the first ContactPref? Return an array of Preference objects?
I feel like offering both:
<?php
class Contact extends ActiveRecord\Model {
static $primary_key = 'contactID';
static $has_many = array(
array(
'prefs',
'foreign_key' => 'contactid',
'primary_key' => 'contactid',
'class_name' => 'ContactPref'
),
array(
'preferences',
'foreign_key' => 'prefid',
'primary_key' => 'prefid',
'through' => 'prefs',
'class_name' => 'Preference'
)
);
public function get_preference() {
return isset($this->prefs[0])
? $this->prefs[0]->preference
: null
;
}
public function get_preferences() {
$preference=array();
foreach($this->prefs as $pref) {
$preference[]=$pref;
}
return $preference;
}
}
Let me explain a little bit what I have done. The ActiveRecord\Model class has a __get($name) function that looks for another function called get_$name, where $name in your case is preference (for the first result) and preference (for the entire collection). This means you can do Contact::find(1234)->preference which would be the same as doing Contact::find(1234)->prefs[0]->preference (but safer, due to the check) and Contact::find(1234)->preferences to get the entire collection of preferences.
This can be made better or optimized in numerous ways, so please don't take it as it is, but do try and adapt it to your specific situation.
For example you can either use the id of the preference as an index in the array or either not force a load of more data from ContactPrefs than the ones you are going to use and try a more intricate query to get the preference objects that you specifically need.
If I find a better implementation by getting through to work in the relationship definition, I'll return. But seeing the Unit Tests for active record, I'm skeptical.
There are several things that look strange, so it's not easy to come to a "this will fix it" for you, but this is an issue at least:
Fieldnames should always be lower-case in phpactiverecord. SQL doesn't mind it either way (not that table names ARE case-sensitive, but column names aren't). So make this:
static $primary_key = 'contactID';
into
static $primary_key = 'contactid';
The connections // find commands can be used in SQL, in which case it doesn't really matter how your key-string is 'cased', so some stuff works. But if the connection goes trough the inner-workings of phpmyadmin, it will fail. So check out this contactID but also the prefID.
Again, this goes only for COLUMN names, so don't go changing classnames or table-names to lowercase.
(extra point: phpmyadmin has trouble with combined primary keys. So while it might be ugly, you could add an extra row to your contactprefs table (if you don't allready have it) called id, to make that table actually have something to work with. It wouldn't give you much trouble, and it would help the activerecord library a lot)
Try the following:
<?php
var_dump(Contact::find(1234)->preferences);
The documentation says that with a has_many relationship, it should be referenced by a plural (http://www.phpactiverecord.org/projects/main/wiki/Associations#has_many_through). The Contact::find(1234) returns a Contact object which has multiple contactPrefs with their each Preference. In addition, in your Contact model, you specify the has_many as preferences .
static $has_many = array(
array(
'prefs',
'foreign_key' => 'contactid',
'primary_key' => 'contactid',
'class_name' => 'ContactPref'
),
array(
'preferences',
'foreign_key' => 'prefid',
'primary_key' => 'prefid',
'through' => 'prefs',
'class_name' => 'Preference'
)
);
Edit Through Modification:
Try the following Contact model
<?php
class Contact extends ActiveRecord\Model {
static $primary_key = 'contactID';
static $has_many = array(
array(
'prefs',
'foreign_key' => 'contactid',
'class_name' => 'ContactPref'
),
array('preferences',
'through' => 'prefs',
'class_name' => 'Preference',
'primary_key' => 'prefID')
);
}

Cakephp Custom Datasource Save/Update

Using the latest CakePHP build 1.3.6.
I'm writing a custom datasource for a external REST API. I've got all the read functionality working beautifully. I'm struggling with the Model::save & Model::create.
According to the documentation, the below methods must be implemented (see below and notice it does not mention calculate). These are all implemented. However, I was getting an "Fatal error: Call to undefined method ApiSource::calculate()". So I implemented the ApiSource::calculate() method.
describe($model) listSources() At
least one of:
create($model, $fields = array(), $values = array())
read($model, $queryData = array())
update($model, $fields = array(), $values = array())
delete($model, $id
= null)
public function calculate(&$model, $func, $params = array())
{
pr($model->data); // POST data
pr($func); // count
pr($params); // empty
return '__'.$func; // returning __count;
}
If make a call from my model
$this->save($this->data)
It is calling calculate, but none of the other implemented methods. I would expect it to either call ApiSource::create() or ApiSource::update()
Any thoughts or suggustions?
Leo, you tipped me in the right direction. The answer was in the model that was using the custom datasource. That model MUST define your _schema.
class User extends AppModel
{
public $name = 'User';
public $useDbConfig = 'cvs';
public $useTable = false;
public $_schema = array(
'firstName' => array(
'type' => 'string',
'length' => 30
),
'lastName' => array(
'type' => 'string',
'length' => 30
),
'email' => array(
'type' => 'string',
'length' => 50
),
'password' => array(
'type' => 'string',
'length' => 20
)
);
...
}
I'm guessing that if you implement a describe() method in the custom datasource that will solve the problem too. In this case it needed to be predefined to authorize the saves and/or creation.
From the API: http://api13.cakephp.org/class/dbo-source#method-DboSourcecalculate
"Returns an SQL calculation, i.e. COUNT() or MAX()"
A quick search in ~/cake finds 20 matches in 8 files. One of those is the definition in dbo_source.php
The other seven are:
dbo_source.test.php
code_coverage_manager.test.php
code_coverage_manager.php
dbo_db2.php
model.php
tree.php
containable.php
Without delving too deeply into this, I suspect your problem lies in Model::save
You'll probably have to define a calculate method to suit the structure of your custom datasource because Cake won't know how to do that.

Mysql : Join/Relating two tables

I have two table
1. Airline -id(primary), name
2. Form - id(primary), operator, other unwanted fields
I want to relate Airline.name to Form.operator. Is it possible since Form.operator is not primary key, if yes give me the query.
Can some one also guide me as how will the cakephp model relation be in this case
I would advise you to not use the name Form as is it used elsewhere in the system, however try this (or something similar) and read http://book.cakephp.org/view/1039/Associations-Linking-Models-Together
In app/models/airline.php:
<?php
class Airline extends AppModel
{
var $name = 'Airline';
var $hasOne = array(
'Form' => array(
'className' => 'Form',
'foreignKey' => 'operator')
);
// other stuff
// ... //
?>
In app/models/form.php:
<?php
class Form extends AppModel
{
var $name = 'Form';
var $belongsTo = array(
'Airline' => array(
'className' => 'Airline',
'foreignKey' => 'operator')
)
;
// other stuff
// ... //
?>
var $hasOne = array(
'airline' => array(
'className' => 'airline',
'foreignKey' => false,
'conditions' => array(
'`form`.`yourfield` = `airline`.`yourfield`'
)
)
}
This should work. just replace your fields
in order to make the relations, as Leo suggested, work, you have to follow the cake conventions. In order to save you some headaches later on, I would therefore suggest the nicely written and short material here and here. You will learn e.g. that a good foreign key for which cakephp can do some lifting for you is named operator_id, instead of simply operator (if operator is not yet a foreign key, it could be that you have a database design issue).
Lifting here refers to automatically recognizing relations once defined in e.g. a $belongsTo.
select * from `airline`, `form` where `airline.id`=`form.operator`

Categories