CakePHP - Using a different model in current model - class

I am creating a custom validation function in my model in CakePHP. After reading similar questions I have understood that I could be using ClassRegistry::init('Model'); to load a foreign model in my current model. But it doesn't say much more on the syntax and how to actually use it afterwards. This is what I have tried, but nothing "is happening" when I am trying to print the array to see if it contains the right stuff. Basically I want to pull out the User data to use it in my validation.
class Booking extends AppModel {
public $name = 'Booking';
public $validate = array(
'start_time' => array(
'noOptionViolation' => array(
'rule' => 'noOptionViolation',
'allowEmpty' => false
)
),
);
public function noOptionViolation ($start_time) {
$this->User = ClassRegistry::init('User');
$allUsers = $this->User->find('all');
print_r($allUsers);
}
Is this correct syntax? Can I use all the methods of $this->User just like I would in a controller?

You can use import as detailed on this post:
https://stackoverflow.com/a/13140816/1081396
App::import('Model', 'SystemSettings.SystemSetting');
$settings = new SystemSetting();
$mySettings = $settings->getSettings();
In your example it would be like:
App::import('Model', 'Users.User');
$user = new User();
$allUsers = $user->find('all');
print_r($allUsers);
You could better use the import at the beginning of the model.

You could use this too to load Models
$this->loadModel('User');
and access all functions by
$this->User

Related

Why my Zend-HAL implementation is not working with protected values

I am new in Zend framework, and trying to use HAL for API response generation. In the following is a simpler situation of my issues.
The class:
class Version
{
protected $data;
public function __construct($ar){
$data = $ar;
}
public function getArrayCopy(){
return $data;
}
}
$obj = new version(['major'=>1,'minor'=>2,'fix'=>3]);
When I test with hydrator, it works well as per the following:
use Zend\Hydrator\ArraySerializableHydrator;
$hydrator = new ArraySerializableHydrator();
$data = $hydrator->extract($obj);
print_r($data); // outputs ['major'=>1,'minor'=>2,'fix'=>3]
My HAL configuration is following:
MetadataMap::class => [
[
'__class__' => RouteBasedResourceMetadata::class,
'resource_class' => Version::class,
'route' => 'version',
'extractor' => ArraySerializableHydrator::class,
],
]
I use the following line in my Zend expressive (version 3) request handler
$resource = $this->resourceGenerator->fromObject($obj, $request);
$res = $this->responseFactory->createResponse($request, $resource);
The link is generated correctly, but the meta data (version info) is coming as empty. Any help will be much appreciated.
N.B.: My real code is complex, here I tried to generate a simpler version of the issue.
I think that when generating response the hydrate method is called. So your test does not seem to test what you meant to test.
When hydrating the hydrator works with ReflectionClass. So you need to add the indexes from $data as properties in the Version class.
e.g.
class Version
{
protected $major;
protected $minor;
protected $fix;
public function __construct($data){
foreach($data as $key => $value) {
$this->{$key} = $value;
}
}
public function getArrayCopy(){
return [
'major' => $this->major,
'minor' => $this->minor,
'fix' => $this->fix
];
}
}
$obj = new version(['major'=>1,'minor'=>2,'fix'=>3]);

unit testing legacy code

I'm new to unit testing and trying to unit test the model validation of an old zend application which is using forms.
Inside one of the forms it creates an instance of a second class and I'm struggling to understand how I can mock the dependent object. The form reads as follows :
class Default_Form_Timesheet extends G10_Form {
public function init() {
parent::init();
$this->addElement( 'hidden', 'idTimesheet', array( 'filters' => array ('StringTrim' ), 'required' => false, 'label' => false ) );
$this->addElement('checkbox', 'storyFilter', array('label' => 'Show my stories'));
$user = new Default_Model_User();
$this->addElement('select', 'idUser', array('filters' => array('StringTrim'), 'class' => 'idUser', 'required' => true, 'label' => 'User'));
$this->idUser->addMultiOption("","");
$this->idUser->addMultiOptions($user->fetchDeveloper());
...
......
My problem occurs when the call is made to $user->fetchDeveloper(). I suspect it has something todo with mocking objects and dependency injection but any guidence would be appreciated. My Failing unit test reads as follows...
require_once TEST_PATH . '/ControllerTestCase.php';
class TimesheetValidationTest extends ControllerTestCase {
public $Timesheet;
public $UserStub;
protected function setUp()
{
$this->Timesheet = new Default_Model_Timesheet();
parent::setUp();
}
/**
* #dataProvider timesheetProvider
*/
public function testTimesheetValid( $timesheet ) {
$UserStub = $this->getMock('Default_Model_User', array('fetchDeveloper'));
$UserStub->expects( $this->any() )
->method('fetchDeveloper')
->will( $this->returnValue(array(1 => 'Mickey Mouse')));
$Timesheet = new Default_Model_Timesheet();
$this->assertEquals(true, $Timesheet->isValid( $timesheet ) );
}
My data provider is in a separate file.
It is terminating at the command line with no output and I'm a bit stumped. Any help would be greatly appreciated.
You can't mock the Default_Model_User class in your test for the form. Because your code is instantiating the class internally you are not able to replace it with a mock.
You have a couple of options for testing this code.
You look into what fetchDeveloper is doing and control what it is returning. Either via a mock object that you can inject somewhere (looks unlikely) or by setting some data so that you know what the data will be. This will make your test a little brittle in that it could break when the data you are using changes.
The other option is to refactor the code so that you can pass the mock into your form. You can set a constructor that would allow you to set the Default_Model_User class and then you would be able to mock it with your test as written.
The constructor would like like this:
class Default_Form_Timesheet extends G10_Form {
protected $user;
public function __construct($options = null, Default_Model_User $user = null){
if(is_null($user)) {
$user = new Default_Model_User();
}
$this->user = $user;
parent::__construct($options);
}
Zend Framework allows options to be passed to forms constructor which I am not sure if you use in your code anywhere so this should not break any of your current functionality. When can then pass an optional Default_Model_User again so as to not break your current functionality. You need to set the values for $this->user before calling parent::__construct otherwise Zend will throw an error.
Now your init function will have to change from:
$user = new Default_Model_User();
to
$user = $this->user;
In your test you can now pass in your mock object and it will be used.
public function testTimesheetValid( $timesheet ) {
$UserStub = $this->getMock('Default_Model_User', array('fetchDeveloper'));
$UserStub->expects( $this->any() )
->method('fetchDeveloper')
->will( $this->returnValue(array(1 => 'Mickey Mouse')));
$Timesheet = new Default_Model_Timesheet(null, $UserStub);
$this->assertEquals(true, $Timesheet->isValid( $timesheet ) );
}
Creating a mock doesn't replace the object so that when new is called that your mock object is created. It creates a new object that extends your class that you can now pass around. new is a death to testability.

Create model instance in Form in ZF2 like ZF1?

In ZF1 it is possible to create an instance of a model and also access its properties from any form class.`
class Application_Form_Drydepot extends Zend_Form
{
$model = new Application_Model_DbTable_DrydepotModel();
$List = $model ->formationSelect();
array_unshift($List, array('key' => '', 'value' => '--Please Select--'));
$id = new Zend_Form_Element_Hidden('id');
$id->addFilter('Int')
->setDecorators($this->elementDecoration);
$formation = new Zend_Form_Element_Select('formation_id');
$formation->setLabel('Formation Name')
->setRequired(true)
->setAttrib('id', 'formation')
->setAttrib('class', 'required')
->addValidator('NotEmpty', true)
->setMultiOptions($List)
->setDecorators($this->elementDecoration);
}
In here $model directly possible to call but use it easily but zf2 it is quite difficult. I am not successfull about to do it. In ZF2 how do I do it same operation.
Another ways are like here : the documentation
Programmatic Form Creation
ZF2 Coding close to ZF1
use Zend\Captcha;
use Zend\Form\Element;
use Zend\Form\Form;
$captcha = new Element\Captcha('captcha');
$captcha
->setCaptcha(new Captcha\Dumb())
->setLabel('Please verify you are human');
$form = new Form('my-form');
$form->add($captcha);

Yii CJuiAutoComplete for Multiple values

I am a Yii Beginner and I am currently working on a Tagging system where I have 3 tables:
Issue (id,content,create_d,...etc)
Tag (id,tag)
Issue_tag_map (id,tag_id_fk,issue_id_fk)
In my /Views/Issue/_form I have added a MultiComplete Extension to retrieve multiple tag ids and labels,
I have used an afterSave function in order to directly store the Issue_id and the autocompleted Tag_ids in the Issue_tag_map table, where it is a HAS_MANY relation.
Unfortunately Nothing is being returned.
I wondered if there might be a way to store the autocompleted Tag_ids in a temporary attribute and then pass it to the model's afterSave() function.
I have been searching for a while, and this has been driving me crazy because I feel I have missed a very simple step!
Any Help or advices of any kind are deeply appreciated!
MultiComplete in Views/Issue/_form:
<?php
echo $form->labelEx($model, 'Tag');
$this->widget('application.extension.MultiComplete', array(
'model' => $model,
'attribute' => '', //Was thinking of creating a temporary here
'name' => 'tag_autocomplete',
'splitter' => ',',
'sourceUrl' => $this->createUrl('Issue/tagAutoComplete'),
// Controller/Action path for action we created in step 4.
// additional javascript options for the autocomplete plugin
'options' => array(
'minLength' => '2',
),
'htmlOptions' => array(
'style' => 'height:20px;',
),
));
echo $form->error($model, 'issue_comment_id_fk');
?>
AfterSave in /model/Issue:
protected function afterSave() {
parent::afterSave();
$issue_id = Yii::app()->db->getLastInsertID();
$tag; //here I would explode the attribute retrieved by the view form
// an SQL with two placeholders ":issue_id" and ":tag_id"
if (is_array($tag))
foreach ($tag as $tag_id) {
$sql = "INSERT INTO issue_tag_map (issue_id_fk, tag_id_fk)VALUES(:issue_id,:tag_id)";
$command = Yii::app()->db->createCommand($sql);
// replace the placeholder ":issue_id" with the actual issue value
$command->bindValue(":issue_id", $issue_id, PDO::PARAM_STR);
// replace the placeholder ":tag_id" with the actual tag_id value
$command->bindValue(":tag_id", $tag_id, PDO::PARAM_STR);
$command->execute();
}
}
And this is the Auto Complete sourceUrl in the Issue model for populating the tags:
public static function tagAutoComplete($name = '') {
$sql = 'SELECT id ,tag AS label FROM tag WHERE tag LIKE :tag';
$name = $name . '%';
return Yii::app()->db->createCommand($sql)->queryAll(true, array(':tag' => $name));
actionTagAutoComplete in /controllers/IssueController:
// This function will echo a JSON object
// of this format:
// [{id:id, name: 'name'}]
function actionTagAutocomplete() {
$term = trim($_GET['term']);
if ($term != '') {
$tags = issue::tagAutoComplete($term);
echo CJSON::encode($tags);
Yii::app()->end();
}
}
EDIT
Widget in form:
<div class="row" id="checks" >
<?php
echo $form->labelEx($model, 'company',array('title'=>'File Company Distrubution; Companies can be edited by Admins'));
?>
<?php
$this->widget('application.extension.MultiComplete', array(
'model' => $model,
'attribute' => 'company',
'splitter' => ',',
'name' => 'company_autocomplete',
'sourceUrl' => $this->createUrl('becomEn/CompanyAutocomplete'),
'options' => array(
'minLength' => '1',
),
'htmlOptions' => array(
'style' => 'height:20px;',
'size' => '45',
),
));
echo $form->error($model, 'company');
?>
</div>
Update function:
$model = $this->loadModel($id);
.....
if (isset($_POST['News'])) {
$model->attributes = $_POST['News'];
$model->companies = $this->getRecordsFromAutocompleteString($_POST['News']
['company']);
......
......
getRecordsFromAutocompleteString():
public static cordsFromAutocompleteString($string) {
$string = trim($string);
$stringArray = explode(", ", $string);
$stringArray[count($stringArray) - 1] = str_replace(",", "", $stringArray[count($stringArray) - 1]);
$criteria = new CDbCriteria();
$criteria->select = 'id';
$criteria->condition = 'company =:company';
$companies = array();
foreach ($stringArray as $company) {
$criteria->params = array(':company' => $company);
$companies[] = Company::model()->find($criteria);
}
return $companies;
}
UPDATE
since the "value" porperty is not implemented properly in this extension I referred to extending this function to the model:
public function afterFind() {
//tag is the attribute used in form
$this->tag = $this->getAllTagNames();
parent::afterFind();
}
You should have a relation between Issue and Tags defined in both Issue and Tag models ( should be a many_many relation).
So in IssueController when you send the data to create or update the model Issue, you'll get the related tags (in my case I get a string like 'bug, problem, ...').
Then you need to parse this string in your controller, get the corresponding models and assigned them to the related tags.
Here's a generic example:
//In the controller's method where you add/update the record
$issue->tags = getRecordsFromAutocompleteString($_POST['autocompleteAttribute'], 'Tag', 'tag');
Here the method I'm calling:
//parse your string ang fetch the related models
public static function getRecordsFromAutocompleteString($string, $model, $field)
{
$string = trim($string);
$stringArray = explode(", ", $string);
$stringArray[count($stringArray) - 1] = str_replace(",", "", $stringArray[count($stringArray) - 1]);
return CActiveRecord::model($model)->findAllByAttributes(array($field => $stringArray));
}
So now your $issue->tags is an array containing all the related Tags object.
In your afterSave method you'll be able to do:
protected function afterSave() {
parent::afterSave();
//$issue_id = Yii::app()->db->getLastInsertID(); Don't need it, yii is already doing it
foreach ($this->tags as $tag) {
$sql = "INSERT INTO issue_tag_map (issue_id_fk, tag_id_fk)VALUES(:issue_id,:tag_id)";
$command = Yii::app()->db->createCommand($sql);
$command->bindValue(":issue_id", $this->id, PDO::PARAM_INT);
$command->bindValue(":tag_id", $tag->id, PDO::PARAM_INT);
$command->execute();
}
}
Now the above code is a basic solution. I encourage you to use activerecord-relation-behavior's extension to save the related model.
Using this extension you won't have to define anything in the afterSave method, you'll simply have to do:
$issue->tags = getRecordsFromAutocompleteString($_POST['autocompleteAttribute'], 'Tag', 'tag');
$issue->save(); // all the related models are saved by the extension, no afterSave defined!
Then you can optimize the script by fetching the id along with the tag in your autocomplete and store the selected id's in a Json array. This way you won't have to perform the sql query getRecordsFromAutocompleteString to obtain the ids. With the extension mentioned above you'll be able to do:
$issue->tags = CJSON::Decode($_POST['idTags']);//will obtain array(1, 13, ...)
$issue->save(); // all the related models are saved by the extension, the extension is handling both models and array for the relation!
Edit:
If you want to fill the autocomplete field you could define the following function:
public static function appendModelstoString($models, $fieldName)
{
$list = array();
foreach($models as $model)
{
$list[] = $model->$fieldName;
}
return implode(', ', $list);
}
You give the name of the field (in your case tag) and the list of related models and it will generate the appropriate string. Then you pass the string to the view and put it as the default value of your autocomplete field.
Answer to your edit:
In your controller you say that the companies of this model are the one that you added from the Autocomplete form:
$model->companies = $this->getRecordsFromAutocompleteString($_POST['News']
['company']);
So if the related company is not in the form it won't be saved as a related model.
You have 2 solutions:
Each time you put the already existing related model in you autocomplete field in the form before displaying it so they will be saved again as a related model and it won't disapear from the related models
$this->widget('application.extensions.multicomplete.MultiComplete', array(
'name' => 'people',
'value' => (isset($people))?$people:'',
'sourceUrl' => array('searchAutocompletePeople'),
));
In your controller before calling the getRecordsFromAutocompleteString you add the already existing models of the model.
$model->companies = array_merge(
$model->companies,
$this->getRecordsFromAutocompleteString($_POST['News']['company'])
);

is it different 'default ' and 'Default' in zend default session namespace?

I am using sessions in zend framework.
the question is i need to know is there a difference between
new Zend_Session_Namespace("default");
and
new Zend_Session_Namespace("Default");
in my application, I have used both, it seems the code is not working correctly,
if there is a difference, what is the correct one to use.
here is my code
<?php
class Admin_DashboardController extends Zend_Controller_Action
{
function init()
{
//
}
/**
* Add hotelId to default session
* redirect to admin/hotels if hotelId is not avialble
*/
public function indexAction()
{
$params = $this->getRequest()->getParams();
$hotelid = NULL;
$config_session = new Zend_Session_Namespace("default");
$config_session->hotelid = $params['id'];
if(isset($params['id']) && !empty($params['id'])){
}else{
//redirect user to select hotels page
$redirector = new Zend_Controller_Action_Helper_Redirector();
$url = array(
'action' => 'admin/hotels/index'
);
$redirector->gotoRouteAndExit($url);
}
}
}
All Zend_Session_Namespace does internally is create a named array inside the $_SESSION superglobal. As array keys in PHP are case sensitive, "Default" and "default" will be treated as separate namespaces.
You can use whichever one you want, just be consistent if you expect to use the same data.