OctoberCms record finder with dynamic scope - php

I'm using OctoberCMS based on Laravel and trying to get a list of products within a form via Record Finder.
The use-case is that the record-finder must show available products based on dynamic condition.
I tried to achieve this via "Scope" option of record finder for related form model but not finding a way to pass the dynamic value to the scope.
Sample Code --
class A extends Model
{
public $belongsTo = [
'product' => [
'Plugin\Models\B',
'key' => 'id',
'scope' => 'specificProduct'
],
];
}
class B extends Model
{
public function scopeSpecificProduct($query , $product_type)
{
return $query->where('product_type', $product_type);
}
}
Here $product_type is the dynamic value which I am trying to pass via record finder and get in scope.
Can anyone suggest that is this a correct way for such requirement or how should I achieve this ?

In your fields definition you have to use the scope attribute
fields:
products:
label: Products
type: recordfinder
scope: specificProduct
With this, the second param of your scope will be the A model that is creating or updating
class B extends Model
{
public function scopeSpecificProduct($query , $model)
{
return $query->where('product_type', $model->depend_attribute);
}
}

Related

Laravel factory state method is only run once for multiple models

Working with Laravel 9, I have a state method in my factory defined similar to this:
class PostFactory extends Factory
{
public function definition(): array
{
return [
"user" => User::factory(),
"title" => $this->faker->bs(),
"content" => $this->faker->text(1000),
];
}
public function tagged(): self
{
return $this->state(["tag" => $this->faker->word()]);
}
}
I would like to generate some users who have some tagged posts. But when I try to generate models with this syntax:
$users = User::factory()
->has(Post::factory()->tagged()->count(5), "posts")
->count(5)
->create();
The state method PostFactory::tagged() is only called once (confirmed with debug output in the method) and all 25 posts (5 for each user) have the same tag. I would like each post to have a unique tag. Is there a straightforward way to do this without manually changing them after creation?

Laravel 8 Multiple Relationships for Factory

In Laravel 8 it is possible to quickly fill relationships with factories. However, I cannot figure out how to generate more than one relationship. How can I create a random or new relationship for each link using the new Laravel 8 syntax?
This factory syntax is only available in Laravel 8.
https://laravel.com/docs/8.x/database-testing#factory-relationships
Problem
Consider the following relationship:
Each link belongs to a website and a post.
Both websites and posts can have many links.
<?php
class Post extends Model
{
use HasFactory;
function links()
{
return $this->hasMany(Link::class);
}
}
class Website extends Model
{
use HasFactory;
function links()
{
return $this->hasMany(Link::class);
}
}
class Link extends Model
{
use HasFactory;
function post()
{
return $this->belongsTo(Post::class);
}
function website()
{
return $this->belongsTo(Website::class);
}
}
What I tried/want
What I tried below will only generate one model for all the links. How can I create a random or new relationship for each link using the new Laravel 8 syntax?
Link::factory()->count(3)->forPost()->forWebsite()->make()
=> Illuminate\Database\Eloquent\Collection {#4354
all: [
App\Models\Link {#4366
post_id: 1,
website_id: 1,
},
App\Models\Link {#4395
post_id: 1, // return a different ID
website_id: 1,
},
App\Models\Link {#4370
post_id: 1, // return a different ID
website_id: 1, // return a different ID
},
],
}
Just add this to your LinkFactory:
public function definition()
{
return [
'post_id' => function () {
return Post::factory()->create()->id;
},
.....
];
}
And now you can create new Post for each new Link:
Link::factory()->count(3)->create();//Create 3 links with 3 new posts
or attach new Links to existing Post:
Link::factory()->count(3)->create(['post_id' => Post::first()->id]); //create 3 links and 0 new posts
In Laravel 9, you can use this macro:
// database/factoryMacros.php
<?php
namespace Database\Support;
use Illuminate\Database\Eloquent\Factories\BelongsToRelationship;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
/** #param Factory|Model $factory */
Factory::macro('hasParent', function (mixed $factory, string $relationship = null): self {
return $this
->state(function () use ($factory, $relationship): array {
$belongsTo = new BelongsToRelationship(
factory: $factory,
relationship: $relationship ?? guessBelongsToMethodName($factory),
);
return $belongsTo
->recycle(recycle: $this->recycle)
->attributesFor(model: $this->newModel());
});
});
Factory::macro('hasChildren', fn (...$arguments): self => $this->has(...$arguments));
Factory::macro('hasChild', fn (...$arguments): self => $this->has(...$arguments));
/** #param Factory|Model $factory */
function guessBelongsToMethodName(mixed $factory): string
{
$modelName = is_subclass_of($factory, Factory::class)
? $factory->modelName()
: $factory::class;
return Str::camel(class_basename($modelName));
}
Usage
Use the method hasParent($factory) instead of for($factory):
// Creates 3 Link, 3 Post, 3 Website
Link::factory()
->count(3)
->hasParent(Post::factory())
->hasParent(Website::factory())
->make();
You can also use hasChildren($factory) or hasChild($factory) instead of has for name consistency:
// Creates 3 Post, 3 Link
Post::factory()
->count(3)
->hasChild(Link::factory())
->make();
The syntax of the macros is the same as for and has.
You can explicitly define the relationship name, pass complex factory chains, pass a concrete model, and use it with recycle, for example.
Installation
Add the file to your composer.json:
{
...
"autoload": {
"files": [
"database/factoryMacros.php"
]
}
}
Run a composer dump-autoload to reload the composer file.
Alternatively, you can register the macro as a service or load it as a mixin.
PS: I intend to create a library for this in the future.
Tests
/**
* Use "DatabaseEloquentFactoryTest.php" as base:
* https://github.com/laravel/framework/blob/de42f9987e01bfde50ea4a86becc237d9c8c5c03/tests/Database/DatabaseEloquentFactoryTest.php
*/
class FactoryMacrosTest extends TestCase
{
function test_belongs_to_relationship()
{
$posts = FactoryTestPostFactory::times(3)
->hasParent(FactoryTestUserFactory::new(['name' => 'Taylor Otwell']), 'user')
->create();
$this->assertCount(3, $posts->filter(function ($post) {
return $post->user->name === 'Taylor Otwell';
}));
$this->assertCount(3, FactoryTestUser::all());
$this->assertCount(3, FactoryTestPost::all());
}
}
TL;DR;
In Laravel 9, it is not possible to achieve this. The for() uses a single model for all instances.
There's a PR to fix this behavior, but the PR was closed, and I'm not sure it will ever be implemented:
https://github.com/laravel/framework/pull/44279
The laravel magic factory method for allows you to populate the database with one record from the foreign table. See link to documentation https://laravel.com/docs/8.x/database-testing#belongs-to-relationships
In your case, using forPost() and forWebsite() will allow you to populate the database with one id from the Post table and the Website table.
If you want to use different IDs use this syntax instead
Link::factory()->count(3)->make()
Had a similar problem and was only able to get it working when I attached within the afterCreating() on a single factory. This allows me to create/store the id of each model and then attach to the Link model
I'm choosing to start with WebsiteFactory but you can also start with PostFactory since those are the "highest parent" models. If you try to make a Link without the website_id and the post_id I believe you will get a error asking for both.
class WebsiteFactory extends Factory
{
public function definition(){...}
public function configure()
{
return $this->afterCreating( function (Website $website){
// the website model is created, hence after-creating
// attach Website to a new Post
$post = Post::factory()->hasAttached($website)->create();
// create new links to attach to both
$links = Link::factory()->for($website)->for($post)->count(3)->create();
});
You can leave PostFactory and LinkFactory as simple definition blocks (or add other stuff if you wanted). Now when you create a new Website via factory, it will create a new post and 3 new links. For example, you can now run
php artisan tinker
$w = Website::factory()->create(); // one website-one post-3 links
$ws = Website::factory()->count(5)->create(); // 5 website-5 post- 15 links
Check out the Factory Callbacks here (9.x docs, but they are in 8.x too):
https://laravel.com/docs/9.x/database-testing#factory-callbacks
\App\Models\Category::factory(10)
->has(Product::factory()->count(10), 'products')
->create();
It would be better if you play around with code. You will understand better.
$user = User::factory()
->has(Post::factory()->count(3), 'posts')
->create();
The above code will create three post for a single user. It will insert three post row and a user row. On the other hand the code below, seems three post will be inserted for user with name Jessica Aercher, that is it won't insert a user.
$posts = Post::factory()
->count(3)
->for(User::factory()->state([
'name' => 'Jessica Archer',
]))
->create();

Yii2 REST ActiveRecord deep relation not working

We've been coding an API service with Yii2 and created all model classes extending from ActiveRecord.
Next we started adding the simple relations. This is where things got strange.
We have 3 tables (limited to explain the problem) 'app_customers', 'lu_postcodes' and 'lu_countries'.
A customer has one postcode_id.
A postcode has one country_id.
In the customer model we would like to add relationships to get the postcode and country data when getting customer data.
Customer model:
namespace api\modules\v1\models;
use Yii;
use \api\common\components\BaseModel;
/**
*
*/
class Customer extends BaseModel {
public function extraFields() {
return ['postcode'];
}
public function getPostcode() {
return $this->hasOne(Postcode::className(), ['id' => 'postcode_id'])
->with(['country']);
}
....[more code]
Postcode model:
namespace api\modules\v1\models;
use Yii;
use \api\common\components\BaseModel;
/**
*
*/
class Postcode extends BaseModel {
public function extraFields() {
return ['country'];
}
public function getCountry() {
return $this->hasOne(Country::className(), ['id' => 'country_id']);
}
....[more code]
So when calling v1/customer?expand=postcode it returns all customer data and postcode is populated with a postcode object. But we can't get the country data to load together with the postcode data.
In an ideal situation we would like the index and view actions from customer to include both postcode and country data. (did not try yet .. one step at a time :) )
Trying to debug this issue We dumped the sql from Customer::getPostcode
with var_dump($q->createCommand()->sql); and got the output:
'SELECT * FROM `lu_postcodes` WHERE `id`=:qp0'
This might have something to do with missing tablenames?
In earlier attempt I managed to get the country data loaded, but it used the ID from customer which resulted in a wrong country obviously.
Any ideas? Thanks in advance!
I did some research and found https://github.com/yiisoft/yii2/issues/6844#issuecomment-131482508, which looked promising, but once implemented it still remained the same result.
----------------------- EDIT BELOW
I tried all options and in most cases I get a JSON parse error from Yii. In one case I get a result like
...
"display_name": "Rxxxxxxx xxxxxxx",
"postcode": {
"id": 361,
"country_id": 20,
"state_id": 2,
"zip": "3945",
"city": "Ham",
"alpha": "ham",
"longitude": "5.1730329000",
"latitude": "51.0966039000"
},
"country": null
...
Used option: Adding country in extraFields() in Customer.
public function extraFields() {
return ['postcode', 'country' => function($model) { return $model->postcode->country; }];
}
Yii2 itself does not directly support nested relations . It is not good idea to build such complex objects in a REST API using ActiveRecord classes. Remember you will likely have a collection API i.e. a group of customer , each customer will need multiple subqueries to satisfy the request.
There are multiple ways to address this.
Use separate controller to solve this /customer/<id>/country. This is good solution if country/ postcode is a hasMany relationship
Define country in the default fields() functions of postcode instead of extraFields(). This way when you pass expand?=postcode, both postcode and country will always show in the expanded output
Use ad-hoc expansion function in your extraFields() definition of Customer Class with something like this
.
public function extraFields()
{
return [
'postcode',
'country'=>function($model){
return $model->postcode->country;
}
];
}
Define a country function in your Customer class using Via Relation, this is more useful when you have hasMany relationships
.
public function getCountry()
{
return $this->hasOne(Country::className(), ['id' => 'country_id'])
->via('postCode');
}
Define a custom query with the exact parameters you desire and use a dataProvider as the response to your Index function something like this
..
public function actionIndex()
{
$query = (new \yii\db\Query())
// This can also be a ActiveQuery
// using Customer::find() with addSelects() etc
// ... additional query conditions
return new ActiveDataProvider(['query'=>$query]);
}
Similarly
public function actionView($id)
{
$query = (new \yii\db\Query()) // simlar to above
$query->andWhere(['customer.id'=>(int)$id]); // or similar condition
return $query->one();
}
I got it working.
It was an encoding issue...
'charset' => 'UTF-8', in the db config array did the trick.

Can I populate a collection of Eloquent models from a static array?

I'm using Laravel 5.1 and its Eloquent model with mysql.
But I would like to populate a collection of models fetching a static array and then querying it as normal.
So let's say I have my Photo model and I would query it as usual:
App\Photo::where('published', 1)->get();
But the Photo model should fetch not a DB table but an array like this:
$photos = [
[ "id" => 1, "published" => 1 ],
[ "id" => 2, "published" => 1 ],
[ "id" => 2, "published" => 0 ],
]
My problem is to link this array to that model before querying it.
Any ideas?
Don`t know why you want this but if I were you I would put every query method in a service class:
namespace App\Services;
class PhotoService implements PhotoServiceInterface
{
public function getAllPhotos() {
return App\Photo::$photos;
}
//your static array defined in the class as an associative array, you should use **try** **catch** for this or define a default if the key does not exist
public static function getPhotoById($id) {
return App\Photo::$photos[$id];
}
//other functions
public function getAllAlbums() {
return App\Albums::all();
}
}
This way if you want to put them in a table or other changes occur in your app you just have to change this class.
Problem:
If you have a model that has a relation to this model you would have to use the service to get it (getPhotoById($user->getPhotoId())) rather than for ex $user->getPhoto().
Or you can:
class User
{
....
public function getPhoto() {
return App\Services\PhotoService::getPhotoById($this->photo_id);
}
....
}
Using the lists functionality will return an array of the columns that you specify in the query.
App\Photo::where('published', 1)->lists('id','published');
Hence you can query the model what ever way you want the have it return an array of the fields placed inside lists functionality

There are <select> populated with (key->value), and field "key" in a model. How I can display it's "value" in a view, instead of "key"?

In model:
public function getOptionsGender()
{
array(0=>'Any', 1=>Male', 2=>'Female');
}
In view (edit):
echo $form->dropDownList($model, 'gender', $model->optionsGender);
but I have a CDetailView with "raw" attributes, and it displays numbers instead of genders.
$attributes = array(
...
'gender',
)
What is appropriate way to convert these numbers back to genders? Should I do it in a model, replacing fields such as $this->gender = getOptionsGender($this->gender)? Any github examples will be very appreciated.
I had to choose gender, age, city, country etc. in a few views that are not related to this one. Where should I place my getOptionsGender function definitions?
Thank for your help, the problem is solved.
In model:
public function getGenderOptions() { ... }
public function genderText($key)
{
$options = $this->getGenderOptions();
return $options[$key];
}
In view:
$attributes = array(
array (
'name'=>'gender',
'type'=>'raw',
'value'=>$model->genderText($model->gender), //or $this->genderText(...)
),
);
$this->widget('zii.widgets.CDetailView', array(
'data'=>$model,
'attributes'=>$attributes,
));
The working example can be found here:
https://github.com/cdcchen/e23passport/blob/c64f50f9395185001d8dd60285b0798098049720/protected/controllers/UserController.php
In Jeffery Winsett's book "Agile Web Application Development with Yii 1.1", he deals with the issue using class constants in the model you are using. In your case:
class Model extends CActiveRecord
{
const GENDER_ANY=0;
const GENDER_MALE=1;
const GENDER_FEMALE=2;
public function getGenderOptions(){
return array(
self::GENDER_ANY=>'Any',
self::GENDER_MALE=>'Male',
self::GENDER_FEMALE=>'Female',
);
}
public function getGenderText(){
$genderOptions=$this->genderOptions();
return isset($genderOptions[$this->gender]) ? $genderOptions[$this->gender] : "unkown gender({$this->gender})";
}
}
Then in your CDetailView you would have to alter it from gender to:
array(
'name'=>'gender',
'value'=>CHtml::encode($model->genderText()),
),
If several models have the same data, you may want to create a base model that extends CActiveRecord and then extend the new model instead of CActiveRecord. If this model is the only one with that data (ie User model only has gender), but other views use that model to display data, then I would leave it just in the single model class. Also keep in mind that if you place getGenderOptions in the extended class, and you extend ALL your models, they will all have that option available, but may not have the attributes needed and will throw an error if you aren't checking for it.
All this being said, I still think it is a matter or preference. You can handle it however you want, wherever you want. This is just one example from a book I have specifically on Yii.

Categories