Translate url cakephp - cakephp

I'm creating a page in CakePHP, i'm trying to translate the url. What i want is to translate the controller name so let says i have this url domain/da/product then it should translate it into danish so it became domain/da/produkt.
I've written my own url method in AppHelper but i have problems with accessing the model for translation. When i search for accessing model from helper, people are against doing it that way.
So what will be a proper way to do this. When the user use the link i need to make a lookup where i translate it back to domain/da/product.
Here is the Apphelper code
App::uses('Helper', 'View');
App::import("Model", "ControllerTranslation");
class AppHelper extends Helper {
public function url($url = null, $full = false) {
$Model = new ControllerTranslation();
$lang = Configure::read('Config.language');
$controller = $Model->find("first",array("conditions"=> array("ControllerTranslation.translation = "=> $url['controller'],"ControllerTranslation.language" => $lang)));
if (count($controller))
{
$url['controller'] = $controller["ControllerTranslation"]["translation"];
}
return parent::url($url, $full);
}

if you want the url that has translation = $url['controller'] and language = $lang then you should use syntax:
$controller = $Model->find("first",
array("conditions"=>
array(
"ControllerTranslation.translation" => $url['controller'],
"ControllerTranslation.language" => $lang
)
)
);

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 ?

List all controllers/actions in Cakephp 3

How do I list all the controllers/actions on my site? Configure::listObjects('model') doesnt seem to exist anymore. I am trying to write a function to generate/add to the ACO's in my ACL setup. Thanks.
So here is what I did. In my Resource Controller:
Include the reflection class/method libraries
use ReflectionClass;
use ReflectionMethod;
To get the controllers:
public function getControllers() {
$files = scandir('../src/Controller/');
$results = [];
$ignoreList = [
'.',
'..',
'Component',
'AppController.php',
];
foreach($files as $file){
if(!in_array($file, $ignoreList)) {
$controller = explode('.', $file)[0];
array_push($results, str_replace('Controller', '', $controller));
}
}
return $results;
}
And now for the actions:
public function getActions($controllerName) {
$className = 'App\\Controller\\'.$controllerName.'Controller';
$class = new ReflectionClass($className);
$actions = $class->getMethods(ReflectionMethod::IS_PUBLIC);
$results = [$controllerName => []];
$ignoreList = ['beforeFilter', 'afterFilter', 'initialize'];
foreach($actions as $action){
if($action->class == $className && !in_array($action->name, $ignoreList)){
array_push($results[$controllerName], $action->name);
}
}
return $results;
}
Finally, to tie them boths together:
public function getResources(){
$controllers = $this->getControllers();
$resources = [];
foreach($controllers as $controller){
$actions = $this->getActions($controller);
array_push($resources, $actions);
}
return $resources;
}
I hope that helps some people.
It doesn't look like anything similar to this is still available in Cake3, nor is it still needed because of the namespaces I think.
So in short you can try to do this:
Read all controllers from the app level controller folder
Read all plugin controller folders (Get the plugin folder via Plugin::path())
Iterate over the controllers you've collected in the previous steps (You'll need to use App::uses())
Use reflections to get the public methods from each controller
I am using CakePHP 3.x and had problems with the function "getActions"
The correct syntax for "ReflectionClass" and "ReflectionMethod" is:
public function getActions($controllerName) {
$className = 'App\\Controller\\'.$controllerName.'Controller';
$class = new \ReflectionClass($className);
$actions = $class->getMethods(\ReflectionMethod::IS_PUBLIC);
$results = [$controllerName => []];
$ignoreList = ['beforeFilter', 'afterFilter', 'initialize'];
foreach($actions as $action){
if($action->class == $className && !in_array($action->name, $ignoreList)){
array_push($results[$controllerName], $action->name);
}
}
return $results;
}
Warning for "\" before ReflectionClass and ReflectionMethod.

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, i18n, Retrieve translation records for associated models

Quoting from the cakephp Book (ver 1.3):
Note that only fields of the model you are directly doing find on will be translated. Models attached via associations won't be translated because triggering callbacks on associated models is currently not supported.
Has anyone come up with a solution for this?
If not could you give me some pointers concerning the following simple scenario.
I have 2 models:
Project, Category.
Project HABTM Category
I have properly set up i18n table and I have a few entries in the db, all translated. When I retrieve a project it does retrieve the translation but not the translated category because as it says in the cakephp book models attached via associations won't be translated.
I have another workaround; I don't know if it is any better or worse performance- or style-wise, only that it suits the "fat models, skinny controllers" goal:
AppModel.php
public function getTranslatedModelField($id = 0, $field) {
$res = false;
$db = $this->getDataSource();
$tmp = $db->fetchAll('SELECT content from s2h_i18n WHERE model = ? AND locale = ? AND foreign_key = ? AND field = ? LIMIT 1',
array($this->alias, Configure::read('Config.language'), $id, $field)
);
if (!empty($tmp)) {
$res = $tmp[0]['s2h_i18n']['content'];
}
return $res;
}
SomeModel.php
public function afterFind($results, $primary = false) {
foreach ($results as $key => $val) {
if (isset($val['SomeOtherModel']) && isset($val['SomeOtherModel']['id'])) {
$results[$key]['SomeOtherModel']['name'] =
$this->SomeOtherModel->getTranslatedModelField($val['SomeOtherModel']['id'], 'name');
}
// other possible queries for other models and/or fields
}
return $results;
}
OK I found a solution. Which is mostly a workaround. I should have thought of that earlier.
What I'm doing is the following. I'm finding all projects and recursively all categories associated with projects. Now since cakephp does not translate categories I am using the results from the initial query and I am performing a second one only for categories but using the category id values that I found on the first query. Now cakephp translates categories since I'm only searching for them and I can have their data translated.
At the moment I'm OK with this solution but it would be nice if first cakephp makes the translate behavior out of the box ready or secondly if someone had a behavior that could support retrieval of translation on associated models.
I generalized the afterFind part a bit, so that it automatically grabs the fields to translate from the associated models' actsAs["Translate"] array, and uses an array of associated models to (potentially) translate:
public function afterFind($results, $primary = false){
$modelsToTranslate = array("SomeModel", "AnotherModel");
foreach ($results as $key => $val){
foreach($modelsToTranslate as $mtt){
if (isset($val[$mtt])){
foreach($val[$mtt] as $fieldname => $fieldval){
foreach ($this->$mtt->actsAs["Translate"] as $fieldToTranslate){
$results[$key][$mtt][$fieldname][$fieldToTranslate] = $this->$mtt->getTranslatedModelField($val[$mtt][$fieldname]['id'], $fieldToTranslate);
}
}
}
}
}
return $results;
}
I took above solution and generalized both functions a bit, now it needs to be used together with the translate behaviour and both functions need to go into the model.php - everything else should work by itself:
public function getTranslatedModelField($id = 0, $field) {
$res = false;
$translateTable = (isset($this->translateTable))?$this->translateTable:"i18n";
$db = $this->getDataSource();
$tmp = $db->fetchAll(
"SELECT content from {$translateTable} WHERE model = ? AND locale = ? AND foreign_key = ? AND field = ? LIMIT 1",
array($this->alias, Configure::read('Config.language'), $id, $field)
);
if (!empty($tmp)) {
$res = $tmp[0][$translateTable]['content'];
}
return $res;
}
public function afterFind($results, $primary = false) {
if($primary == false && array_key_exists('Translate', $this->actsAs)) {
foreach ($results as $key => $val) {
if (isset($val[$this->name]) && isset($val[$this->name]['id'])) {
foreach($this->actsAs['Translate'] as $translationfield) {
$results[$key][$this->name][$translationfield] =
$this->getTranslatedModelField($val[$this->name]['id'], $translationfield);
}
} else if($key == 'id' && is_numeric($val)) {
foreach($this->actsAs['Translate'] as $translationfield) {
$results[$translationfield] =
$this->getTranslatedModelField($val, $translationfield);
}
}
}
}
return $results;
}

Resources