YII2: custom sorting in search model - sorting

Please, help me with such a problem:
1) I have default search model of Users.
2) I need a list of users. And first in this list always must be user with login 'admin', and second - with login 'finance', and then all others sorted by id.
My method in UserController
public function actionUsersList() {
$searchModel = new UserSearch();
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
return $this->renderPartial('users-list', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}
As I understood I have to change params of search in this line, to add sort conditions
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
But how exactly can I do this?

You can do that by adding following code to your action:
$dataProvider->sort->attributes['id'] = [
'asc' => [
new \yii\db\Expression("FIELD(login, 'finance', 'admin') DESC"),
'id' => SORT_ASC,
],
'desc' => [
new \yii\db\Expression("FIELD(login, 'finance', 'admin') DESC"),
'id' => SORT_DESC,
],
'label' => $searchModel->getAttributeLabel('id'),
];
$dataProvider->sort->defaultOrder = ['id' => SORT_ASC];
The field function returns the position of first parameter among other parameters or 0 if the value is not present among them. So for 'admin' it will return 2, for 'finance' 1 and for others 0. If you order DESC by that you will get the required order.
Other option is to add this definitions for sort into the search method of UserSearch model as suggested in mahsaa's answer. It depenends if you want to use this sorting in different actions.

In UserSearch class, add sort to ActiveDataProvider:
$dataProvider = new ActiveDataProvider([
'query' => $query,
'sort' => [
'defaultOrder' => [
'login' => SORT_ASC,
'id' => SORT_DESC,
],
]]);
It first sorts by login and then id.

Related

Common Rule for Multiple Input Fields pattern in Laravel

I want to create a common rule for multiple Inpute fields with the name pattern "name_*".
e.g.
I have multiple input fields with the following names:
name
name_fruit
name_animal
name_people
name_region
name_language
description
keywords
image
And I want fields with pattern name, name_* should be 'required|min:3|max:120' Validator.
Currently, I am using the following rule but it wonly works for the first input field name
$rules = array(
'name*' => 'required|min:3|max:120',
);
Is there any way using asterisk or regex or other method through which I can?
You're better off just manually returning a rule array with every single one of those keys. It's easier to read.
public function rules(): array
{
return [
'name' => 'required|min:3|max:120',
'name_fruit' => 'required|min:3|max:120',
'name_animal' => 'required|min:3|max:120',
'name_people' => 'required|min:3|max:120',
'name_region' => 'required|min:3|max:120',
'name_language' => 'required|min:3|max:120',
'description' => '...',
'keywords' => '...',
'image' => '...',
];
}
Of course, that doesn't mean you can't do what you want. But there is no built in wildcard like that. You'd have to write some extra logic.
public function rules(): array
{
$name_fields = ['name', 'name_fruit', 'name_animal', 'name_people', 'name_region', 'name_language'];
$rules = [];
foreach ($name_fields as $name_field) {
$rules[$name_field] = 'required|min:3|max:120';
}
return $rules + [
'description' => '...',
'keywords' => '...',
'image' => '...',
];
}
Another option is to use array validation. For this to work, you'll need to change the name attribute of your inputs.
From name to name_fields[name]
From name_fruit to name_fields[fruit]
From name_animal to name_fields[animal]
From name_people to name_fields[people]
From name_region to name_fields[region]
From name_language to name_fields[language]
public function rules(): array
{
return [
'name_fields' => 'required|array',
'name_fields.*' => 'required|min:3|max:120',
'description' => '...',
'keywords' => '...',
'image' => '...',
];
}
But this also means there's nothing validating against someone passing in an unkonwn field like say.... name[company] to give an example.

Stripe & Laravel how to upgrade or downgrade session subscription?

I have some issues using the Laravel Cashier for creating subscriptions.
First, from my backend I am creating a Package, which calls the following two Strip functions:
public function createStripeProduct(array $data)
{
$product = $this->stripe->products->create([
'name' => $data['title']." ".appName(),
]);
return $product->id;
}
public function createStripePrice(array $data)
{
$price = $this->stripe->prices->create([
'unit_amount' => $data['price'] * $this->multiple,
'currency' => $this->currency,
'recurring' => ['interval' => 'month'],
'product' => $data['stripe_prod_id'],
]);
return $price->id;
}
Then in my Controller, I am creating the session:
public function create(Request $request)
{
$key = config('services.stripe.secret');
$stripe = new Stripe\StripeClient($key);
$stripeCustomer = $user->createOrGetStripeCustomer();
$checkout_session = $stripe->checkout->sessions->create([
'customer' => $stripeCustomer['id'],
'success_url' => route('frontend.user.account'),
'cancel_url' => route('frontend.user.account'),
'payment_method_types' => ['card'],
'line_items' => [
[
'price' => $request->stripe_price_id,
'quantity' => 1,
],
],
'mode' => 'subscription',
'allow_promotion_codes' => true,
]);
return $checkout_session['id'];
}
Everything is working so far, but with the implementation, I can subscribe one use multiple times to the same or to a different Package.
How can I prevent this from happening and also how to implement a future upgrade/downgrade of the Package?
To answer your two questions:
1) I can subscribe one use multiple times to the same or to a different Package. How can I prevent this from happening
Your code is fetching a Stripe Customer object in createOrGetStripeCustomer(). You can list all Subscriptions on the Customer with https://stripe.com/docs/api/subscriptions/list#list_subscriptions-customer and then check if you want to create an additional CheckoutSession Subscription on that Customer.
2) how to implement a future upgrade/downgrade of the Package?
You would use the code snippets here: https://stripe.com/docs/billing/subscriptions/upgrade-downgrade#changing where you update the Subscription's SubscriptionItem with a new Price ID.
$sub = \Stripe\Subscription::update('sub_123', [
'cancel_at_period_end' => false,
'proration_behavior' => 'create_prorations',
'items' => [
[
'id' => $subscription->items->data[0]->id,
'price' => 'price_456', // the new Price to update to
],
],
]);

ListEntries in table for relationship on show page - backpack for laravel

Just new with backpack. I search on official site and googled it, but dit not found an answer
In laravel 7, using Backpack 4.1
My data model is : Customer has many addresses
Relationship is configured in the Customer model :
public function addresses()
{
return $this->hasMany(\App\Models\Address::class, 'user_id');
}
Relationship is configured in the Address model :
public function customer()
{
return $this->belongsTo(\App\Models\Customer::class);
}
public function country()
{
return $this->belongsTo(\App\Models\Country::class);
}
public function address_type()
{
return $this->belongsTo(\App\Models\AddressType::class);
}
In my customer show page, I would like to show all customer addresses in a table, just under the customer details.
So in my CustomerCrudController, I have implemented this method :
protected function setupShowOperation()
{
$this->crud->set('show.setFromDb', false);
$this->crud->addColumn(['name' => 'name', 'type' => 'text', 'label' => __('models/customers.fields.name'), ]);
$this->crud->addColumn(['name' => 'email', 'type' => 'email', 'label' => __('models/customers.fields.email'), ]);
$this->crud->addColumn([
'name' => 'addresses',
'label' => __('models/addresses.plural'),
'type' => 'table',
'columns' => [
'address_type_id' => __('models/addresses.fields.address_type'),
'address_type.name' => __('models/addresses.fields.address_type'),
'address1' => __('models/addresses.fields.address1'),
'address2' => __('models/addresses.fields.address2'),
'city' => __('models/addresses.fields.address2'),
'postal_code' => __('models/addresses.fields.address2'),
'country.name' => __('models/countries.singular'),
],
]);
}
When I go on my page : /admin/customer/3/show,
In my debugbar, I saw the query how load addresses
select * from `addresses` where `addresses`.`user_id` = 3 and `addresses`.`user_id` is not null
I have the table rendered with the corresponding number of lines from data in DB, but rows are blank.
Is this the correct way to do that ? What are the correct parameters ?
Is there a way to show a table with action buttons (show entry, edit) - same as in List view ?
Should it be implemented in another way ?
Hope I'm clear.
Thanks
Don't know if it is a laravel bug, but my solution was to create my own table blade, base on the file :
\vendor\backpack\crud\src\resources\views\crud\columns\table.blade.php
and have created my own :
\resources\views\vendor\backpack\crud\columns\address_table.blade.php
I have juste changed the file:40
#elseif( is_object($tableRow) && property_exists($tableRow, $tableColumnKey) )
to
#elseif( is_object($tableRow) && isset($tableRow->{$tableColumnKey}) )
now, in my CustomerCrudController.php :
protected function setupShowOperation()
{
$this->crud->set('show.setFromDb', false);
$this->crud->addColumn(['name' => 'name', 'type' => 'text', 'label' => __('models/customers.fields.name'),]);
$this->crud->addColumn(['name' => 'email', 'type' => 'email', 'label' => __('models/customers.fields.email'),]);
$this->crud->addColumn([
'name' => 'addresses',
'label' => __('models/addresses.plural'),
'type' => 'address_table', // my custom type
'model' => \App\Models\Address::class,
'entity' => 'addresses',
'columns' => [
'address_type_name' => __('models/addresses.fields.address_type'),
'postal_code' => __('models/addresses.fields.postal_code'),
'city' => __('models/addresses.fields.city'),
'address1' => __('models/addresses.fields.address1'),
'address2' => __('models/addresses.fields.address1'),
],
]);
}
And I've added an accessor in my model (Address.php)
public function getAddressTypeNameAttribute()
{
return "{$this->address_type->name}";
}
Don't know if there is a better way ...
Hope this will help others.
I use Laravel 8,
In addition for the answer above, and based on this answer https://stackoverflow.com/a/65072393 and https://stackoverflow.com/a/43011286/1315632 regarding PHP function property_exists vs Laravel magic methods to create dynamic properties and methods
After creating the overwrite column php artisan backpack:publish crud/columns/table
I change line 40 in file:\resources\views\vendor\backpack\crud\columns\table.blade.php into
#elseif( is_object($tableRow) && ( property_exists($tableRow, $tableColumnKey) || property_exists((object)$tableRow->toArray(), $tableColumnKey) ) )
adding OR checking from answer https://stackoverflow.com/a/65072393

Map array values to collection of items

How would one do the following elegantly with laravel collections ?
Map the values of the $baseMap as keys to the collection.
The baseMap :
$baseMap = [
'name' => 'new_name',
'year' => 'new_year',
];
The collection :
$items = collect([
[
'name' => 'name1',
'year' => '1000',
'not_in_basemap' => 'foo'
],
[
'name' => 'name2',
'year' => '2000',
'not_in_basemap' => 'foo'
],
//...
]);
The end result :
$result =[
[
'new_name' => 'name1',
'new_year' => '1000',
],
[
'new_name'=> 'name2',
'new_year' => '2000',
],
];
I know how to do it in plain php , just wondering what a nice collection version would be. Thanks!
I tried to find collection methods, or php functions, but without success. Some dirty code that works with different keys from both sides (items and basemap).
$result = $items->map(function($item) use ($baseMap) {
$array = [];
foreach($baseMap as $oldKey => $newKey){
if(isset($item[$oldKey])){
$array[$newKey] = $item[$oldKey];
}
}
return $array;
});
$result = $result->toArray();
Thanks to #vivek_23 and #IndianCoding for giving me idea's I ended up with the following :
I made a small edit to make sure the mapping and the items keys lined up.
so you don't have to worry of misalignment and all in laravel collection !
$baseMap = collect($baseMap)->sortKeys();
$result = $items->map(function ($item) use ($baseMap) {
return $baseMap->values()
->combine(
collect($item)->sortKeys()->intersectByKeys($baseMap)
)
->all();
});
Use intersectByKeys to filter your baseMap keys with $items values.
$result = $items->map(function($item,$key) use ($baseMap){
return array_combine(array_values($baseMap),collect($item)->intersectByKeys($baseMap)->all());
});
dd($result);
Update:
In a pure collection way,
$baseMapCollect = collect($baseMap);
$result = $items->map(function($item,$key) use ($baseMapCollect){
return $baseMapCollect->values()->combine(collect($item)->intersectByKeys($baseMapCollect->all())->values())->all();
});
dd($result);
Here are my two cents, using map. Don't know how dynamic your collection should be, but knowing the keys I would do the following:
$baseMap = [
'name' => 'new_name',
'year' => 'new_year',
];
$items = collect([
[
'name' => 'name1',
'year' => '1000',
'not_in_basemap' => 'foo'
],
[
'name' => 'name2',
'year' => '2000',
'not_in_basemap' => 'foo'
],
])->map(function($item, $key) use ($baseMap) {
return [
$baseMap['name'] => $item['name'],
$baseMap['year'] => $item['year']
];
});

How to enable and disable sort in Yii2 GridView?

How to enable and disable sort in Yii2 GridView ?
You can customize columns sort in your DataProvider. For example if you use ActiveDataProvider in your GridView you can indicate sort-able columns like below:
$dataProvider = new ActiveDataProvider([
'query' => Model::find(),
'sort' => ['attributes' => ['column1','column2']]
]);
In above example, only column1 and column2 are sort-able.
You can also disable sorting for all columns like below:
'sort' =>false
It is suggested to take a look at Yii2's official document : Class yii\data\Sort As it defines it:
Sort represents information relevant to sorting.
When data needs to be sorted according to one or several attributes, we can use Sort to represent the sorting information and generate appropriate hyperlinks that can lead to sort actions.
In addition to Ali's answer, for aggregated and related columns you could do the following:
public function actionIndex()
{
$dataProvider = new ActiveDataProvider([
'query' => User::find()->joinWith('role'),
'sort' => ['attributes' => [
//Normal columns
'username',
'email',
//aggregated columns
'full_name' => [
'asc' => ['first_name' => SORT_ASC, 'last_name' => SORT_ASC],
'desc' => ['first_name' => SORT_DESC, 'last_name' => SORT_DESC],
'default' => SORT_DESC
],
//related columns
'role.name' => [
'asc' => ['user_role.name' => SORT_ASC],
'desc' => ['user_role.name' => SORT_DESC],
'default' => SORT_DESC
],
],],
]);
}
Source: http://www.yiiframework.com/doc-2.0/yii-data-sort.html
If you want disable sorting from gridview for particular column then do like this:
[
'attribute' => 'name',
'enableSorting' => false
],
by using 'enableSorting' => false
You can disable sort in controller like this:
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
$dataProvider->sort->sortParam = false;

Resources