CakePHP set redirect parameters using an array - cakephp

I am writing some simple redirects in CakePHP 2.4.2 and came across one that stumped me a bit.
How can I redirect to another view with the same passedArgs? It seems like it should be simple, but I must be overlooking something. I tried this:
$this->redirect(array_merge(array('action' => 'index',$this->params['named'])));
The debug output seems correct:
array(
'action' => 'index',
(int) 0 => array(
'this' => 'url',
'and' => 'stuff'
)
)
My desired outcome is that
view/this:url/and:stuff
redirects to
index/this:url/and:stuff
but now it just sends me to
index/
Not sure what I am missing here, perhaps I have deeper configuration issues - although it's not a terribly complicated app.

For passed params (numeric keys) use array_merge.
But since you use named params, which have a string based key, you can leverage PHP basics:
$this->redirect(array('action' => 'index') + $this->request->params['named']));
This adds all array values from the named params except for action, which is already in use here (and hence should not be overwritten anyway). So the priority is clear, as well.

Cake expects a flat array of parameters, so you need to use array_merge to add in extra arrays to it on the sides. Try this:
$this->redirect(array_merge(array('action' => 'index'), $this->params['named']));
... or using your original variable $passedArgs:
$this->redirect(array_merge(array('action' => 'index'), $this->passedArgs));

Maybe better solution will be used persist attribute in Router::connect()?

Related

Make CakePHP consider two different routes/models for same path?

I have a website that has users and entries, both of which are stored in a database. Every entry has its own page, using its slug, and every user has a public profile, using its username. Their respective URLs might look something like this:
Entry: https://example.com/hello-world
User: https://example.com/test-user
Refactoring said website with CakePHP 3.4 (the original being built with “vanilla” PHP), I implemented the following routes:
$routes->connect('/:slug',
['controller' => 'Entries', 'action' => 'view'],
['pass' => ['slug']]
);
$routes->connect('/:username',
['controller' => 'Users', 'action' => 'view'],
['pass' => ['username']]
);
The entry pages work like a charm — no problem there — but when I try to access a user profile, Cake throws a RecordNotFoundException. This makes sense, since it’s looking for an entry that does no exist.
I was hoping switching from firstOrFail to find in the EntriesController would allow the application to continue with the next route in line (because no exception would be thrown), but the result is actually worse: It tries to render the entry view without an object, causing PHP notices on an otherwise empty layout.
I have read the CakePHP documentation (“Book”), but could not find a solution to this (I would assume rather generic) problem. I have also tried many other (often less obvious) route setups, but no luck there either.
Now my mind keeps going to something like a EntryOrUserController, but I doubt that would be the best solution, or even a good one. Frankly, I think it’s silly. I guess I am really hoping for some controller or middleware function that does exactly what I want out of the box, but any elegant solution would do.
P.S. I do realize that the default CakePHP/MVC way of going about this would be to have URLs a bit more like this:
Entry: https://example.com/entries/hello-world
User: https://example.com/users/test-user
…but that is not an option in this case, so thanks but no. ☺
Thanks to ndm for pointing me in the right direction. After some tinkering I have a solution that works nicely. Posting it here because it might be of use to others. There are three steps.
1. Create custom routing class
/src/Routing/Route/SlugRoute.php:
namespace App\Routing\Route;
use Cake\Routing\Route\Route;
use Cake\ORM\Locator\LocatorAwareTrait;
class SlugRoute extends Route
{
use LocatorAwareTrait;
public function parse($url, $method = '')
{
$params = parent::parse($url, $method);
if (!$params ||
!isset($this->options['model']) ||
!isset($this->options['pass'][0])
) {
return false;
}
$count = $this
->tableLocator()
->get($this->options['model'])
->find()
->where([
$this->options['pass'][0] => $params['pass'][0]
])
->count();
if ($count !== 1) {
return false;
}
return $params;
}
}
2. Apply new routing class to relevant routes
$routes->connect('/:slug',
['controller' => 'Entries', 'action' => 'view'],
['pass' => ['slug', 'name'], 'routeClass' => 'SlugRoute', 'model' => 'Entries']
);
$routes->connect('/:username',
['controller' => 'Users', 'action' => 'view'],
['pass' => ['username'], 'routeClass' => 'SlugRoute', 'model' => 'Users']
);
3. Consider a few things
I’m also telling the routes which models to use. It would probably be nice if the routing class figures this out by itself.
The routing class assumes we want to do a lookup on the first value of the pass array. That’s fine in this case (with only slug and username being passed), but it’s not very transparent and easily broken.

Dynamically add virtual field in cakephp

I am using CakePHP 2.1.3; I want to create a virtual field dynamically in a controller. Is it possible?
The problem is when I am trying to find max value in a table it gives me another array from the model array. Please ask if you need more information.
When I am trying to execute the following query,
$find_max_case_count = $this->CaseMaster->find('first', array(
'conditions' => array(
'CaseMaster.CLIENT_ID' => $client_id,
'CaseMaster.CASE_NO LIKE' => '%-%'
),
'fields' => array('max(CaseMaster.CASE_NO) AS MAX_NO')
));
It is giving me an array like:
[0]=> array([MAX_NO]=> 52)
However I want it to be like as:
[CaseMaster] => array([MAX_NO] => 52)
I found a solution. I can make the virtual field at runtime. The code should looks like:
$this->CaseMaster->virtualFields['MAX_NO'] = 0;
Write it just above the find query and the query will remain same as it was written.
This link was helpful to find out the solution.
There is no way (as far as I am knowledgeable) to create virtual fields "on the fly". What virtual fields are is "arbitrary SQL expressions" that will be executed when a find runs through the Model and "will be indexed under the Model's key alongside other Model fields".
What do you need to do with "dynamically created virtual fields"? If you explain what exactly you need to accomplish maybe we can provide a different (even more suitable? :) ) solution? I'd personally be happy to help you.
After you editing your question I can say that what you're getting is the way the array should be returned, this is because of the fields parameter. If you want to get a different structure out of it I suggest applying a callback to format it.
Firstly move the method inside the CaseMaster Model:
public function getMaxCaseCount($client_id){
$data = $this->find('first', array(
'conditions' => array(
'CaseMaster.CLIENT_ID' => $client_id,
'CaseMaster.CASE_NO LIKE' => '%-%'),
'fields' => array('max(CaseMaster.CASE_NO) AS MAX_NO')));
return array_map(array('CaseMaster', '__filterMaxCaseCount'), $data);
}
private function __filterMaxCaseCount($input){
//This is just an example formatting
//You can do whatever you would like here.
$output['CaseMaster'] = $input[0];
return $output;
}
The array_map function will apply the __filterMaxCaseCount callback method so that when you call:
$this->CaseMaster->getMaxCaseCount($client_id);
from your controller you will get the data in the way you need it. The array_map function could also look like this:
return array_map(array($this, '__filterMaxCaseCount'), $data);
because you're in the same Class.
Just adding your model alias to field definition also works for this purpose
'fields' => array('max(CaseMaster.CASE_NO) AS CaseMaster__MAX_NO')

$this->find not working in cakePHP

I have a model with the name Deal class Deal extends AppModel
Now in my controller I call a method in the Deal model called getDeal()
$dealInfo = Deal::getDeal($dealID);
I want the info returned to me but the var_dump displays blank
function getDeal($dealID){
$deal = $this->Deal->find('first', array(
'conditions' =>
'Deal.id' =>$dealID
) ,
'fields' => array(
'Deal.id'
) ,
'recursive' => 1,
));
}
This is the first time I'm working in cakePHP, so this question might sound a bit dumb
When you're just using an id as your find condition you can use CakePHP's dynamic finder methods.
Dynamic finders work like this.
$this->Model->findByModelFieldName();
In your example it would be
$this->findById($dealId, array(
'fields' => array('Deal.id'),
'recursive' => 1
));
I don't know if I'm just being mental, but is this simply because you're not returning anything from getDeal()?
Try adding return $deal; and see if that helps. Your question doesn't state exactly where you're doing the var_dump so I might be well off.
Edit:
As per the discussion with you and 8vius, we've established that this isn't right, and you simply need to change $this->Deal->find() to $this->find() because its being run from the model.
Check your function declaration, $deal_id does not exist and as you can see from the parameter you pass to the function which should me $deal_id and not dealID. So you have a misdeclared function calling find with a variable that does not exist.

CakePhp Router:connect, I don't get the syntax

I am trying to connect the next urls:
1) /food/tips
2) /happiness/tips/best_tips
To the following objects:
1) controller=tips / action=index / passed_parameters=food
2) controller=tips / action=index / passed_parameters=(happiness,best_tips)
--edit--
These routes are not fixed.
Meaning: what I try to do is to route every url that have tips as action, to the tips controller, to any fixed(index is good enough) action, and chaining the rest of the url as it was in the original call.
Something like /any_controller/tips/any_param to /tips/index/any_params
-- end edit --
Hope that now there is some sense.
How should it be done?
(please - also explain)
Thanks
the routing is all done through the call Router::connect('thing to catch', 'where to send it');
so it can be as simple as:
Router::connect('/food/tips', '/tips/index/food');
or the preferred method (using cakes built in url builder)
Router::connect('/food/tips/*', array('controller' => 'tips', 'action' => 'index', 'food');
The first method takes a string argument and passes it to another string which would be a url and you would then have to catch it in your controller, and expect a passed parameter through the url.
The second method uses cakes built in url former which takes an array with keys controller and action (there are other options: http://api.cakephp.org/class/router#method-Routerurl)
The second is preferred due to proper formatting and future flexibility (I believe).
any passed parameters in the second method are just passed as un-named items in the array. named parameters are just passed as keyed elements. So if I wanted to create a URL like this
/posts/index/find:all/page:2
I would write the url like this:
Router::connect('/url_to_catch', array('controller' => 'posts', 'action' => 'index', 'find' => 'all', 'page' => 2);
So just to finish up, I would actually pass your parameter through as named:
Router::connect('/happiness/tips/best_tips', array('controller' => 'tips', 'action' => 'index', 'items' => array('happiness', 'best_tips'));
which would need a function in your tips controller that looks like this:
function tips(){ $this->passedArgs['items']; }
I would recommend reading the chapter on Routing the the book, as it will explain things better than I can and it seems counter productive to paste it here.
http://book.cakephp.org/#!/view/948/Defining-Routes
For the sake of explanation I will try,
Router::connect('/food/tips', array('controller' => 'tips', 'action' => 'index', 'food'));
Router::connect('/happiness/tips/best_tips', array('controller' => 'tips', 'action' => 'index', 'happiness','best_tips'));
This should get things working for you. What you are essentially doing is telling the Cake Routing what url you want it to capture, as it will be doing this using Regex. Then you want to tell it which code you want it to run. So this takes a Controller and Action pair, as a set of things to do.
You also want to pass through your named paremeters afterwards. These will tack onto the function in your controller so that you can do stuff with them.
It's quite easy, just check the Router configuration in the manual. You have to use the connect method from the Router class. This accepts 2 parameters. First your desired routed (e.g. food/tips) and second an array with the actual path it should follow. So for your examples you'd do something like this:
Router::connect('/food/tips', array('controller' => 'tips', 'action' => 'index', 'food');
Router::connect('/happiness/tips/best_tips', array('controller' => 'tips', 'action' => 'index', 'happiness', 'best_tips');
This is equivalent to calling TipsController->index('food') and TipsController('happiness', 'best_tips) respectively.
However, your routes look a bit funny. The Cake convention for routes is /controller/action/param1/param2/etc where the parameters param1 etc. are optional and the index action is assumed when no other action is given.
You're taking a different approach and I would suggest (if you can) change it to the Cake conventional routes, as this will save you a lot of work later on because Cake will automatically connect these routes to the desired methods.
So my suggestion is going for tips/food and tips/happiness/best_tips instead of the routes you suggest. This way, you don't have to do any router configuration.
UPDATE
After you're edit, I think it's best to try something with defining custom routes. I can't test this for you at the moment, so you should do some testing yourself, but in that case it would be something like:
Router::connect('/:section/tips/:param',
array('action' => 'index'),
array(
'section' => '[a-z]*',
'param' => '[a-z]*'
)
);
UPDATE2
Sorry, I've tested the above and it doesn't seem to work.

CakePHP AutoComplete Question

I am working on a book review application and I am using autoComplete to search for titles in when creating a review. The review model has an associated book_id field and the relationship is setup as the review hasOne book and a book hasMany reviews.
I am trying to pass the Book.id (into the book_id field), but I want to display Book.name for the user to select from. With the default setup (accomplished via CakePHP's tutorial), I can only pass Book.name. Is it possible to display the name and pass the id?
Also, I am passing it via the following code in the create() action of the review controller:
$this->data['Review']['book_id'] = $this->data['Book']['id'];
Is that the proper way to do it in CakePHP? I know in Ruby on Rails, it is automatic, but I can't seem to make it work automagically in CakePHP. Finally, I am not using the generator because it is not available in my shared hosting environment... so if this is the wrong way, what do I need other than associates in my models to make it happen automatically?
Thanks for the help and I promise this is my question for awhile...
UPDATE- I tried the following, but it is not working. Any ideas why?
function autoComplete() {
$this->set('books', $this->Book->find('all', array(
'conditions' => array(
'Book.name LIKE' => $this->data['Book']['name'].'%'
),
'fields' => array('id','name')
)));
$this->layout = 'ajax';
}
The problem is that when I use the code above in the controller, the form submits, but it doesn't save the record... No errors are also thrown, which is weird.
UPDATE2:
I have determine that the reason this isn't working is because the array types are different and you can't change the array type with the autoComplete helper. As a workaround, I tried the follow, but it isn't working. Can anyone offer guidance why?
function create() {
if($this->Review->create($this->data) && $this->Review->validates()) {
$this->data['Review']['user_id'] = $this->Session->read('Auth.User.id');
$this->Book->find('first', array('fields' => array('Book.id'), 'conditions' => array('Book.name' => $this->data['Book']['name'])));
$this->data['Review']['book_id'] = $this->Book->id;
$this->Review->save($this->data);
$this->redirect(array('action' => 'index'));
} else {
$errors = $this->Review->invalidFields();
}
}
FINAL UPDATE:
Ok, I found that the helper only takes the find(all) type or array and that the "id" field wasn't passing because it only applied to the autoComplete's LI list being generated. So, I used the observeField to obtain the information and then do a database lookup and tried to create a hidden field on the fly with the ID, but that didn't work. Finally, the observeField would only take the characters that I put in instead of what I clicked, due to an apparent Scriptaculous limitation. So, I ended up going to a dropdown box solution for now and may eventually look into something else. Thanks for all of the help anyway!
First of all, $this->data will only contain ['Book']['id'] if the field exists in the form (even if it's hidden).
To select something by name and return the id, use the list variant of the find method, viz:
$selectList = $this->Book->find('list', array(
'fields' => array(
'id',
'name'
)));
$this->set('selectList', $selectList);
In the view, you can now use $selectList for the options in the select element:
echo $form->input('Book.id', array('type' => 'hidden'));
echo $form->input('template_id', array(
'options' => $selectList,
'type' => 'select'
));

Resources