all
I know View::share should be available anywhere within my application, A common place is in my routes.php file.
I am doing this.
1) User login system, after login successfully, share something for all the views.
2) when user click other pages, can use the shared variables.
Ok, my controller.
public function index() {
$data = array(
"pageTitle" => "index",
);
$privilegeMenu = $this->privilegeApi->getUserPrivilegeByUserId(Session::get('uid'));
$topMenus = $this->menuApi->getTopMenuById($privilegeMenu);
$subMenus = $this->menuApi->getSubMenuesById($privilegeMenu);
View::share('topMenus', $topMenus);
View::share('subMenus', $subMenus);
return View::make('home.index',$data);
}
So, because I am using View share, that it is to say, in every other views, I can use topMenus and subMenus now.
But when I click other pages, I got the error: Undefined variable: topMenus.
So, I am so confused what happened? I dive into View::share source code
Laravel have a class named Environment under namespace Illuminate\View.
protected $shared = array();
public function share($key, $value = null)
{
if ( ! is_array($key)) return $this->shared[$key] = $value;
foreach ($key as $innerKey => $innerValue)
{
$this->share($innerKey, $innerValue);
}
}
public function shared($key, $default = null)
{
return array_get($this->shared, $key, $default);
}
And I found when user login successfully, topMenus shared successfully. But when I click other pages, can't topMenus in shared.
It seems everything OK, I am confused. Any one knows ?
Thanks in advanced.
This is because your View::share statement is never ran for other routes. In Laravel 4.x, there are quite a few places you could put it where it would be ran every time, but these two are most commonly used:
The easiest and most simple way is to just add it at the end of your app/start/global.php which is ran at the start of every request, for all environments.
The other way is to create a new service provider, make sure it's autoloadable and add it to your app/config/app.php's providers array.
If all you want is share a few views, I'd say it's kinda of an overkill to create a whole new class just for that. If, however, you start noticing your app/start/global.php file is getting too cluttered, I'd recommend you start splitting stuff into service providers.
PS: In Laravel 5, app/start/global.php was removed and you're left only with the second option.
Related
I made a view composer in Laravel 5. When i use a wildcard *, to add something to all my views, it get's called at least twice. It runs when my master template is loaded, and again when my content page is included. This will give problems in the future, because it executes the query it does multiple times. I was able to fix the multiple querying by storing it in a static variable :
class StoreComposer {
static $composed;
public function __construct(StoreRepository $repository)
{
$this->store = $repository;
}
public function compose(View $view)
{
if(static::$composed)
{
return $view->with('store', static::$composed);
}
static::$composed = $this->store->pushCriteria( new WithCategories() )
->pushCriteria( new WithSettings() )
->applyCriteria()
->all();
$view->with('store', static::$composed);
}
}
My question is, is there a way to make sure it only runs once, no matter how many views i load, or is there another solution to this? The way i fixed it now doesn't feel right to me. Thanks!
Unfortunately no, there is no way to make it run once, due to the way View Composers are implemented. The Illuminate\View\View::renderContents() method is responsible for calling the composer bound to a view, and since any form of view rendering (Blade template inheritance or simple #include statements) executes that method, it means that when any view is rendered any composer bound to it gets triggered.
Since in your case you use a * wildcard to bind all views, if your page renders ten views, the composer will get executed ten times. However your approach looks like a good solution to solve this shortcoming.
You can use config here to resolve multiple times query run issue for example show below code.
public function compose(View $view)
{
if(!Config::has('composeVars'))
{
Config::set('composeVars') = [
'data' => User::all();
];
}
$view->with('*', Config::get('composeVars'));
}
Try this singleton solution or use cache https://laracasts.com/discuss/channels/laravel/executing-a-view-composer-only-once
On Laravel 5.6.38 works fine
i'm trying to implement Respect/Rest in my existing CMS.
The Problem:
I would like to create a single magic route to works like: /admin/*/save, calls the * controller...
I would like to make something like this:
$r->any('/admin/*/save/*/', function($controller, $id = null) use ($r) {
return $r->dispatchClass($controller,array($id));
});
Note that i don't know which HTTP method user is using.
Actually I "solved" this problem with something like:
$r->any('/admin/*/save/*/', function($controller, $id = null) use ($tcn) {
$r = new Router;
$r->any('/admin/*/save/*/', $tcn($controller . '_save'), array($id));
return $r->run();
});
$tcn is a named function that returns the full namespace of the controller.
I know it's not a good approach.
EDIT:
This project wants to be Open Source, but it's still being created.
We're trying to transport an old project made on functional paradigm to OOP.
We are trying to learn about OOP while making an useful project.
Actuall state of the files can be found at: https://github.com/dindigital/skeleton
Alganet: The bootstrap for admin routes can be found at: https://github.com/dindigital/skeleton/blob/master/admin_routes.php
A simple controller sample: https://github.com/dindigital/skeleton/blob/master/src/app/admin/controllers/TagController.php
https://github.com/dindigital/skeleton/blob/master/src/app/admin/controllers/TagSaveController.php
I liked the Forwards and also the Factory approach... I could not decide yet.
Tricky question! That depends a lot of why are you making these routes dynamic. Can you show us some sample structure for your controllers so I can improve the answer?
Meanwhile, two native features that can help:
Forwards
You can treat the problem as an internal forward (does not make redirects). It's normally to redirect to other static routes, but you can redirect to a new one as well:
$r->any(
'/admin/*/save/*/',
function ($controller, $id) use ($tcn, $r) {
return $r->any(
"/admin/$controller/save",
$tcn($controller . '_save'),
array($id)
);
}
);
Factory Routes
Also, Respect\Rest implements factory routes. It is experimental but stable in tests:
$r->factoryRoute(
'ANY',
'/admin/*/save/*/',
'MyAbstractController',
function ($method, array $params) use ($tcn) {
return new $tcn($params[0] . '_save');
}
);
Let's keep it simple.
A project has just two models:
User (hasMany Project)
Project (belongsTo User)
Users are only allowed to perform actions on the projects which they own. No one else's.
I know how to manually check who the logged in user is and whether or not he/she owns a specific project, but is there a better, more global way to do this? I'm looking for a more D.R.Y. way that doesn't require repeating the same validation inside multiple actions. For example, is there a config setting like maybe...
Configure::write('Enforce_belongs_to', true);
...or maybe a setting/option on the Auth component.
Maybe this is crazy, but I figured I'd ask.
Adding to Nunser's answer, here would be a general concept of how the behavior would be. You can then attach it to the applicable model.
class StrongBelongBehavior extends ModelBehavior
{
public function beforeFind( Model $Model, $query = array() ) {
$query['conditions'] = array_merge( (array)$query['conditions'], array( $Model->alias.'.user_id' => CakeSession::read("Auth.User.id" ) );
return $query;
}
public function beforeSave( Model $Model ) {
$projectId = Hash::get( $Model->data, 'Poject.id' );
if( $projectId ) {
$Model->loadModel('UserProject'); // UserProject is a custom model
$canEdit = $Model->UserProject->projectIDExists( $projectId ); // returns true if projectId belongs to the current user
if ( ! $canEdit ) {
return false;
}
}
return true;
}
}
I'm not sure if what I'm answering is the best-utermost-dry-it's-almost-dehydrating approach, but is the simplest thing I could think of.
In the Project model, create a function that return an array of project ids associated to an user.
class Project extends AppModel {
public function getByUserId($userId) {
$projectsArray = array();
if ($userId != "valid")
//do all the checks, if it's not null, numeric, if the id exists, etc
$projects = $this->Project->find('all', array('conditions'=>
array('user_id'=>$userId)));
if (!empty($projects)) {
foreach($projects as $i => $project)
$projectsArray[] = $project['Project']['id'];
}
return $projectsArray;
}
}
You mention a find('first') in your comment, but I'm assuming you want all the projects related to the user instead of just the first. If not, it's a simple modification of that function. Also, I'm just getting the ids, but you may want an $id=>$name_project array, up to you.
Now, I don't know what you mean by "only allowed to perform actions", is it just edits that are restricted? Or lists or views should be restricted and not even shown to the user if the project is not his/hers?
For the first case, restrict editing, modify beforeSave.
public function beforeSave($options = array()) {
if(!$this->id && !isset($this->data[$this->alias][$this->primaryKey])) {
//INSERT
//not doing anything
} else {
//UPDATE
//check if project inside allowed projects array
$allowed = $this->getByUserId(CakeSession::read("Auth.User.id"));
if (!in_array($this->id, $allowed))
return false; //or throw error and catch it in the controller
}
return true;
}
The code is untested, but in general terms, you prevent the edit of a project that is not "the user's" just before the update of the record. I assume the insert of new projects is free for everyone. According to this post, all saving functions except saveAll pass through this filter first, so you will need to overwrite the saveAll function and add a validation similar to the one in beforeSave (as explained in the answer there).
And for the second part, filtering results so the user isn't even aware that there are other projects instead of his/hers, change beforeFind. The docs talk about restricting results based on user's roles, so I guess we're on the right track.
public function beforeFind($queryData) {
//force the condition
$allowed = $this->getByUserId(CakeSession::read("Auth.User.id"));
$queryData['conditions'][$this->alias.'.user_id'] = $allowed;
return $queryData;
}
Since the $allowed array has just id values, it'll work like an IN clause, but if you change that array structure, be sure to modify these functions accordingly.
And that's it. I'm thinking about the more basic cases here, edit, view, delete... Ups, delete... change the beforeDelete function also, to avoid any evil users who want to delete others property. The logic remains the same (check if project id is in allowed array, if not, return false or throw error), so I won't add the example of that function here. But that's the basic stuff. If for some reason you want to have the allowed projects in the controller, call the getByUserId function in beforeFilter and handle that ids array there. You can even store it in session, but you'll have to have in mind maintaining that session when adding or deleting projects.
If you want a superadmin that can see and edit everything, just add a condition in getByUserId that checks the role of the user, and if it is an admin, return all projects.
Also, keep in mind: maybe Project has many... subprojects (not much imagination), and so, the user related to the project can add subprojects, but the same evil user as before modifies the hidden project_id that subproject has and edits it. In that case, I recommend you also add a validation in Subproject to avoid actions on models related to Project that are not his. If you have the Security component in place and the edit and delete actions can just be reached by forms, this is a minor thing because Security Component well used avoids form tampering. But give it a thought to see if you need to validate "Subproject" instances also.
As Ayo Akinyemi mentioned, you can use all this as a behavior. I haven't personally done so, but it meets the requirements, all the callbacks modified here are what you modify in a behaviour. You'll have to encapsulate the logic, column names (need to be variable an not set hardcoded, like user_id), etc, but it will be reusable in any other cake project you'll have. Something like StrongBelongBehavior or MoreDRYBehavior. And share it if you do it :)
I'm not sure if Auth component has some way of doing what you want, but that would be the best option I guess. Until some enlightens me (I haven't investigate much this issue), this is the solution I'd use.
I'm looking for a way to programatically get a list of controllers in a Kohana application.
Something like:
public function build_site_map(){
$controllers = Kohana::get_controllers();
echo '<ul>';
foreach($controllers as $controller){
echo '<li>'.$controller.'</li>';
}
echo '</ul>';
}
I realize I could read the /application/classes/controllers/ directory, but I'm hoping there's an easier way.
Thanks,
Getting a list of your controller files could be done with Kohana::list_files('classes/controller'). But as Michal already said, there isn't a 1:1 realtionships between controllers/actions and routes.
I'm afraid there is no Kohana::get_controllers() method that you can easily call to get a a sitemap of sorts. This is because controllers are called dynamically, i.e. based on the request's URL and Routes configuration Kohana's checking whether a controller (and action) exist and then call them. Kohana does not keep record of all available controllers and actions that can be accessible.
Neither traversing the /application/classes/controllers directory and getting the list of all files would give you the desired result, because there are not only actions to be read (which can be fairly easily done with Reflection class), but there are also Routes which you have to take into account.
As you can see this is potentially very complex issue and one that cannot be simply answered with a snippet of code that can be pasted here.
If you decide to write a script that would actually create such a map, but you stumble into a problem on the way, we would be able to more helpful then otherwise this question is too open. Also, if you were to write it, I suggest you create it as a module that you would be able to include in any other projects and share it.
Here my solution to get all the controllers and their actions. I use it to add permissions into our system https://github.com/open-classifieds/openclassifieds2/
/**
* get all the controllers and the actions that can be used
* #return array
*/
public static function list_controllers()
{
$list_controllers = array();
$controllers = Kohana::list_files('classes/controller');
foreach ($controllers as $controller)
{
$controller = basename($controller,'.php');
$list_controllers[] = $controller;
$class = new ReflectionClass('Controller_Panel_'.$controller);
$methods = $class->getMethods();
foreach ($methods as $obj => $val)
{
if (strpos( $val->name , 'action_') !== FALSE )
{
$list_controllers[$controller][] = str_replace('action_', '', $val->name);
}
}
}
return $list_controllers;
}
I have built a simple Notification system in my Cake app that I want to have a function that will create a new notification when I call a certain method. Because this is not something the user would actually access directly and is only database logic I have put it in the Notification model like so:
class Notification extends AppModel
{
public $name = 'Notification';
public function createNotification($userId, $content, $url)
{
$this->create();
$this->request->data['Notification']['user_id'] = $userId;
$this->request->data['Notification']['content'] = $content;
$this->request->data['Notification']['url'] = $url;
$result = $this->save($this->request->data);
if ($result)
{
$this->saveField('datetime', date('Y-m-d H:i:s'));
$this->saveField('status', 0);
}
}
}
And then whenever I want to create a notification within my app I just do:
$this->Notification->createNotification($userId,'Test','Test');
However this doesn't work! The controller is talking to the model fine, but it doesn't create the row in the database... I'm not sure why... but it would seem I'm doing this wrong by just doing all the code in the model and then calling it across the app.
Edit: Based on answers and comments below, I have tried the following the code to create a protected method in my notifications controller:
protected function _createNotification($userId, $content, $url)
{
$this->Notification->create();
$this->request->data['Notification']['user_id'] = $userId;
$this->request->data['Notification']['content'] = $content;
$this->request->data['Notification']['url'] = $url;
$result = $this->save($this->request->data);
if ($result)
{
$this->saveField('datetime', date('Y-m-d H:i:s'));
$this->saveField('status', 0);
}
}
Now the thing that is stumping me still (apologies if this is quite simple to others, but I have not used protected methods in CakePHP before) is how do I then call this from another controller? So for example If have a method in my PostsController and want to create a notification on successful save, how would I do this?
I thought about in my PostsController add method:
if($this->save($this->request-data){
$this->Notification->_createNotification($userId,'Test','Test');
}
But being protected I wouldn't be able to access the method from outside of the NotificationsController. Also I'm using the same syntax as if I was calling a function from a model so again it doesn't feel right.
Hopefully someone can help me out and get me back on track as this is a new area to me.
the controller should pass all data to the model
$this->createNotification($this->request->data);
the model then can use the data:
public function createNotification(array $data) {
$key = $data[$this->alias]['key'];
$data[...] = ...;
$this->create();
return $this->save($data);
}
you never ever try to access the controller (and/or its request object) from within a model.
you can also invoke the method from other models, of course:
public function otherModelsMethod() {
$this->Notification = ClassRegistry::init('Notification');
$data = array(
'Notification' => array(...)
);
$this->Notification->createNotification($data);
}
and you can make your methods verbose, but that usually makes it harder to read/understand/maintain with more and more arguments:
public function createNotification($userId, $content, $url) {
$data = array();
// assign the vars to $data
$data['user_id'] = $userId;
...
$this->create();
return $this->save($data);
}
so this is often not the cake way..
Methods in a model are not "publicly accessible" by definition. A user cannot call or invoke a method in a model. A user can only cause a controller action to be initiated, never anything in the model. If you don't call your model method from any controller, it's never going to be invoked. So forget about the "non-public" part of the question.
Your problem is that you're working in the model as if you were in a controller. There is no request object in a model. You just pass a data array into the model method and save that array. No need for $this->request. Just make a regular array(), put the data that was passed by the controller in there and save it.
The whole approach is totally wrong in the MVC context IMO and screams for the use of the CakePHP event system. Because what you want is in fact trigger some kind of event. Read http://book.cakephp.org/2.0/en/core-libraries/events.html
Trigger an Event and attach a global event listener that will listen for this kind of events and execute whatever it should do (save something to db) when an event happens. It's clean, flexible and extendible.
If you did a proper MVC stack for your app most, if not all, events aka notifications should be fired from within a model like when a post was saved successfully for example.
This is what I have ended up doing. While it certainly isn't glamorous. It works for what I want it to do and is a nice quick win as the notifications are only used in a few methods so I'm not creating a large amount of code that needs improving in the future.
First to create a notification I do the following:
$notificationContent = '<strong>'.$user['User']['username'].'</strong> has requested to be friends with you.';
$notificationUrl = Router::url(array('controller'=>'friends','action'=>'requests'));
$this->Notification->createNotification($friendId,$notificationContent,$notificationUrl);
Here I pass the content I want and the URL where the user can do something, in this case see the friend request they have been notified about. The url can be null if it's an information only notification.
The createNotification function is in the model only and looks like:
public function createNotification($userId, $content, $url = null)
{
$this->saveField('user_id',$userId);
$this->saveField('content',$content);
$this->saveField('url',$url);
$this->saveField('datetime', date('Y-m-d H:i:s'));
$this->saveField('status', 0);
}
This creates a new record in the table with the passed content, sets its status to 0 (which means unread) and the date it was created. The notification is then set as read when a user visits the notifications page.
Again this is most probably not an ideal solution to the problem outlined in this question... but it works and is easy to work with And may prove useful to others who are learning CakePHP who want to run functions from models when building prototype apps.
Remember nothing to stop you improving things in the future!
First of all, you can improve your last solution to do one save() (instead of 5) the following way:
public function createNotification($userId, $content, $url = null){
$data = array(
'user_id' => $userId,
'content' => $content,
'url' => $url,
'datetime' => date('Y-m-d H:i:s'),
'status' => 0
);
$this->create();
$this->save($data);
}
When I began programming CakePHP(1.3) more than a year ago I also had this problem.
(I wanted to use a function of a controller in any other controller.)
Because I didn't know/researched where to place code like this I've done it wrong for over a year in a very big project. Because the project is really really big I decided to leave it that way. This is what i do:
I add a function (without a view, underscored) to the app_controller.php:
class AppController extends Controller {
//........begin of controller..... skipped here
function _doSomething(){
//don't forget to load the used model
$this->loadModel('Notification');
//do ur magic (save or delete or find ;) )
$tadaaa = $this->Notification->find('first');
//return something
return $tadaaa;
}
}
This way you can access the function from your Notification controller and your Posts controller with:
$this->_doSomething();
I use this kind of functions to do things that have nothing to do with data submittance or reading, so i decided to keep them in the app_controller. In my project these functions are used to submit e-mails to users for example.. or post user actions to facebook from different controllers.
Hope I could make someone happy with this ;) but if you're planning to make a lot of these functions, it would be much better to place them in the model!