custom form field 'province/state' for dynamic dropdown in Joomla - joomla3.0

I am trying to create a custom form field naming as city. On select of country field, city field should populate options of all the provinces listed under the selected country.
Just exactly as the Joomla documentation.
I am following https://docs.joomla.org/Creating_a_custom_form_field_type documentation.
<?php
// Check to ensure this file is included in Joomla!
defined('_JEXEC') or die('Restricted access');
JFormHelper::loadFieldClass('list');
class JFormFieldCity extends JFormFieldList {
protected $type = 'City';
public function getOptions() {
$app = JFactory::getApplication();
//country is the dynamic value which is being used in the view
$country = $app->input->get('country');
$db = JFactory::getDbo();
$query = $db->getQuery(true);
$query->select('a.cityname')
->from('`#__tablename` AS a')
->where('a.country = "'.$country.'" ');
$rows = $db->setQuery($query)->loadObjectlist();
foreach($rows as $row){
$cities[] = $row->cityname;
}
// Merge any additional options in the XML definition.
$options = array_merge(parent::getOptions(), $cities);
return $options;
}
}
However, I can see $country = $app->input->get('country'); is not working.
At view, I have a country field.

Related

Adding addCondition() in a Form with fields from other tables

I don't know the syntax to access "CurrentTable.ForeignKey" nor "OtherTable.PrimaryKey" in a $model->addCondition() statement.
This is a fragment of my code which works:
$mm = new SYSPC_MODEL($this->app->db,['title_field'=>'MODEL_NAME']);
$mm->addCondition('MODEL_NAME', 'LIKE', 'DESK%');
In place of simply searching for MODEL_NAME like 'DESK%', I would like to display only the FK_MODEL_id values which exist in the SYSPC_MODEL table for the same FK_OS_ID than the current record FK_OS_ID value. So in SQL, we should have something like:
SELECT SYSPC_MODEL.MODEL_NAME WHERE ( DHCP_PC.FK_OS_ID = SYSPC_MODEL.id )
To understand easier the context, I reduced my code as much as possible:
<?php
include_once ('../include/config.php');
require '../vendor/autoload.php';
class SYSPC_OS extends \atk4\data\Model {
public $table = 'SYSPC_OS';
function init() {
parent::init();
$this->addFields([ ['OS_NAME', 'required'=>true, 'caption'=>'Identifiant d\'OS'],
['OS_DESCRIPTION', 'required'=>true, 'caption'=>'Description d\'OS']
]);
}
} // End of class SYSPC_OS
class SYSPC_MODEL extends \atk4\data\Model {
public $table = 'SYSPC_MODEL';
function init() {
parent::init();
$this->addFields([ ['MODEL_NAME', 'caption'=>'Nom du modele'],
['MODEL_BASE_RPM', 'caption'=>'Rpm de base']
]);
$this->hasOne('FK_OS_id',[new SYSPC_OS(),'ui'=>['visible'=>false]])->addField('OS_NAME','OS_NAME');
}
} // End of class SYSPC_MODEL
class DHCP_PC extends \atk4\data\Model {
public $table = 'DHCP_PC';
function init() {
parent::init();
$this->addFields([ ['PCNAME', 'required'=>true, 'caption'=>'Nom du pc']
]);
$this->hasOne('FK_OS_ID',['required'=>true,new SYSPC_OS(),'ui'=>['visible'=>false]])->addField('OS_NAME','OS_NAME');
$this->setOrder('PCNAME','asc');
$this->hasOne('FK_MODEL_id',['required'=>true,new SYSPC_MODEL(),'ui'=>['visible'=>false]])->addField('MODEL_NAME','MODEL_NAME');
}
} // End of class DHCP_PC
class PcForm extends \atk4\ui\Form {
function setModel($m, $fields = null) {
$PcWidth = 'three';
parent::setModel($m, false);
$gr = $this->addGroup('PC name');
$gr->addField('PCNAME',['required'=>true,'caption'=>'Nom du pc']);
$gr = $this->addGroup('OS');
$mm2 = new SYSPC_OS($this->app->db,['title_field'=>'OS_NAME']);
$gr->addField('FK_OS_ID',['width'=>$PcWidth],['DropDown'])->setModel($mm2);
$gr = $this->addGroup('Modèle');
$mm = new SYSPC_MODEL($this->app->db,['title_field'=>'MODEL_NAME']);
$mm->addCondition('MODEL_NAME', 'LIKE', 'DESK%'); // Works fine but I would like to display only the FK_MODEL_id values
// which exist in the SYSPC_MODEL table for the same FK_OS_ID
// than the current record FK_OS_ID value :
// SELECT SYSPC_MODEL.MODEL_NAME WHERE ( DHCP_PC.FK_OS_ID = SYSPC_MODEL.id )
$gr->addField('FK_MODEL_id', ['width'=>$PcWidth], ['DropDown'])->setModel($mm);
return $this->model;
}
} // End of class PcForm
$app = new \atk4\ui\App();
$app->title = 'Gestion des PC';
$app->initLayout($app->stickyGET('layout') ?: 'Admin');
$app->db = new \atk4\data\Persistence_SQL(
"pgsql:host=".$GLOBALS['dbhost'].";dbname=".$GLOBALS['dbname'],
$GLOBALS['dbuser'],
$GLOBALS['dbpass']
);
$g = $app->add(['CRUD', 'formDefault'=>new PcForm()]);
$g->setIpp([10, 25, 50, 100]);
$g->setModel(new DHCP_PC($app->db),['PCNAME', 'OS_NAME', 'MODEL_NAME']);
?>
Please look at https://github.com/atk4/ui/pull/551 - it might be what you're looking for.
Example here: https://ui.agiletoolkit.org/demos/autocomplete.php
Docs: https://agile-ui.readthedocs.io/en/latest/autocomplete.html?highlight=lookup#lookup-field
$form = $app->add(new \atk4\ui\Form(['segment']));
$form->add(['Label', 'Add city', 'top attached'], 'AboveFields');
$l = $form->addField('city',['Lookup']);
// will restraint possible city value in droddown base on country and/or language.
$l->addFilter('country', 'Country');
$l->addFilter('language', 'Lang');
//make sure country and language belong to your model.
$l->setModel(new City($db));
Alternatively you can use something other than drop-down, here is UI example:
https://ui.agiletoolkit.org/demos/multitable.php
Selecting value in the first column narrows down options in the next. You can have a hidden field inside your form where you can put the final value.
Thanks for your support but I still have some questions.
Question 1: I found "addRelatedEntity" and "relEntity" but I didn't found a description of those commands. Does it exist ? Is this a possible solution for my issue ?
Question 2: Is it possible to 'Lookup' in another table and if yes, how ?
Question 3: If 'Lookup' is not the solution, how to make a join (with filtering in the where clause) inside a model ?
Question 4: If the join is not the solution, is it possible to use DSQL inside a model ?
Question 5: Or do you have a DSQL example (with a self made join between several tables) associated with a CRUD ?

In cake php , How we can access value from the database inside foreach loop?

I am having one table
menu with field
id and name
1 suriya
2 kamal
3 ram
I want to retrive these value inside foreach loop in below order
1) kamal
2) suriya
3) ram
I am beginner in cake php.
Pretty vague question, but this should get you started. Put this code in your menu's controller.
$menus = $this->Menu->find('all');
foreach ($menus as $menu) {
echo $menu['Menu']['name'];
}
Regarding the order, if you're not ordering alphabetically, then you'll need to add a new column to your table, such as sort_order, and order that when you call $this->Menu->find('all')
create a model named Menu.php in app/models folder -
class Menu extends AppModel {
public $primaryKey = 'id';
public $useTable = 'menu';
}
And create a controller named MenuController.php in app/controller folder -
class MenuController extends AppController {
public $use = false;
public function index () {
$this->loadModel ('Menu');
$data = $this->Menu->find ('all', array ('order' => 'name ASC'));
foreach ($data as $value) {
echo $value['Menu']['name'] . "<br/>";
}
exit;
}
}
Then try it, could work. :)

CakePHP pass a array to paginate() - new model

Hi I've done a find() and added a new field to some of the results:
$approved = $this->ExpenseClaim->find('all', array('conditions'=> array('ExpenseClaim.claim_status_id' => '3')));
$i = 0;
foreach ($approved as $ap) {
$approved[$i]['ExpenseClaim']['claimTotal'] = $this->ExpenseClaim->expenseClaimTotal($approved[$i]['ExpenseClaim']['id']);
$i++;
}
I now need to pass this to paginate, however I read here that you cannot do this and that I must create another model to use the afterFind() method only on this one particular find.
So I've created the new Model called ExpenseClaimTotal and set the UseTable to
public $useTable = 'expense_claims';
Then in the new models afterFind() method I did a simple debug:
public function afterFind($results, $primary = false) {
debug($results);
//return $results;
}
But when I now try and do a find against this new model in pagesController it fails:
$this->loadModel('ExpenseClaimTotal');
$approved = $this->ExpenseClaimTotal->find('all', array('conditions'=> array('ExpenseClaim.claim_status_id' => '3')));
This is the error I get:
Database Error
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'ExpenseClaim.claim_status_id' in 'where clause'
SQL Query: SELECT `ExpenseClaimTotal`.`id`, `ExpenseClaimTotal`.`user_id`, `ExpenseClaimTotal`.`claim_status_id`, `ExpenseClaimTotal`.`created`, `ExpenseClaimTotal`.`modified`, `ExpenseClaimTotal`.`approved`, `ExpenseClaimTotal`.`approved_by`, `ExpenseClaimTotal`.`declined_by`, `ExpenseClaimTotal`.`date_submitted` FROM `expenses`.`expense_claims` AS `ExpenseClaimTotal` WHERE `ExpenseClaim`.`claim_status_id` = 3
There doesnt seem to be much in the docs about using 2 models for one table
You don't want to paginate an array
You're already performing a find, it's not sensible to perform a find and then paginate the resultant array.
Simply paginate your model data directly and inject your total values in the process. As such - if you put your original "added a new field to some of the results" logic in the model:
class ExpenseClaim extends AppModel {
public function afterFind($results, $primary = false) {
foreach ($results as &$ap) {
if (isset($ap['ExpenseClaim']['id'])) {
$ap['ExpenseClaim']['claimTotal'] = $this->expenseClaimTotal($ap['ExpenseClaim']['id']);
}
}
return $results;
}
}
Your controller code becomes simply:
public function index() {
$conditions = array('ExpenseClaim.claim_status_id' => '3');
$data = $this->paginate($conditions);
$this->set('data', $data);
}
And the code is simple and "just works".
Enhancements
The above is the simplest way to achieve the desired results, but has some disadvantages - namely it will call the total method on pretty much all finds.
Depending on exactly what you're doing you may wish to for example:
Cache your totals
If appropriate, you can remove problems by simply adding the field "claim_total" to the database, and recalculate whenever it changes. That would mean there is absolutely no extra logic when reading from the expense claim model.
Use a custom find type
If you don't want to recaculate the total on all finds - you can create a custom find type
class ExpenseClaim extends AppModel {
public $findMethods = array('allWithTotals' => true);
protected function _findAllWithTotals($state, $query, $results = array()) {
if ($state === 'before') {
return $query;
}
foreach ($results as &$ap) {
$ap['ExpenseClaim']['claimTotal'] = $this->expenseClaimTotal($ap['ExpenseClaim']['id']);
}
return $results;
}
And then use it in your paginate call:
public function index() {
$this->paginate['findType'] = 'allWithTotals'; # <-
$conditions = array('ExpenseClaim.claim_status_id' => '3');
$data = $this->paginate($conditions);
$this->set('data', $data);
}
In this way, only the index method will trigger the call to add the totals.

CakePHP label convention

When using $this->Form->input('fieldname'), CakePHP creates a label element that, by convention, uses the fieldname. I know I can specify another string to use for the label as an argument to the input method, but is there any way I can specify the label name in the model, so I don't have to repeat the label name in multiple ctp files (e.g. so I only have to change it in one place)?
So I created this and made a quick test which worked, however, can't guarantee it's bug free:
<?php
App::uses('FormHelper', 'View/Helper');
class MyFormHelper extends FormHelper {
public function label($fieldName = null, $text = null, $options = array()) {
if ($text === null) {
$entity = $this->entity();
$field = array_pop($entity);
$model = $this->model();
$object = $this->_models[$model];
if (isset($object->labels[$field])) {
$text = $object->labels[$field];
}
}
return parent::label($fieldName, $text, $options);
}
}
Drop that in app/View/Helper/MyFormHelper.php
Add it to the helper array and alias it if you want to.
Add a public $labels array to the model with field => label-text structure.
Hope it works.

CakePHP 2.0 - Use MySQL ENUM field with form helper to create Select Input

I've been researching a bit and I found that CakePHP's form helper doesn't interpret ENUM fields correctly, so it simply outputs a text input. I found a post that suggested to use a helper for that specific purpose. Does anybody know a better way to achieve this? Or if CakePHP devs intend to correct this some day?
Thanks for reading!
Below is one of the helper extention.
App::uses('FormHelper', 'View/Helper');
/**
* APP/View/Helper/MySqlEnumFormHelper.php
* It extends FormHelper to implement ENUM datatype of MySQL.
*
* http://blog.xao.jp/blog/cakephp/implementation-of-mysql-enum-datatype-in-formhelper/
*
* created Oct. 15, 2012
* CakePHP 2.2.3
*/
class MySqlEnumFormHelper extends FormHelper
{
public function input($fieldName, $options = array())
{
if (!isset($options['type']) && !isset($options['options'])) {
$modelKey = $this->model();
if (preg_match(
'/^enum\((.+)\)$/ui',
$this->fieldset[$modelKey]['fields'][$fieldName]['type'],
$m
)) {
$match = trim($m[1]);
$qOpen = substr($match, 0, 1);
$qClose = substr($match, -1);
$delimiter = $qOpen . ',' . $qClose;
preg_match('/^'.$qOpen.'(.+)'.$qClose.'$/u', $match, $m);
$_options = explode($delimiter, $m[1]);
$options['type'] = 'select';
$options['options'] = array_combine($_options, $_options);
}
}
return parent::input($fieldName, $options);
}
}
Cake attempts to be database agnostic and therefore this issue won't be "corrected" since it's not a bug. For example, SQL server doesn't have an exact equivalent of MySQL's ENUM field type.
I would recommend getting your possible list of enum values like so:
YourController.php
// get column type
$type = $this->Model->getColumnType('field');
// extract values in single quotes separated by comma
preg_match_all("/'(.*?)'/", $type, $enums);
// enums
var_dump($enums[1]);
Then use a select field in your view and pass the enums as options. Your current value you'll already have. How does that sound?
I am new to cakephp I found some old code and pieced together an enum select box for you enjoy
/**
* Behavior with useful functionality around models containing an enum type field
*
* Copyright (c) Debuggable, http://debuggable.com
*
*
*
* #package default
* #access public
*
* reworked by Nathanael Mallow for cakephp 2.0
*
*/
/*
*Use case:Add this (EnumerableBehavior.php) to app/Model/Behavior/
* -->in the Model add public $actsAs = array('Enumerable');
* -->in the *_controller add $enumOptions = $this->Categorie->enumOptions('Section');
* -->in the view add print $this->Form->input('{db_field_name}', array('options' => $enumOptions, 'label' => 'here'));
*
*
*/
class EnumerableBehavior extends ModelBehavior {
/**
* Fetches the enum type options for a specific field
*
* #param string $field
* #return void
* #access public
*/
function enumOptions($model, $field) {
//Cache::clear();
$cacheKey = $model->alias . '_' . $field . '_enum_options';
$options = Cache::read($cacheKey);
if (!$options) {
$sql = "SHOW COLUMNS FROM `{$model->useTable}` LIKE '{$field}'";
$enumData = $model->query($sql);
$options = false;
if (!empty($enumData)) {
$enumData = preg_replace("/(enum|set)\('(.+?)'\)/", '\\2', $enumData[0]['COLUMNS']['Type']);
$options = explode("','", $enumData);
}
Cache::write($cacheKey, $options);
}
return $options;
}
}
?>
If you want to use MySqlEnumFormHelper instead of normal and call it by $this->Form-> instead by $this->MySqlEnumFormHelper . You should add this line in your controller to alias MySqlEnumFormHelper as Form.
public $helpers = array('Form' => array(
'className' => 'MySqlEnumForm'
));
/* comments about previus answers ***
Use case:Add this (EnumerableBehavior.php) to app/Model/Behavior/
-->in the Model add public $actsAs = array('Enumerable');
-->in the action of the *_controller add $enumOptions = $this->YourModelName->enumOptions('db_field_name'); $this->set('enumOptions',$enumOptions);
-->in the view add print $this->Form->input('{db_field_name}', array('options' => $enumOptions, 'label' => 'here'));
*
*/
i think the Behaviour way it's good...but the array keys are integer
so i have modified the function like this
function enumOptions($model, $field) {
//Cache::clear();
$cacheKey = $model->alias . '_' . $field . '_enum_options';
$options = Cache::read($cacheKey);
$enumOptions = array();
if (!$options) {
$sql = "SHOW COLUMNS FROM `{$model->useTable}` LIKE '{$field}'";
$enumData = $model->query($sql);
$options = false;
if (!empty($enumData)) {
$enumData = preg_replace("/(enum|set)\('(.+?)'\)/", '\\2', $enumData[0]['COLUMNS']['Type']);
$options = explode("','", $enumData);
foreach ($options as $option) {
$enumOptions["$option"] = $option;
}
}
Cache::write($cacheKey, $enumOptions);
}
return $enumOptions;
}
in order to be able to save the right value in the db field when the form is submitted
I created a function that goes into AppController to handle this. I combined some of the information provided above.
Usage:
$enumList = getEnumValues($ModelField) where ModelField is in this format: 'Model.Field'
Function that I put in AppController:
function getEnumValues($ModelField){
// split input into Model and Fieldname
$m = explode('.', $ModelField);
if ($m[0] == $ModelField) {
return false;
} else {
(! ClassRegistry::isKeySet($m[0])) ? $this->loadModel($m[0]): false;
$type = $this->$m[0]->getColumnType($m[1]);
preg_match_all("/'(.*?)'/", $type, $enums);
foreach ($enums[1] as $value){$enumList[$value] = $value;}
return $enumList;
}
}

Resources