Yii2 Sort multiple parameters URL - yii

I have an endpoint in yii2 where I need to sort by multiple parameters. Im using ActiveDataProvider. This is the enpdoint:
public function actionIndex($client)
{
$sort = new Sort([
'attributes'=> ['name','mail','crdate']
]);
$query = Customer::find()
->andWhere(['client' => $client])
->orderBy($sort->orders);
$provider = new ActiveDataProvider(['query' => $query]);
return $provider->getModels();
}
Now sorting by a single parameter as in: customers?client=1&sort=mail works fine.
I want to sort by multiple parameters though. How should I do this in the url?

Set
$sort = new Sort([
'attributes'=> ['name','mail','crdate'],
'enableMultiSort' => true,
]);
Now you can pass multiple sort column by separating them with , like
?sort=name,mail
If you prepend name with - you get descending order otherwise it's ascending.
If you want to change , separator to something else set it in separator configuration key for Sort object.

You should try search model for Costumer model
public function search($params)
{
$query = Costumer::find();
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
$this->load($params);
if (!$this->validate()) {
return $dataProvider;
}
$query->andFilterWhere([some condition for search]);
return $dataProvider;
}

Related

Laravel query with multiple 'with' statements

I'm not sure if there's a a way of doing this but I would like to add a where clause to the second with in my query but it doesn't work. It just returns all the votes as if the condition wasn't there. Any help would be appreciated.
public function PostId($request)
{
$post_id = $request->post_id;
$user_id = auth('api')->user();
$post = Post::with('categories')
->where('id', $post_id)
->with('votes')
->where('user_id', $user_id->id)
->first();
return $post;
}
You need to use closure in your with statement.
Also, I'd recommend using findOrFail() instead of where conditional for your query. Therefore, in case you pass a wrong post_id in your request an exception 404 will be thrown.
A nicer way to accomplish what you want could be:
public function PostId($request)
{
$post = Post::with(['categories', 'votes' => function($query){
$query->where('user_id', auth('api')->user()->id);
})
->findOrFail($request->post_id);
return $post;
}
$post = Post::find($post_id) // Find method will return only first record. no need to call ->first() explicitly.
->with([
'categories',
'votes'
])
For the ->where('user_id', $user_id->id), you dont need to do it in here as you already have defined the relation 'votes'.
Class Post
{
public function votes()
{
return $this->hasMany(Vote::class)->where('user_id', $this->user_id); // You can have the where condition here assuming you have user id field present in the Post model. Else you can keep it as below in your query
}
}
With user id in the runtime query
$post = Post::find($post_id)
->with([
'categories',
'votes' => function($query) use($user_id) {
$query->where('user_id', $user_id->id);
}
])

Yii1 - sanitize GET parameter in the before action

I am trying to find if it possible to use the beforeAction in the controller to access the injected parameter.
For example, every action in the controller accepts type parameter, which I need to sanitize:
public function actionGetCustomPaymentsChunk($type) {
$type = TextUtil::sanitizeString($type);
// Get filter data
$filter = FilterHelper::getFilterData();
// Initialize components
$totalCostPayableComponent = new TotalCostPayableComponent($filter);
// Get chunk data
$data = $totalCostPayableComponent->getCustomPaymentChunk($type);
// Return content to client side
$this->renderPartial('/partials/_payable_cost_chunk', array(
'data' => $data,
'route' => 'totalCostPayable/getCustomPaymentsGrid',
'type' => $type,
'paymentType' => 'Custom',
));
}
}
Is this possible to do (I am trying to avoid repetition)?
You should be able to, what did you try?
Assuming the $type is passed via GET, you can modify it in a beforeAction and the modified value will be applied to the target action with a request like
http://myhost.com/route/test?type=something
using the below, $type = "foo" in any action in this controller.
protected function beforeAction($action)
{
if (isset($_GET['type'])) {
$_GET['type'] = 'foo';
}
...
return parent::beforeAction($action);
}
public function actionTest($type)
{
# $type === 'foo'
...
}
Change the manipulation in beforeAction to satisfy whatever your requirements are.

Is my order complete return approach correct?

When a customer is returned to the following URL (example);
http://prestashop.dev/index.php?action=completed&controller=callback&fc=module&hmac={valid-hmac}&merchant_order_id=14&module=chippin
After a successful payment, It will call on this FrontController sub-class;
class ChippinCallbackModuleFrontController extends ModuleFrontController
{
public function postProcess()
{
$chippin = new Chippin();
$payment_response = new PaymentResponse();
$payment_response->getPostData();
// if a valid response from gateway
if(ChippinValidator::isValidHmac($payment_response)) {
// "action" is passed as a param in the URL. don't worry, the Hmac can tell if it's valid or not.
if ($payment_response->getAction() === "completed") {
// payment_response->getMerchantOrderId() will just return the id_order from the orders table
$order_id = Order::getOrderByCartId((int) ($payment_response->getMerchantOrderId()));
$order = new Order($order_id);
// this will update the order status for the benefit of the merchant.
$order->setCurrentState(Configuration::get('CP_OS_PAYMENT_COMPLETED'));
// assign variables to smarty (copied this from another gateway, don't really understand smarty)
$this->context->smarty->assign(
array(
'order' => $order->reference,
)
);
// display this template
$this->setTemplate('confirmation.tpl');
I'm quite new to Prestashop. I'm just not sure if this is technically done or not. The confirmation.tlp view does display with the order->reference and the order status is updated to "Completed" but is this all I need?
Are there any other considerations? I have the opportunity to call a hookDisplayPaymentReturn at this point but why should I?
I seem to have a pretty standard return page. Is this enough;
Update - Do I just call a hook something like;
public function displayPaymentReturn()
{
$params = $this->displayHook();
if ($params && is_array($params)) {
return Hook::exec('displayPaymentReturn', $params, (int) $this->module->id);
}
return false;
}
As far as I can see everything seems okay for me.
You should consider adding hookDisplayPaymentReturn it allows other modules to add code to your confirmation page. For example a Google module could add javascript code that sends order informations to analytics on confirmation page.
EDIT
class ChippinCallbackModuleFrontController extends ModuleFrontController
{
public function postProcess()
{
$chippin = new Chippin();
$payment_response = new PaymentResponse();
$payment_response->getPostData();
// if a valid response from gateway
if(ChippinValidator::isValidHmac($payment_response)) {
// "action" is passed as a param in the URL. don't worry, the Hmac can tell if it's valid or not.
if ($payment_response->getAction() === "completed") {
// payment_response->getMerchantOrderId() will just return the id_order from the orders table
$order_id = Order::getOrderByCartId((int) ($payment_response->getMerchantOrderId()));
$order = new Order($order_id);
// this will update the order status for the benefit of the merchant.
$order->setCurrentState(Configuration::get('CP_OS_PAYMENT_COMPLETED'));
// assign variables to smarty (copied this from another gateway, don't really understand smarty)
$this->context->smarty->assign(
array(
'order' => $order->reference,
'hookDisplayPaymentReturn' => Hook::exec('displayPaymentReturn', $params, (int) $this->module->id);
)
);
$cart = $this->context->cart;
$customer = new Customer($cart->id_customer);
Tools::redirect('index.php?controller=order-confirmation&id_cart='.$cart->id.'&id_module='.$this->module->id.'&id_order='.$order->id.'&key='.$customer->secure_key);
And in your module :
class myPaymentModule extends PaymentModule
{
public function install()
{
if (!parent::install() || !$this->registerHook('paymentReturn'))
return false;
return true;
}
// Example taken from bankwire module
public function hookPaymentReturn($params)
{
$state = $params['objOrder']->getCurrentState();
$this->smarty->assign(array(
'total_to_pay' => Tools::displayPrice($params['total_to_pay'], $params['currencyObj'], false),
'bankwireDetails' => Tools::nl2br($this->details),
'bankwireAddress' => Tools::nl2br($this->address),
'bankwireOwner' => $this->owner,
'status' => 'ok',
'id_order' => $params['objOrder']->id
));
if (isset($params['objOrder']->reference) && !empty($params['objOrder']->reference))
$this->smarty->assign('reference', $params['objOrder']->reference);
return $this->display(__FILE__, 'confirmation.tpl');
}
}

Yii2 iterate dataprovider with relations

I need to know how to iterate through a Yii2 dataprovider with relations.
I have a model Asset that has a relationship to another model Make.
class Equipment extends \yii\db\ActiveRecord
{
// ...
public function getMake() {
return $this->hasOne(Make::className(), ['make_id' => 'make_id']);
}
}
In my controller, I have 2 functions, one to render a grid, and another to export the data to a CSV file.
public function actionEquipment()
{
$searchModel = new EquipmentSearch();
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
// Store the search model in session
Yii::$app->session->set('exportEquipmentModel', $searchModel);;
// Render grid
return $this->render('equipment', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}
public function actionExportequipment()
{
header('Content-type: text/csv');
header('Content-Disposition: attachment; filename="equipment_report-' . date('YmdHi') .'.csv"');
$searchModel = new EquipmentSearch();
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
// Use the search model from session, or get all
if(\Yii::$app->session->get('exportEquipmentModel')) {
$searchModel = Yii::$app->session->get('exportEquipmentModel');
$dataProvider = $searchModel->search(false);
$dataProvider->setPagination(false);
}
// csv header
$columns =[
'serial_number',
'Make',
'Model',
];
echo implode(",", $columns) . " \r\n";
// csv data
foreach ($dataProvider->getModels() as $data) {
$row =[
$data['serial_number'],
// TODO: I need to resolve the relation here
$data['make_id'] // Works
// $data->make_id // Works
// $data->make->description // Does not work
// $data['make_id']['description'], // Does not work
$data['model_id']
];
echo implode(",", $row) . " \r\n";
}
}
As can be see from the comments in the code, various forms of getting the make->description field is not yielding results.
Lesson learned : Always check for errors.
My code was correct. However, the data was not what I expected. The dba deleted some of the makes and models after setting off foreign key checks. Therefore, $data->make.
I therefore changed the line
$data->make->description
to
!empty($data->make)?$data->make->description:'Not Set',

Authentication with 2 different tables

I need to create a new "auth" config with another table and users. I have one table for the "admin" users and another table for the normal users.
But how can I create another instance of Auth with a different configuration?
While trying to solve this problem myself, I found a much simpler way. I basically created a custom ServiceProvider to replace the default Auth one, which serves as a factory class for Auth, and allows you to have multiple instances for multiple login types. I also stuck it all in a package which can be found here: https://github.com/ollieread/multiauth
It's pretty easy to use really, just replace the AuthServiceProvider in app/config/app.php with Ollieread\Multiauth\MultiauthServiceProvider, then change app/config/auth.php to look something like this:
return array(
'multi' => array(
'account' => array(
'driver' => 'eloquent',
'model' => 'Account'
),
'user' => array(
'driver' => 'database',
'table' => 'users'
)
),
'reminder' => array(
'email' => 'emails.auth.reminder',
'table' => 'password_reminders',
'expire' => 60,
),
);
Now you can just use Auth the same way as before, but with one slight difference:
Auth::account()->attempt(array(
'email' => $attributes['email'],
'password' => $attributes['password'],
));
Auth::user()->attempt(array(
'email' => $attributes['email'],
'password' => $attributes['password'],
));
Auth::account()->check();
Auth::user()->check();
It also allows you to be logged in as multiple user types simultaneously which was a requirement for a project I was working on. Hope it helps someone other than me.
UPDATE - 27/02/2014
For those of you that are just coming across this answer, I've just recently added support for reminders, which can be accessed in the same factory style way.
You can "emulate" a new Auth class.
Laravel Auth component is basically the Illuminate\Auth\Guard class, and this class have some dependencies.
So, basically you have to create a new Guard class and some facades...
<?php
use Illuminate\Auth\Guard as AuthGuard;
class CilentGuard extends AuthGuard
{
public function getName()
{
return 'login_' . md5('ClientAuth');
}
public function getRecallerName()
{
return 'remember_' . md5('ClientAuth');
}
}
... add a ServiceProvider to initialize this class, passing it's dependencies.
<?php
use Illuminate\Support\ServiceProvider;
use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Hashing\BcryptHasher;
use Illuminate\Auth\Reminders\PasswordBroker;
use Illuminate\Auth\Reminders\DatabaseReminderRepository;
use ClientGuard;
use ClientAuth;
class ClientServiceProvider extends ServiceProvider
{
public function register()
{
$this->registerAuth();
$this->registerReminders();
}
protected function registerAuth()
{
$this->registerClientCrypt();
$this->registerClientProvider();
$this->registerClientGuard();
}
protected function registerClientCrypt()
{
$this->app['client.auth.crypt'] = $this->app->share(function($app)
{
return new BcryptHasher;
});
}
protected function registerClientProvider()
{
$this->app['client.auth.provider'] = $this->app->share(function($app)
{
return new EloquentUserProvider(
$app['client.auth.crypt'],
'Client'
);
});
}
protected function registerClientGuard()
{
$this->app['client.auth'] = $this->app->share(function($app)
{
$guard = new Guard(
$app['client.auth.provider'],
$app['session.store']
);
$guard->setCookieJar($app['cookie']);
return $guard;
});
}
protected function registerReminders()
{
# DatabaseReminderRepository
$this->registerReminderDatabaseRepository();
# PasswordBroker
$this->app['client.reminder'] = $this->app->share(function($app)
{
return new PasswordBroker(
$app['client.reminder.repository'],
$app['client.auth.provider'],
$app['redirect'],
$app['mailer'],
'emails.client.reminder' // email template for the reminder
);
});
}
protected function registerReminderDatabaseRepository()
{
$this->app['client.reminder.repository'] = $this->app->share(function($app)
{
$connection = $app['db']->connection();
$table = 'client_reminders';
$key = $app['config']['app.key'];
return new DatabaseReminderRepository($connection, $table, $key);
});
}
public function provides()
{
return array(
'client.auth',
'client.auth.provider',
'client.auth.crypt',
'client.reminder.repository',
'client.reminder',
);
}
}
In this Service Provider, I put some example of how to create a 'new' password reminder component to.
Now you need to create two new facades, one for authentication and one for password reminders.
<?php
use Illuminate\Support\Facades\Facade;
class ClientAuth extends Facade
{
protected static function getFacadeAccessor()
{
return 'client.auth';
}
}
and...
<?php
use Illuminate\Support\Facades\Facade;
class ClientPassword extends Facade
{
protected static function getFacadeAccessor()
{
return 'client.reminder';
}
}
Of course, for password reminders, you need to create the table in database, in order to work. In this example, the table name should be client_reminders, as you can see in the registerReminderDatabaseRepository method in the Service Provider. The table structure is the same as the original reminders table.
After that, you can use your ClientAuth the same way you use the Auth class. And the same thing for ClientPassword with the Password class.
ClientAuth::gust();
ClientAuth::attempt(array('email' => $email, 'password' => $password));
ClientPassword::remind($credentials);
Don't forget to add your service provider to the service providers list in the app/config/app.php file.
UPDATE:
If you are using Laravel 4.1, the PasswordBroker doesn't need the Redirect class anymore.
return new PasswordBroker(
$app['client.reminder.repository'],
$app['client.auth.provider'],
$app['mailer'],
'emails.client.reminder' // email template for the reminder
);
UPDATE 2
Laravel 5.2 just introduced multi auth, so this is no longer needed in this version.
Ok, I had the same problem and here is how I solved it:
actually in laravel 4 you can simply change the auth configs at runtime so to do the trick you can simply do the following in your App::before filter:
if ($request->is('admin*'))
{
Config::set('auth.model', 'Admin');
}
this will make the Auth component to use th Admin model when in admin urls. but this will lead to a new problem, because the login session key is the same if you have two users in your admins and users table with the same id you will be able to login to the admin site if you have logged in before as a regular user! so to make the two different authetications completely independent I did this trick:
class AdminGuard extends Guard
{
public function getName()
{
return 'admin_login_'.md5(get_class($this));
}
public function getRecallerName()
{
return 'admin_remember_'.md5(get_class($this));
}
}
Auth::extend('eloquent.admin', function()
{
return new AdminGuard(new EloquentUserProvider(new BcryptHasher, 'Admin'), App::make('session.store'));
});
and change the App::before code to:
if ($request->is('admin*'))
{
Config::set('auth.driver', 'eloquent.admin');
Config::set('auth.model', 'Admin');
}
you can see that I made a new auth driver and rewrote some methods on the Guard class so it will generate different session keys for admin site. then I changed the driver for the admin site. good luck.
I had the same problem yesterday, and I ended up creating a much simpler solution.
My requirements where 2 different tables in two different databases. One table was for admins, the other was for normal users. Also, each table had its own way of hashing. I ended up with the following (Code also available as a gist on Github: https://gist.github.com/Xethron/6790029)
Create a new UserProvider. I called mine MultiUserProvider.php
<?php
// app/libraries/MultiUserProvider.php
use Illuminate\Auth\UserProviderInterface,
Illuminate\Auth\UserInterface,
Illuminate\Auth\GenericUser;
class MultiUserProvider implements UserProviderInterface {
protected $providers;
public function __construct() {
// This should be moved to the config later...
// This is a list of providers that can be used, including
// their user model, hasher class, and hasher options...
$this->providers = array(
'joomla' => array(
'model' => 'JoomlaUser',
'hasher' => 'JoomlaHasher',
)
'another' => array(
'model' => 'AnotherUser',
'hasher' => 'AnotherHasher',
'options' => array(
'username' => 'empolyee_number',
'salt' => 'salt',
)
),
);
}
/**
* Retrieve a user by their unique identifier.
*
* #param mixed $identifier
* #return \Illuminate\Auth\UserInterface|null
*/
public function retrieveById($identifier)
{
// Returns the current provider from the session.
// Should throw an error if there is none...
$provider = Session::get('user.provider');
$user = $this->createModel($this->providers[$provider]['model'])->newQuery()->find($identifier);
if ($user){
$user->provider = $provider;
}
return $user;
}
/**
* Retrieve a user by the given credentials.
*
* #param array $credentials
* #return \Illuminate\Auth\UserInterface|null
*/
public function retrieveByCredentials(array $credentials)
{
// First we will add each credential element to the query as a where clause.
// Then we can execute the query and, if we found a user, return it in a
// Eloquent User "model" that will be utilized by the Guard instances.
// Retrieve the provider from the $credentials array.
// Should throw an error if there is none...
$provider = $credentials['provider'];
$query = $this->createModel($this->providers[$provider]['model'])->newQuery();
foreach ($credentials as $key => $value)
{
if ( ! str_contains($key, 'password') && ! str_contains($key, 'provider'))
$query->where($key, $value);
}
$user = $query->first();
if ($user){
Session::put('user.provider', $provider);
$user->provider = $provider;
}
return $user;
}
/**
* Validate a user against the given credentials.
*
* #param \Illuminate\Auth\UserInterface $user
* #param array $credentials
* #return bool
*/
public function validateCredentials(UserInterface $user, array $credentials)
{
$plain = $credentials['password'];
// Retrieve the provider from the $credentials array.
// Should throw an error if there is none...
$provider = $credentials['provider'];
$options = array();
if (isset($this->providers[$provider]['options'])){
foreach ($this->providers[$provider]['options'] as $key => $value) {
$options[$key] = $user->$value;
}
}
return $this->createModel($this->providers[$provider]['hasher'])
->check($plain, $user->getAuthPassword(), $options);
}
/**
* Create a new instance of a class.
*
* #param string $name Name of the class
* #return Class
*/
public function createModel($name)
{
$class = '\\'.ltrim($name, '\\');
return new $class;
}
}
Then, I told Laravel about my UserProvider by adding the following lines to the top of my app/start/global.php file.
// app/start/global.php
// Add the following few lines to your global.php file
Auth::extend('multi', function($app) {
$provider = new \MultiUserProvider();
return new \Illuminate\Auth\Guard($provider, $app['session']);
});
And then, I told Laravel to use my user provider instead of EloquentUserProvider in app/config/auth.php
'driver' => 'multi',
Now, when I authenticate, I do it like so:
Auth::attempt(array(
'email' => $email,
'password' => $password,
'provider'=>'joomla'
)
)
The class would then use the joomlaUser model, with the joomlaHasher, and no options for the hasher... If using 'another' provider, it will include options for the hasher.
This class was built for what I required but can easily be changed to suite your needs.
PS: Make sure the autoloader can find MultiUserProvider, else it won't work.
I'm using Laravel 5 native auth to handle multiple user tables...
It's not difficult, please check this Gist:
https://gist.github.com/danielcoimbra/64b779b4d9e522bc3373
UPDATE: For Laravel 5, if you need a more robust solution, try this package:
https://github.com/sboo/multiauth
Daniel