Add new attribute dynamically to the existing model object in Yii2 framework - yii2

In Yii2 framework is it possible to add a new attribute dynamically to an existing object, which is retrieved from Database?
Example
//Retrieve from $result
$result = Result::findone(1);
//Add dynamic attribute to the object say 'result'
$result->attributes = array('attempt' => 1);
If it is not possible, please suggest an alternate best method to implement it.
Finally I would be converting the result to a json object. In my application, at the behaviour code block, I have used like this:
'formats' => [
'application/json' => Response::FORMAT_JSON,
],

You can add define a public variable inside your model, that will store dynamic attributes as associative array. It'll look something like this:
class Result extends \yii\db\ActiveRecord implements Arrayable
{
public $dynamic;
// Implementation of Arrayable fields() method, for JSON
public function fields()
{
return [
'id' => 'id',
'created_at' => 'created_at',
// other attributes...
'dynamic' => 'dynamic',
];
}
...
..in your action pass some dynamic values to your model, and return everything as JSON:
public function actionJson()
{
\Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
$model = Result::findOne(1);
$model->dynamic = [
'field1' => 'value1',
'field2' => 2,
'field3' => 3.33,
];
return $model;
}
In result you will get JSON like this:
{"id":1,"created_at":1499497557,"dynamic":{"field1":"value1","field2":2,"field3":3.33}}

Related

Laravel: Parse JSON object with array of "sub-objects" to model instance

In my (Laravel) application receive a JSON which looks like:
{
"name": "order 1",
"customer": "cus123",
"orderItems": [
{
"amount": 1,
"name": "cola",
"price": "2.10"
},
{
"amount": 3,
"name": "fanta",
"price": "2.00"
},
]
}
I have create 2 models in Laravel, one Order and one OrderItem. I want to parse the received JSON to one Order instance $order.
I can get this done so by doing this in my OrderController:
class OrderController extends Controller
{
public function store(Request $request) {
$order = new Order();
$order->forceFill($request->toArray());
}
}
It's possible to access properties now like $order->name and $order->customer in the store function of the controller. When i access the $order->orderItems i receive an array with "orderItemsbut as array, not as instance ofOrderItem`.
I want that $order->orderItems returns an array of OrderItem instances. I tried the following in Order but this does not work as 'orderItems' is not a OrderItem::class but is an array with multiple "OrderItems".
protected $casts = [
'orderItems' => OrderItem::class,
];
How can i achieve that $order->orderItems returns an array of OrderItem instances?
Thanks for any help in advance!
Try to add the following to your controller
validation
manual storing your Order
manual storing each of your order items
.
class OrderController extends Controller
{
public function store(Request $request)
{
$your_rules = [
'name' => 'required|string',
'customer' => 'required|string', // related to customer id ?
'orderItems' => 'array',
'orderItems.*.name' => 'string',
'orderItems.*.amount' => 'integer|gte:1',
'orderItems.*.price' => 'numeric|between:0,99.99',
];
$validated = $request->validate($your_rules);
$order = Order::create([
'name' => $validated['name'],
'customer' => $validated['customer'], // is this customer id or name ?
]);
// I assume you already declare relationship to OrderItem inside your Order model
foreach ($validated['orderItems'] as $orderItem) {
// this array only is optional
$orderItem = Arr::only($orderItem, ['name', 'amount', 'price');
$order->orderItems()->save($orderItem);
}
// reload saved order items
$order->load('orderItems');
dd($order);
}
}
You can also create multiple children in single command.
$order->orderItems()->saveMany([
new OrderItem(['name' => '...', ... ]),
new OrderItem(['name' => '...', ... ]),
]);
Read here for more info https://laravel.com/docs/9.x/eloquent-relationships#the-save-method
You can move this into your model as extra custom method.
For example:
public function saveOrderItems(array $orderItems): void
{
$this->orderItems()->saveMany($orderItems);
}
And you call it as $order->saveOrderItems($orderItems);
P.S.
Dont forget to declare relationship in Order model.
public function orderItems()
{
return $this->hasMany(OrderItem::class);
}
I think you are confuse with the whole Model relationship. Checkout the documentation here, you need to define proper relationship and foreign key between your Order and OrderItem model.
Then your model should be like this;
//Order.php
class Order extends Model {
protected $fillable = [
'name',
'customer',
];
public function items() {
return $this->hasMany(OrderItem::class);
}
}
//OrderItem.php
class OrderItem extends Model {
protected $fillable = [
'amount',
'name',
'price'
];
public function order() {
return $this->belongsTo(Order::class);
}
}
Then your store method
public function store( Request $request ) {
$request->validate([
'name' => 'required',
'customer' => 'required|exists:customers_table,id',
'orderItems' => 'required|array'
]);
$order = Order::create( $request->except('orderItems') );
$items = $order->items()->createMany( $request->input('orderItems') );
}

Yii2: Filter query by pivot table when using `joinWith()`

Take the following sample Cart model. It has a CartItem "pivot record/table" which links it to Item.
class Cart extends ActiveRecord {
public function getCartItems() {
return $this
->hasMany(CartItem::class, ['cart_id' => 'id'])
->inverseOf('cart');
}
public function getItems($callback = null) {
return $this
->hasMany(Item::class, ['item_id' => 'id'])
->via('cartItems', $callback);
}
}
(Example #1) At this point I would be able to filter either by Item's activequery or CartItem's query like so:
$booksAddedToCartSinceYesterday = $cart
->getItems(function($cartItemQuery) {
$cartItemQuery->andWhere('cartItem.created_at > NOW()');
})
->andWhere(['item.category' => 'books']);
(Example #2) But how do I accomplish the same when I use the static find() method in combination with joinWith()? In the following example I am only able to filet by Item's ActiveQuery, but I no longer have any reference to the CartItem's ActiveQuery object:
$booksAddedToCartSinceYesterday = Cart::find()
->andWhere(['cart.user_id' => $some_user_id])
->joinWith([
'items' => function($itemQuery) {
$itemQuery->andWhere(['item.category' => 'books']);
},
]);
How do I modify the code above so that I am able to filter CartItem junction table records like I did in my example #1? How do I access the junction ActiveQuery object so that I can call $cartItemQuery->andWhere('cartItem.created_at > NOW()');?
you can pass any variable value in anonymous function with use keyword link below
make your magic code here for filter or any
->joinWith([
'items' => function($itemQuery) use ($var1,$var2){
$itemQuery->andWhere(['item.category' => 'books']);
$itemQuery->andWhere(['some_condition' => $var1]); <<<---------
},
]);

Cannot generate HalResource for object of type ArrayObject

I've some problems to return a paginator object as HAL json collection. I'm using the latest versions of zend-expressive and zend-expressive-hal.
This is the setting from my ConfigProvider:
public function __invoke() : array
{
return [
'dependencies' => $this->getDependencies(),
MetadataMap::class => $this->getHalConfig(),
];
}
public function getHalConfig() : array
{
return [
[
'__class__' => RouteBasedCollectionMetadata::class,
'collection_class' => RoleCollection::class,
'collection_relation' => 'user_roles',
'route' => 'api.user.roles',
],
];
}
And these are my handler methods:
public function get(ServerRequestInterface $request) : ResponseInterface
{
// read some records from the database
$select = new Select();
$select->from(['r' => 'user_roles']);
$select->columns(['id', 'name']);
$paginator = new RoleCollection(new DbSelect($select, $this->dbAdapter));
$paginator->setItemCountPerPage(25);
$paginator->setCurrentPageNumber(1);
return $this->createResponse($request, $paginator);
}
private function createResponse(ServerRequestInterface $request, $instance) : ResponseInterface
{
return $this->responseFactory->createResponse(
$request,
$this->resourceGenerator->fromObject($instance, $request)
);
}
The RoleCollection class is only an inheritance of the Paginator:
class RoleCollection extends Paginator
{
}
The error message which I get is:
Cannot generate Zend\Expressive\Hal\HalResource for object of type ArrayObject; not in metadata map
I think you are missing the metadata for the Role object itself.
For example this is something similar for my posts object:
MetadataMap::class => [
[
'__class__' => RouteBasedCollectionMetadata::class,
'collection_class' => Posts::class,
'collection_relation' => 'posts',
'route' => 'api.posts',
],
[
'__class__' => RouteBasedResourceMetadata::class,
'resource_class' => Post::class,
'route' => 'api.posts.view',
'extractor' => ArraySerializable::class,
],
],
You have only described the collection and the resource class is missing for a single role.
I also see the resource generator tries to parse an ArrayObject. This should be wrapped in a Role object, which you can add to the MetadataMap.
Where it goes wrong in your code is this line:
$paginator = new RoleCollection(new DbSelect($select, $this->dbAdapter));
This adds the result of a query into the paginator, but the paginator does not know how to handle it. If I remember correctly, the DbSelect return a ResultSet. I'm guessing this is where the ArrayObject is coming from. What you probably need is to override that ResultSet and make sure it returns an array of Role objects. You might want to look into the dbselect adapter and the hydrating resultset.
Once you have the Role object in the paginator, you can describe it in the metadata.
[
'__class__' => RouteBasedResourceMetadata::class,
'resource_class' => UserRole::class,
'route' => 'api.roles',
'extractor' => ...,
],
I use doctrine myself with hal so zend-db is out of my scope. If you need more help, I suggest the zf forums.

Yii2 SearchModel query - map integer values to strings

What I am trying to achieve:
I have a table with a type field which holds integer values. These integer values represent different strings.
I want to be able to search the table using the string values that the integers represent.
E.g type = abc rather than type = 0.
What have I tried:
I have created a query class for the model and tried to make use of the $boolean_map property:
class ReportQuery extends FilterableQuery
{
protected $filterable = [
'type' => 'LIKE',
'removed_the_rest'
];
protected $boolean_map = ["type" => [ 'addacs' => 0, "arudd" => 1,]];
}
Then I have overridden the find method of the model to use the query class:
public static function find()
{
$query = new ReportQuery(get_called_class());
return $query;
}
And in the search model I have:
public function search($params)
{
$query = Report::find();
$dataProvider = new ActiveDataProvider([
'query' => $query
]);
$this->load($params, '');
if (!$this->validate()) {
return $dataProvider;
}
// grid filtering conditions
$query->andFilterWhere([
'type' => $this->type,
]);
$query->andFilterWhere(['like', 'type', $this->type]);
return $dataProvider;
}
When searching by the string values I get an empty result. Searching by the integer values produces the data.
Any help is appreciated. Thanks.
Maybe it's better for you to make filter on that column instead of searching by string. You can do it for string as follows.
$filter = [
'example1' => 1,
'example2' => 2,
'example3' => 3,
];
$query->andFilterWhere(['like', 'type', $this->filter[$this->type]);
or in this place
// grid filtering conditions
$query->andFilterWhere([
'type' => $this->filter[$this->type],
])
also you can make filter dropdown on column, and for dropdown of that filter you can pass this array and just do
$query->andFilterWhere([
'type' => $this->type,
])
Why do you create mapping mechanism in query object? Okay, you show integer type as a string in frontend of your application, but the query shouldn't have details of representation. You should map string type to integer type in your search model. For example:
class ReportSearchModel extends ReportModel
{
public function mapType($value)
{
$items = [
'addacs' => 0,
'arudd' => 1
];
return array_key_exists($value, $items) ? $items[$value] : null;
}
public function search($params)
{
//another code
$query->andFilterWhere([
'type' => $this->mapType($this->type),
])
//another code
}
}
The alternative way is using an enum instead of mapping.

Yii2 creating a custom function in the model using Gii model generator

I am working on Yii2 using Gii to generate models. What I am trying to do is to customize my models such that all of them will have the following function
public static function getFoobarList()
{
$models = Foobar::find()->all();
return ArrayHelper::map($models, 'id', 'foobar');
}
Where Foobar is the name of individual models.
Thank you in advance.
You can create a custom template for your models which gii can use to generate your class.
Something like the following, added to the top of a copy of the file /vendor/yiisoft/yii2-gii/generators/model/default/model.php and the new file stored in, for example, #app/myTemplates/model/default
/**
* your doc string
*/
public static function get<?php echo $className; ?>List()
{
$models = static::find()->all();
return ArrayHelper::map($models, 'id', static::tableName());
}
will add the method you're looking for to any model created with the new template.
In your config something like
// config/web.php for basic app
// ...
if (YII_ENV_DEV) {
$config['modules']['gii'] = [
'class' => 'yii\gii\Module',
'allowedIPs' => ['127.0.0.1', '::1', '192.168.0.*', '192.168.178.20'],
'generators' => [ //here
'model' => [ // generator name
'class' => 'yii\gii\generators\model\Generator', // generator class
'templates' => [ //setting for out templates
'myModel' => '#app/myTemplates/model/default', // template name => path to template
]
]
],
];
}
will allow you to select your custom template when using gii, from the 'Code Template' menu.
Since you want this in all the models, another solution would be to add this function in ActiveRecord Model from which all generated models extend. You just need to change the function a bit to perform the required functionality.
Just add this to your ActiveRecord class:
public static function getModelList()
{
$models = static::find()->all();
return ArrayHelper::map($models, 'id', static::tableName());
}
To use this for any model, example Foobar all you'll need to do is:
Foobar::getModelList();