I want to have some form-fields encrypted when they are added/edited and decrypted when they are looked up by cake.
Here is the code that works for me in v2.7.2:
core.php
Configure::write('Security.key','secretkey');
app/model/patient.php.
public $encryptedFields = array('patient_surname', 'patient_first_name');
public function beforeSave($options = array()) {
foreach($this->encryptedFields as $fieldName){
if(!empty($this->data[$this->alias][$fieldName])){
$this->data[$this->alias][$fieldName] = Security::encrypt(
$this->data[$this->alias][$fieldName],
Configure::read('Security.key')
);
}
}
return true;
}
public function afterFind($results, $primary = false) {
foreach ($results as $key => $val) {
foreach($this->encryptedFields as $fieldName) {
if (#is_array($results[$key][$this->alias])) {
$results[$key][$this->alias][$fieldName] = Security::decrypt(
$results[$key][$this->alias][$fieldName],
Configure::read('Security.key')
);
}
}
}
return $results;
}
As I understand it I have to replace $this->data[] with the generated entities for the model and the afterFind method with virtual fields, but I just can't put it all together.
There's more than one way to solve this (please note that the following code is untested example code! You should get a grasp on the new basics first before using any of this).
A custom database type
One would be a custom database type, which would encrypt when binding the values to the database statement, and decrypt when results are being fetched. That's the option that I would prefer.
Here's simple example, assuming the db columns can hold binary data.
src/Database/Type/CryptedType.php
This should be rather self explantory, encrypt when casting to database, decrypt when casting to PHP.
<?php
namespace App\Database\Type;
use Cake\Database\Driver;
use Cake\Database\Type;
use Cake\Utility\Security;
class CryptedType extends Type
{
public function toDatabase($value, Driver $driver)
{
return Security::encrypt($value, Security::getSalt());
}
public function toPHP($value, Driver $driver)
{
if ($value === null) {
return null;
}
return Security::decrypt($value, Security::getSalt());
}
}
src/config/bootstrap.php
Register the custom type.
use Cake\Database\Type;
Type::map('crypted', 'App\Database\Type\CryptedType');
src/Model/Table/PatientsTable.php
Finally map the cryptable columns to the registered type, and that's it, from now on everything's being handled automatically.
// ...
use Cake\Database\Schema\Table as Schema;
class PatientsTable extends Table
{
// ...
protected function _initializeSchema(Schema $table)
{
$table->setColumnType('patient_surname', 'crypted');
$table->setColumnType('patient_first_name', 'crypted');
return $table;
}
// ...
}
See Cookbook > Database Access & ORM > Database Basics > Adding Custom Types
beforeSave and result formatters
A less dry and tighter coupled approach, and basically a port of your 2.x code, would be to use the beforeSave callback/event, and a result formatter. The result formatter could for example be attached in the beforeFind event/callback.
In beforeSave just set/get the values to/from the passed entity instance, you can utilize Entity::has(), Entity::get() and Entity::set(), or even use array access since entities implement ArrayAccess.
The result formatter is basically an after find hook, and you can use it to easily iterate over results, and modify them.
Here's a basic example, which shouldn't need much further explanation:
// ...
use Cake\Event\Event;
use Cake\ORM\Query;
class PatientsTable extends Table
{
// ...
public $encryptedFields = [
'patient_surname',
'patient_first_name'
];
public function beforeSave(Event $event, Entity $entity, \ArrayObject $options)
{
foreach($this->encryptedFields as $fieldName) {
if($entity->has($fieldName)) {
$entity->set(
$fieldName,
Security::encrypt($entity->get($fieldName), Security::getSalt())
);
}
}
return true;
}
public function beforeFind(Event $event, Query $query, \ArrayObject $options, boolean $primary)
{
$query->formatResults(
function ($results) {
/* #var $results \Cake\Datasource\ResultSetInterface|\Cake\Collection\CollectionInterface */
return $results->map(function ($row) {
/* #var $row array|\Cake\DataSource\EntityInterface */
foreach($this->encryptedFields as $fieldName) {
if(isset($row[$fieldName])) {
$row[$fieldName] = Security::decrypt($row[$fieldName], Security::getSalt());
}
}
return $row;
});
}
);
}
// ...
}
To decouple this a little, you could also move this into a behavior so that you can easily share it across multiple models.
See also
Cookbook > Database Access & ORM > Database Basics > Adding Custom Types
Cookbook > Database Access & ORM > Query Builder > Adding Calculated Fields
Cookbook > Tutorials & Examples > Bookmarker Tutorial Part 2 > Persisting the Tag String
Cookbook > Database Access & ORM > Behaviors
API > \Cake\Datasource\EntityTrait
API > \Cake\ORM\Table
Edit: #npm was right about the virtual properties not working. Now i'm angry at myself for giving a bad answer. serves me right for not checking it before I posted.
To make it right, I've implemented a version using behaviors to decrypt the fields as they are read, and encrypt them as they are written to the database.
Note: This code does not currently incorporate any custom finders, so it will not support searching by the encrypted field.
eg.
$this->Patient->findByPatientFirstname('bob'); // this will not work
Behavior
/src/Model/Behavior/EncryptBehavior.php
<?php
/**
*
*/
namespace Cake\ORM\Behavior;
use ArrayObject;
use Cake\Collection\Collection;
use Cake\Datasource\EntityInterface;
use Cake\Datasource\ResultSetInterface;
use Cake\Event\Event;
use Cake\ORM\Behavior;
use Cake\ORM\Entity;
use Cake\ORM\Query;
use Cake\ORM\Table;
use Cake\ORM\TableRegistry;
use Cake\Utility\Inflector;
use Cake\Utility\Security;
use Cake\Log\Log;
/**
* Encrypt Behavior
*/
class EncryptBehavior extends Behavior
{
/**
* Default config
*
* These are merged with user-provided configuration when the behavior is used.
*
* #var array
*/
protected $_defaultConfig = [
'key' => 'YOUR_KEY_KERE', /* set them in the EntityTable, not here */
'fields' => []
];
/**
* Before save listener.
* Transparently manages setting the lft and rght fields if the parent field is
* included in the parameters to be saved.
*
* #param \Cake\Event\Event $event The beforeSave event that was fired
* #param \Cake\ORM\Entity $entity the entity that is going to be saved
* #return void
* #throws \RuntimeException if the parent to set for the node is invalid
*/
public function beforeSave(Event $event, Entity $entity)
{
$isNew = $entity->isNew();
$config = $this->config();
$values = $entity->extract($config['fields'], true);
$fields = array_keys($values);
$securityKey = $config['key'];
foreach($fields as $field){
if( isset($values[$field]) && !empty($values[$field]) ){
$entity->set($field, Security::encrypt($values[$field], $securityKey));
}
}
}
/**
* Callback method that listens to the `beforeFind` event in the bound
* table. It modifies the passed query
*
* #param \Cake\Event\Event $event The beforeFind event that was fired.
* #param \Cake\ORM\Query $query Query
* #param \ArrayObject $options The options for the query
* #return void
*/
public function beforeFind(Event $event, Query $query, $options)
{
$query->formatResults(function ($results){
return $this->_rowMapper($results);
}, $query::PREPEND);
}
/**
* Modifies the results from a table find in order to merge the decrypted fields
* into the results.
*
* #param \Cake\Datasource\ResultSetInterface $results Results to map.
* #return \Cake\Collection\Collection
*/
protected function _rowMapper($results)
{
return $results->map(function ($row) {
if ($row === null) {
return $row;
}
$hydrated = !is_array($row);
$fields = $this->_config['fields'];
$key = $this->_config['key'];
foreach ($fields as $field) {
$row[$field] = Security::decrypt($row[$field], $key);
}
if ($hydrated) {
$row->clean();
}
return $row;
});
}
}
Table
/src/Model/Table/PatientsTable.php
<?php
namespace App\Model\Table;
use App\Model\Entity\Patient;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use Cake\Core\Configure;
/**
* Patients Model
*
*/
class PatientsTable extends Table
{
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->table('patients');
$this->displayField('id');
$this->primaryKey('id');
// will encrypt these fields automatically
$this->addBehavior('Encrypt',[
'key' => Configure::read('Security.key'),
'fields' => [
'patient_surname',
'patient_firstname'
]
]);
}
}
I feel your pain. the ORM layer in cakephp 3 is radically different from cake2. They split the entity model and the table ORM into two different classes, and afterFind has been removed. I would take a look at using virtual properties. I think it might be suitable for your use case.
Example below.
<?php
namespace App\Model\Entity;
use Cake\ORM\Entity;
use Cake\Utility\Security;
use Cake\Core\Configure;
class Patient extends Entity
{
protected function _setPatientSurname($str)
{
$this->set('patient_surname', Security::encrypt($str, Configure::read('Security.key'));
}
protected function _setPatientFirstname($str)
{
$this->set('patient_firstname', Security::encrypt($str, Configure::read('Security.key'));
}
protected function _getPatientSurname()
{
return Security::decrypt($this->patient_surname, Configure::read('Security.key'));
}
protected function _getPatientFirstname()
{
return Security::decrypt($this->patient_first_name, Configure::read('Security.key'));
}
}
Related
I am trying to add USE INDEX() to the query builder in Laravel. I tried to follow similar steps to link and was kind of successful but I cannot manage the last bit and I am not sure my ad-hoc code has created a huge backdoor.
The target: The target of my exercise is to add Index to the query builder like below:
DB::table('users')->where('id',1)->**useIndex**('users')->get()->first();
Here an option useIndex specifies the index that I am going to use for this query.
What I have done yet: Created a class named Connection in App/Override
<?php
namespace App\Override;
class Connection extends \Illuminate\Database\MySqlConnection {
//#Override
public function query() {
return new QueryBuilder(
$this,
$this->getQueryGrammar(),
$this->getPostProcessor()
);
}
}
Created a service provider named CustomDatabaseServiceProvider in App/Providers. Here I just manipulated registerConnectionServices function. I further commented Illuminate\Database\DatabaseServiceProvider::class, and added App\Providers\CustomDatabaseServiceProvider::class, to app.php in config directory.
<?php
namespace App\Providers;
use App\Override\Connection;
use Illuminate\Database\DatabaseManager;
use Illuminate\Database\Query\Grammars\Grammar;
use Illuminate\Database\Schema;
use Illuminate\Contracts\Queue\EntityResolver;
use Illuminate\Database\Connectors\ConnectionFactory;
use Illuminate\Database\Eloquent\Factory as EloquentFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\QueueEntityResolver;
use Illuminate\Support\ServiceProvider;
class CustomDatabaseServiceProvider extends ServiceProvider
{
/**
* The array of resolved Faker instances.
*
* #var array
*/
protected static $fakers = [];
/**
* Bootstrap the application events.
*
* #return void
*/
public function boot()
{
Model::setConnectionResolver($this->app['db']);
Model::setEventDispatcher($this->app['events']);
}
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
Model::clearBootedModels();
$this->registerConnectionServices();
$this->registerEloquentFactory();
$this->registerQueueableEntityResolver();
}
/**
* Register the primary database bindings.
*
* #return void
*/
protected function registerConnectionServices()
{
// The connection factory is used to create the actual connection instances on
// the database. We will inject the factory into the manager so that it may
// make the connections while they are actually needed and not of before.
$this->app->singleton('db.factory', function ($app) {
return new ConnectionFactory($app);
});
// The database manager is used to resolve various connections, since multiple
// connections might be managed. It also implements the connection resolver
// interface which may be used by other components requiring connections.
$this->app->singleton('db', function ($app) {
$dbm = new DatabaseManager($app, $app['db.factory']);
//Extend to include the custom connection (MySql in this example)
$dbm->extend('mysql', function ($config, $name) use ($app) {
//Create default connection from factory
$connection = $app['db.factory']->make($config, $name);
//Instantiate our connection with the default connection data
$new_connection = new Connection(
$connection->getPdo(),
$connection->getDatabaseName(),
$connection->getTablePrefix(),
$config
);
//Set the appropriate grammar object
// $new_connection->setQueryGrammar(new Grammar());
// $new_connection->setSchemaGrammar(new Schema\());
return $new_connection;
});
return $dbm;
});
$this->app->bind('db.connection', function ($app) {
return $app['db']->connection();
});
}
/**
* Register the Eloquent factory instance in the container.
*
* #return void
*/
protected function registerEloquentFactory()
{
$this->app->singleton(FakerGenerator::class, function ($app, $parameters) {
$locale = $parameters['locale'] ?? $app['config']->get('app.faker_locale', 'en_US');
if (!isset(static::$fakers[$locale])) {
static::$fakers[$locale] = FakerFactory::create($locale);
}
static::$fakers[$locale]->unique(true);
return static::$fakers[$locale];
});
$this->app->singleton(EloquentFactory::class, function ($app) {
return EloquentFactory::construct(
$app->make(FakerGenerator::class), $this->app->databasePath('factories')
);
});
}
/**
* Register the queueable entity resolver implementation.
*
* #return void
*/
protected function registerQueueableEntityResolver()
{
$this->app->singleton(EntityResolver::class, function () {
return new QueueEntityResolver;
});
}
}
and finally created a class named QueryBuilder in App/Override. this is the problematic class:
<?php
namespace App\Override;
use Illuminate\Support\Facades\Cache;
class QueryBuilder extends \Illuminate\Database\Query\Builder
{
private $Index = [];
public function useIndex($index = null)
{
$this->Index = $index;
return $this;
}
//#Override
public function get($columns = ['*'])
{
if ($this->Index) {
//Get the raw query string with the PDO bindings
$sql_str = str_replace('from `' . $this->from . '`', 'from `' . $this->from . '` USE INDEX (`' . $this->Index . '`) ', $this->toSql());
$sql_str = vsprintf($sql_str, $this->getBindings());
return parent::get($sql_str);
} else {
//Return default
return parent::get($columns);
}
}
}
The issues here are:
The output does not contain USE INDEX
Is it safe to use str_replace to manipulate query?
The query builder is macroable so in your service provider you can probably do:
Illuminate\Database\Query\Builder::macro(
'tableWithIndex',
function ($table, $index) {
$table = $this->grammar->wrapTable($table);
$index = $this->grammar->wrap($index);
return $this->fromRaw("$table USE INDEX ($index)");
}
);
Then you could use this:
DB::tableWithIndex('users', 'users');
within the macro $this would refer to the query builder instance
Note that I have them both in one because you can potentially have multiple from calls for the same query and it would be a mess trying to figure out what goes where
Maybe I'm searching it all wrong but I haven't been able to figure out an answer.. Say I have a model Building, which always has n Floor(s)
I would like to write a constructor for Building, in which I could specify a number of Floor(s) to be created. The problem is that I can't link back a Floor to the Building because when the constructor for Building is being called, it doesn't have a primary key yet...
Basically, my code looks like this but doesn't work:
class Building extends Model {
public function __construct($nbFloors) {
for($i=0; $i<$nbFloors; $i++) {
$foo = new Floor();
$foo->building_id = $this->id;
$foo->save();
}
}
}
What would be the correct solution to achieve something like that?
The primary key will never be available in the constructor and your constructor's definition is not compatible with Model which expects an array of attributes as the first argument.
You're performing too much logic in your constructor, a constructor is meant to just instantiate an object and its dependencies, not perform business logic. By doing this in your constructor, you're actually going to be attempting to create new floors EVERY time your Model is instantiated which includes when your model is retrieved from the database.
I'd recommend adding a new method like:
public function createWithFloors($n) {
$this->save();
...
}
Now, you can use the model as it's expected and call the create method:
$building = new Building(['name' => 'Empire State']);
$building->createWithFloors(102);
Besides the solutions already suggested, you could create an event that is fired when a Building is created. A listener could then store your Floors. For event reference, have a look at the documentation.
First, create an event called BuildingCreated with php artisan make:event BuildingCreated and use below code:
namespace App\Events;
use App\Building;
use Illuminate\Queue\SerializesModels;
class BuildingCreated extends Event
{
use SerializesModels;
public $building;
public function __construct(Building $building)
{
$this->building = $building;
}
}
Then, register the event within your Building model:
use App\Events\BuildingCreated;
class Building
{
protected $dispatchesEvents = [
'created' => BuildingCreated::class,
];
}
Next, you will need a listener that creates the floors. Create it with php artisan make:listener AddFloorsToNewBuilding and adapt it as you need:
namespace App\Listeners;
use App\Building;
use App\Events\BuildingCreated;
class AddFloorsToNewBuilding
{
public function handle(BuildingCreated $event)
{
$floors = ...;
$event->building->floors()->saveMany($floors);
$event->building->save();
}
}
Lastly, have the listener listen for the event by adding it to the $listen array in the EventServiceProvider:
class EventServiceProvider
{
protected $listen = [
\App\Events\BuildingCreated::class => [
\App\Listeners\AddFloorsToNewBuilding::class,
],
];
}
since, you can't bind Floor to a building that is not created yet, you should make the "new floors number" an attribute of the Building instance. Then you overload the save method to create the new floors.
class Building extends Model {
/** number of floors to be created on save
* #var int
*/
private $newFloorsCount;
/**
* Building constructor.
* #param array $attributes
* #param int $nbFloors
*/
public function __construct(array $attributes = [], $nbFloors = 0) {
parent::__construct($attributes);
$this->newFloorsCount = $nbFloors;
}
/**
* #param array $options
* #return bool
*/
public function save(array $options = [])
{
$return = parent::save($options);
for($i=0; $i<$this->newFloorsCount; $i++) {
$foo = new Floor();
$foo->building_id = $this->id;
$foo->save();
}
return $return;
}
}
now you can just do
$building = new Building([],5);
$building->save();
I'm developing an application where my data comes from external server in JSON format.
I would like to set a relationships between each models, but without using a database table.
Is it possible ?
Something like that:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Flight extends Model
{
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'https://.../server/flights.json';
}
You could make a service class which handles the request and returns class instances:
namespace App\Services;
class FlightService
{
/**
* #var FlightFactory
*/
private $flightFactory;
public function __construct(FlightFactory $flightFactory)
{
$this->flightFactory = $flightFactory;
}
public function getAllFlights()
{
$flightsJson = $this->getFromExternalCurl();
return $this->flightFactory->buildFlightList($flightsJson);
}
private function getFromExternalCurl()
{
return Curl::to('http://www.foo.com/flights.json')
->withData( array( 'foz' => 'baz' ) )
->asJson()
->get();
}
}
Basically the service would make the external API call and the response is passed to a factory which creates the instances.
Note that you just need to add the factory in the construct and it's binded because laravel uses https://laravel.com/docs/5.4/container
namespace App\Factories;
class FlightFactory
{
public function buildFlightList($flightJsonList)
{
$flightCollection = collect();
foreach($flightJsonList as $flightJson) {
$flightCollection->push($this->buildFlight($flightJson));
}
return $flightCollection;
}
public function buildFlight($flightJson)
{
$flight = new Flight();
// add properties
return $flight;
}
}
The factory will return a Collection which is verry usefull because it contains usefull methods, or you can return an array.
In this example I used a curl library https://github.com/ixudra/curl but it can be replaced with native php or other libraries.
Then you can use by injecting the FlightService in your controllers.
P.S: Code not tested but represents a possible approach
Within Laravel you can easily define abilities and then hook into them later on a user request regarding to do different actions:
$gate->define('update-post', function ($user, $post) {
return $user->id === $post->user_id;
});
But almost all my defined abilities has this part $user->id === $model->user_id in it. I don't like it as it's a kind of repeating a condition over and over which I think could be more abstract.
Most of my defined abilities are according to updating/deleting records, so it would be better if I could make a global condition applied to all of them or if there could be a group ability defining which is like to what we do in routing.
Is there any workaround for it? I really like it DRY.
Everything in Laravel is extendable, that's the power of its service providers.
You can extend the Gate object to a MyCustomGate object and do whatever you want in that object. Here's an example:
MyCustomGate.php
class MyCustomGate extends \Illuminate\Auth\Access\Gate
{
protected $hasOwnershipVerification = [];
/**
* Define a new ability.
*
* #param string $ability
* #param callable|string $callback
* #return $this
*
* #throws \InvalidArgumentException
*/
public function defineWithOwnership($ability, $callback, $foreignUserIdKey = "user_id")
{
// We will add this
$this->hasOwnershipVerification[$ability] = $foreignUserIdKey;
return $this->define($ability, $callback);
}
/**
* Resolve and call the appropriate authorization callback.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param string $ability
* #param array $arguments
* #return bool
*/
protected function callAuthCallback($user, $ability, array $arguments)
{
$callback = $this->resolveAuthCallback(
$user, $ability, $arguments
);
// We will assume that the model is ALWAYS the first key
$model = is_array($arguments) ? $arguments[0] : $arguments;
return $this->checkDirectOwnership($ability, $user, $model) && call_user_func_array(
$callback, array_merge([$user], $arguments)
);
}
/**
* Check if the user owns a model.
*
* #param string $ability
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param \Illuminate\Database\Eloquent\Model $model
* #return bool
*/
protected function checkDirectOwnership($ability, $user, $model)
{
if(!isset($this->hasOwnershipVerification[$ability])) {
return true
}
$userIdKey = $this->hasOwnershipVerification[$ability];
// getAuthIdentifier() is just ->id, but it's better in case the pk of a user is different that id
return $user->getAuthIdentifier() == $model->{$userIdKey};
}
}
Then, you will have to tell Laravel to use your gate instead of the default one. You ca do that in your AuthServiceProvider (assuming that it's extending Illuminate\Auth\AuthServiceProvider, just add the following method.
AuthServiceProvider
/**
* Register the access gate service.
*
* #return void
*/
protected function registerAccessGate()
{
$this->app->singleton(\Illuminate\Contracts\Auth\Access\Gate::class, function ($app) {
return new MyCustomGate($app, function () use ($app) {
return $app['auth']->user();
});
});
}
And this way, you can define abilities using defineWithOwnership() method instead of define(). You can still use define() for abilities that don't require ownership verification. There's a third parameter defineWithOwnership() accepts which is $foreignUserIdKey; that's used for the case when a model has a different field for the user id.
Note: I wrote the code on the fly and did not try it, it may have errors, but you get the idea.
I checked your question quite a bit, but I've found no "easy" way to do it.
Instead, what I would probably do is this:
<?php
namespace App\Policies;
use App\User;
use App\Post;
trait CheckOwnership {
protected function checkOwnership($user, $model) {
$owned = $user->id === $model->user_id;
if ($owned === false)
throw new NotOwnedException;
}
}
class PostPolicy
{
use CheckOwnership;
public function update(User $user, Post $post)
{
try {
$this->checkOwnership($user, $post);
//continue other checks
} catch (NotOwnedException $ex) {
return false;
}
}
}
Add this function to your AuthServiceProvider
public function defineAbilities(array $abilities, $gate)
{
foreach($abilities as $name => $model){
$gate->define($name, function ($user, $model){
return $user->id === ${$model}->user_id;
});
}
}
and then inside boot method
$this->defineAbilities(['ability1' => 'model1', 'ability2' => 'model2'], $gate);
You can define another function and call it within the anonymous function. This will allow you to have commonly-used code in one central location while still allowing any resource-specific logic.
Add this function to your AuthServiceProvider class:
public function userCheck(User $user, $target)
{
// do the user id check
$result = isset($target->user_id) && isset($user) && $user->id === $target->user_id;
return $result;
}
Your code, modified:
$gate->define('update-post', function ($user, $post) {
// call the function
$result = $this->userCheck($user, $post);
// do some kind of 'update-post' specific check
return $result/* && some_bool_statement*/;
});
I think you can use middlewares.
Simply make a admin middleware and use it in your routes and routes group.
And there is no security bug on your project (delete, create & ... actions) because Laravel has csrf token!
You can use before() function, also.
And then an important note:
if you don't define a correspond function on Policy class and call it $this->authorize($post) on a controller an unauthorized Action error will be thrown unless before()methodreturnstrue.
for example call $this->authorize on Dashboard\PostsController:
public function edit($id)
{
$post = Post::find($id)->first();
$this->authorize($post);
return view('dashboard.post')->with(compact('post'));
}
and if we defined a PostPolicy Class:
class PostPolicy
{
use HandlesAuthorization;
public function before($user, $ability)
{
return $user->is_admin;
}
}
If user be admin he/she can edit post because we returned true in before() method despite of have not a method with same name (as edit method in PostsController).
In fact Laravel will check for before method mthod on Policy Class. if before return'snull will check for correspond method with same name on controller method and if this method not found user cannot perform action.
Thank you laravel for DRY us!♥
I am having trouble with global scopes, especially the removal of the scope.
In my User model, i have a ActivatedUsersTrait, that introduces a global scope to only query for Users with the column "activated" set to true (The User is "activated" after email verification).
So far everything works fine, when i query for User::all(), i only get Users with activated=true.
My problem now is, how to include the non-activated Users into my query, like SoftDeletingTrait does via withTrashed()? This is only relevant in my ActivationController, where i need to get the User, set activated=true and save them back to db.
I've created a withInactive() method in my ActiveUsersTrait, based on the method i found in SoftDeletingTrait, but when i run a query on User::withInactive->get(), the non-activated Users won't show up in the results.
Here's my ActiveUsersTrait:
use PB\Scopes\ActiveUsersScope;
trait ActiveUsersTrait {
public static function bootActiveUsersTrait()
{
static::addGlobalScope(new ActiveUsersScope);
}
public static function withInactive()
{
// dd(new static);
return (new static)->newQueryWithoutScope(new ActiveUsersScope);
}
public function getActivatedColumn()
{
return 'activated';
}
public function getQualifiedActivatedColumn()
{
return $this->getTable().'.'.$this->getActivatedColumn();
}
}
and my ActiveUsersScope:
use Illuminate\Database\Eloquent\ScopeInterface;
use Illuminate\Database\Eloquent\Builder;
class ActiveUsersScope implements ScopeInterface {
public function apply(Builder $builder)
{
$model = $builder->getModel();
$builder->where($model->getQualifiedActivatedColumn(), true);
}
public function remove(Builder $builder)
{
$column = $builder->getModel()->getQualifiedActivatedColumn();
$query = $builder->getQuery();
foreach ((array) $query->wheres as $key => $where)
{
if ($this->isActiveUsersConstraint($where, $column))
{
unset($query->wheres[$key]);
$query->wheres = array_values($query->wheres);
}
}
}
protected function isActiveUsersConstraint(array $where, $column)
{
return $where['type'] == 'Basic' && $where['column'] == $column;
}
}
Any help is highly appreciated!
Thanks in advance! -Joseph
Eloquent queries now have a removeGlobalScopes() method.
See: https://laravel.com/docs/5.3/eloquent#query-scopes (under the Removing Global Scopes subheading).
From the docs:
// Remove one scope
User::withoutGlobalScope(AgeScope::class)->get();
// Remove all of the global scopes...
User::withoutGlobalScopes()->get();
// Remove some of the global scopes...
User::withoutGlobalScopes([
FirstScope::class, SecondScope::class
])->get();
The SoftDeletingTrait where cleanup is simpler because it doesn't involve any bindings (it's a "Null" where, not a "Basic" where). The issue you're encountering is that the binding for [ n => true ] is still there, even when you manually remove the where.
I'm thinking about making a PR because I encountered the same issue myself, and there isn't a great way to keep track of which wheres and which bindings go together.
If you are only using a simple query, you can keep track of the index of the binding more or less like so:
use Illuminate\Database\Eloquent\ScopeInterface;
use Illuminate\Database\Eloquent\Builder;
class ActiveUsersScope implements ScopeInterface {
/**
* The index in which we added a where clause
* #var int
*/
private $where_index;
/**
* The index in which we added a where binding
* #var int
*/
private $binding_index;
/**
* Apply the scope to a given Eloquent query builder.
*
* #param \Illuminate\Database\Eloquent\Builder $builder
* #return void
*/
public function apply(Builder $builder)
{
$model = $builder->getModel();
$builder->where($model->getQualifiedActivatedColumn(), true);
$this->where_index = count($query->wheres) - 1;
$this->binding_index = count($query->getRawBindings()['where']) - 1;
}
/**
* Remove the scope from the given Eloquent query builder.
*
* #param \Illuminate\Database\Eloquent\Builder $builder
* #return void
*/
public function remove(Builder $builder)
{
$query = $builder->getQuery();
unset($query->wheres[$this->where_index]);
$where_bindings = $query->getRawBindings()['where'];
unset($where_bindings[$this->binding_index]);
$query->setBindings(array_values($where_bindings));
$query->wheres = array_values($query->wheres);
}
}
Note how we're storing the indices where the where clause and bindings were added, rather than looping through and checking if we found the right one. This almost feels like a better design—we added the where clause and binding, so we should know where it is without having to loop through all where clauses. Of course, it will all go haywire if something else (like ::withTrashed) is also messing with the where array. Unfortunately, the where bindings and where clauses are just flat arrays, so we can't exactly listen on changes to them. A more object-oriented approach with better automatic management of the dependency between clauses and their binding(s) would be preferred.
Obviously this approach could benefit from some prettier code and validation that array keys exists, etc. But this should get you started. Since the global scopes aren't singletons (they get applied whenever newQuery() is invoked) this approach should be valid without that extra validation.
Hope this helps under the heading of "good enough for now"!
Just found this after having the same problem and I have a more eloquent solution. Simply replace your "remove" method with this.
/**
* Remove the scope from the given Eloquent query builder.
*
* #param \Illuminate\Database\Eloquent\Builder $builder
* #return void
*/
public function remove(Builder $builder)
{
$query = $builder->getQuery();
$column = $builder->getModel()->getQualifiedActivatedColumn();
foreach ((array) $query->wheres as $key => $where)
{
if ($this->isActiveUsersConstraint($where, $column))
{
// Here SoftDeletingScope simply removes the where
// but since we use Basic where (not Null type)
// we need to get rid of the binding as well
$this->removeWhere($query, $key);
$this->removeBinding($query, $key);
}
}
}
/**
* Remove scope constraint from the query.
*
* #param \Illuminate\Database\Eloquent\Builder $builder
* #param int $key
* #return void
*/
protected function removeWhere($query, $key)
{
unset($query->wheres[$key]);
$query->wheres = array_values($query->wheres);
}
/**
* Remove scope constraint from the query.
*
* #param \Illuminate\Database\Eloquent\Builder $builder
* #param int $key
* #return void
*/
protected function removeBinding($query, $key)
{
$bindings = $query->getRawBindings()['where'];
unset($bindings[$key]);
$query->setBindings(array_values($bindings));
}
Look at the newQuey function (Eloquent/Model.php Lvl 4.2):
newQuery{
//'initialized'
$b = $this->newQueryWithoutScopes(); //Get a 'clean' query. Note the plural.
//and applies scopes
return $this->applyGlobalScopes($b); //now builder is 'dirty'
}
So, this suggests a solution:
function someButNotAllScopes(){
$b = $this->newQueryWithoutScopes();
$unwanted = new MyUnwantedScope();
//get all scopes, but skip the one(s) you dont want
foreach($this->getGlobalScopes as $s){
if ($s instanceof $unwanted){continue;}
$s->apply($b, $this)
}
return $b;
}
You can also do something clever with scopes. Make em implement and OnOffInterface with an 'applyMe' method. This method can 'turn' on/off the apply method of a scope. In the function above you can get the unswanted scope and 'turn' if off:
$scope = $this->getGlobalScope(new Unwanted());
$scope->applyme(false); //will turn off the apply method
return $this->newQuery(); //now each scope checks if it is 'off'
Simply, you can apply without bindings like so:
$builder->where($column, new \Illuminate\Database\Query\Expression(1));
or
$builder->where($column, \DB::raw(1));