Yii2 elasticsearch setAttributes() not working - php

I am using Yii 2.1 and Yii2-elasticsearch 2.0.3 with a elasticsearch 1.5.0 server to try and index a Member model for a more powerful search. I have a common\indexes\Member model that extends yii\elasticsearch\ActiveRecord and set up the attributes I want to index.
namespace common\indexes;
use yii\elasticsearch\ActiveRecord;
class Member extends ActiveRecord {
/**
* #return array the list of attributes for this record
*/
public function attributes() {
// path mapping for '_id' is setup to field 'id'
return ['id', 'given_name', 'family_name', 'email'];
}
}
I am having trouble setting the attributes I want in the common\indexes\Member model.
I am creating a new instance of the object and trying to set the attribute values via the ActiveRecord setAttributes() method but it doesn't seem to set any values.
$index = new common\indexes\Member();
$index->setAttributes(['given_name' => 'Test', 'family_name' => 'User', 'email' => 'test.member#test.com']);
$index->save();
This seems to create an empty record. If I manually set the attributes one by one everything seems to work and a record with the correct attributes is created in the elasticsearch database.
$index = new common\indexes\Member();
$index->given_name = 'Test';
$index->family_name = 'User';
$index->email = 'test.member#test.com';
$index->save();
Am I using the setAttributes() method incorrectly for elasticsearch ActiveRecord's? Do I need to set up my elasticsearch Model differently?

By default setAttributes only sets attributes that have at least a single validation rule defined or those that are - as a minimum - defined as being "safe" (either via safeAttributes() or via the safe-validator).
You can force it to assign everything by just changing the call to
$index->setAttributes([
'given_name' => 'Test',
'family_name' => 'User',
'email' => 'test.member#test.com'
], false);
This tells it it is okay to also assign non-safe attributes.
But I usually prefer to make sure the validation is configured correctly

Related

Laravel API Resource timestamp "Call to a member function format() on null"

I'm using Laravel's API Resource functionality to format my responses nicely for the client, but the trouble I'm having is with the code below;
/**
* Transform the resource collection into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'data' => $this->collection->transform(function ($item)
{
return [
'id' => $item->id,
'title' => Str::limit($item->title, 32),
'body' => Str::limit($item->body, 32),
'created_at' => $item->created_at->format('d M Y, H:i a'),
'user' => $item->user
];
}),
'links' => [
'current_page' => $this->currentPage(),
'total' => $this->total(),
'per_page' => $this->perPage(),
],
];
}
When using this code, I get an error; "Call to a member function format() on null" on the created_at attribute.
But I've already used dd($this->collection) to confirm that none of the attributes are in fact null and I'm not really sure what could be causing it. My migration contains $table->timestamps();, and inside my factory, I'm not overriding the timestamps at all, so I'm not really sure what the issue is.
Here is the test I'm running below to get this error as well;
factory(News::class, 10)->create();
$user = factory(User::class)->create();
$this->actingAs($user)
->get('/news')
->assertOk()
->assertPropCount('news.data', 10)
->assertPropValue('news.data', function ($news)
{
$this->assertEquals(
[
'id', 'title', 'body', 'created_at',
'user',
],
array_keys($news[0])
);
});
The extra functions such as assertPropCount and assertPropValue are sourced from the InertiaJS demo app as I'm using InertiaJS in my project.
Hopefully, someone is able to help as I've asked around a few other places and no one seems to know what the reason for this is, and based on my debugging there doesn't really seem to be much of a valid explanation as to WHY created_at is null.
As a note, if I turn $item->user to $item->user->toArray() in the code as well, this then also fails complaining that user is null when it isn't. It seems that trying to chain any method onto any attribute causes this null error and I'm not sure why.
First of all keep in mind that the transform function you are using alter the original $this->collection property, you better use map instead that serves the same purpose as transform without altering the original array.
This might be related to your problem because you are modifying the collection you are iterating on, and that can cause issues.
Furthermore, I would suggest you to keep on reading this answer and try out one of the two refactoring alternatives I explained below. That's because I think you are not using API resources correctly and using them properly could actually solve the issue.
Suggestion about your API resource structure
The correct way would be to have two separate files: a News resource and a NewsCollection resource.
This setup allows to define the rendering structure of a single News as well as a collection of News and reuse the former while rendering the latter.
To implement API resources correctly there are a couple of ways (based on what you are trying to achieve):
Note: in both methods, I take for granted that you already have an additional User resource that defines the structure to render a User model (the $this->user property of a News).
1) Create separate classes for single and collection resources
You have to create two files in your resources folder through these two artisan commands:
// Create the single News resource
php artisan make:resource News
// Create the NewsCollection resource
php artisan make:resource NewsCollection
Now you can customize the collection logic:
NewsCollection.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class NewsCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
// Each $this->collection array item will be rendered automatically
// with the News resource definition, so you can leave data as it is
// and just customize the links section/add more data as you wish.
'data' => $this->collection,
'links' => [
'current_page' => $this->currentPage(),
'total' => $this->total(),
'per_page' => $this->perPage(),
],
];
}
}
as well as the single News resource logic:
News.php
<?php
namespace App\Http\Resources;
use App\Http\Resources\User as UserResource;
use Illuminate\Http\Resources\Json\JsonResource;
class News extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'title' => Str::limit($this->title, 32),
'body' => Str::limit($this->body, 32),
'created_at' => $this->created_at->format('d M Y, H:i a'),
'user' => new UserResource($this->user)
];
}
}
To render a news collection, you only have to do:
use App\News;
use App\Http\Resources\NewsCollection;
// ...
return new NewsCollection(News::paginate());
Laravel will automatically reuse the News resource class to render each single element of the NewsCollection's $this->collection array when you are converting the NewsCollection instance for response.
2) Exploit the ::collection method of the single News resource
This method is applicable only if you need metadata about paginated responses (it seems that is what you are trying to achieve with your code).
You just need the single News api resource that you can generate with:
// Create the single News resource
php artisan make:resource News
Then customize the single resource according to your needs:
News.php
<?php
namespace App\Http\Resources;
use App\Http\Resources\User as UserResource;
use Illuminate\Http\Resources\Json\JsonResource;
class News extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'title' => Str::limit($this->title, 32),
'body' => Str::limit($this->body, 32),
'created_at' => $this->created_at->format('d M Y, H:i a'),
'user' => new UserResource($this->user)
];
}
}
Then to render a paginated collection of news, just do:
use App\News;
use App\Http\Resources\News as NewsResource;
// ...
return NewsResource::collection(News::paginate());
The first method allow for a better overall control of the resulting output structure, but I would not structure the $this->collection inside the collection class.
The responsability to define how each collection element should be structured is of the News resource class.
The second method is quicker and works really nice with Laravel pagination allowing you to save quite some time to generate paginated responses with links (that seems what you want to achieve from your code).
Sorry for the long post, if you need further explaination just ask.

Laravel: array to Model with relationship tree

I want to create an Eloquent Model from an Array() fetched from database which is already toArray() of some model stored in database. I am able to do that using this code:
$model = Admin::hydrate($notification->data);
$notification->data = [
"name" => "abcd"
"email" => "abcd#yahoo.com"
"verified" => 0
"shopowner_id" => 1
"id" => 86
"shopowner" => [
"id" => 1
"name" => "Owner1"
"email" => "owner1#owner.com"
]
];
But i can't access the $model->shopowner->name
I have to use $model->shopowner['name']
I want to use the same class of notification without any specific change to access the data.
If you want to access shopowner as a relationship, you have to hydrate it manually:
$data = $notification->data;
$model = Notification::hydrate([$data])[0];
$model->setRelation('shopowner', ShopOwner::hydrate([$data['shopowner']])[0]);
Solution:
Thanks to #Devon & #Junas. by combining their code I landed to this solution
$data = $notification->data;
$data['shopowner'] = (object) $data['shopowner'];
$model = Admin::hydrate([$data])[0];
I see this as an invalid use of an ORM model. While you could mutate the array to fit your needs:
$notification->data['shopowner'] = (object) $notification->data['shopowner'];
$model = Admin::hydrate($notification->data);
Your model won't be functional because 'shopowner' will live as an attribute instead of a relationship, so when you try to use this model for anything other than retrieving data, it will cause an exception.
You cannot access array data as object, what you can do is override the attribute and create an instance of the object in your model, so then you can use it like that. For example:
public function getShopownerAttribute($value)
{
return new Notification($value); // or whatever object here
}
class Notification {
public function __construct($data)
{
// here get the values from your array and make them as properties of the object
}
}
It has been a while since I used laravel but to my understanding once you use hydrate your getting a Illuminate\Database\Eloquent\Collection Object, which then holds Model classes.
These however could have attributes that are lazy loaded when nested.
Using the collections fresh method could help getting a Full database object as using load missing

Factory Muffin relationships are "null" when using instance method

I'm working on a Laravel 5 app using the jenssegers\laravel-mongodb package and I'm trying to get up and running with Factory Muffin to help with rapidly testing and seeding etc.
I have defined relationships in my factory definitions (code below) - and they work when I run seeds to create new records in the database. By "work", I mean they attach related data correctly to the parent model and persist all those records to the database. But they do not work when I run the $muffin->instance('My\Object') method, which creates a new instance without persisting it. All the relations come back as null.
In a way this makes sense. When the models are stored in the database, they are related by the object _id key. That key doesn't exist until the model is stored. So when I call the instance method, I actually can see via debugging that it does generate the models that would be related, but it does not yet have any key to establish the relation, so that data just kinda goes poof.
This is a bummer because I'd like to be able to generate a fully fleshed-out model with its relations and see for example whether it is saved or passes validation and whatnot when I submit its data to my routes and things. I.e., right now I would not be able to check that the contact's email was valid, etc.
Simplified code samples are below - am I going about this in an entirely wrong way? This is my first time working with Mongo and Factory Muffin. Should this even be able to work the way I want it to?
// factories/all.php
$fm->define('My\Models\Event', [
'title' => Faker::text(75),
'contact' => 'factory|My\Models\Contact'
]);
$fm->define('My\Models\Contact', [
'first_name' => Faker::firstName(),
'email' => Faker::email(),
... etc
]);
// EventControllerTest.php
...
/**
* #var League\FactoryMuffin\FactoryMuffin
*/
protected $muffin;
public function setUp()
{
parent::setUp();
$this->muffin = new FactoryMuffin();
$this->muffin->loadFactories(base_path('tests/factories'));
}
...
public function testCanStoreEvent()
{
$event = $this->muffin->instance('Quirks\Models\Event');
echo $event->contact; // returns null
$event_data = $event->toArray();
$this->call('POST', 'events', $event_data);
$retrieved_event = Event::where('title', '=', $event->title)->get()->first();
$this->assertNotNull($retrieved_event); // passes
$this->assertRedirectedToRoute('events.edit', $retrieved_event->id); // passes
// So, the event is persisted and the controller redirects etc
// BUT, related data is not persisted
}
Ok, in order to get factory muffin to properly flesh out mongo embedded relationships, you have to do like this:
// factories/all.php
$embedded_contact = function() {
$definitions = [
'first_name' => Faker::firstName(),
'email' => Faker::email(),
// ... etc
];
return array_map(
function($item) { return $item(); },
$definitions
);
}
$fm->define('My\Models\Event', [
'title' => Faker::text(75),
'contact' => $embedded_contact
]);
If that seems onerous and hacky ... I agree! I'm definitely interested to hear of easier methods for this. For now I'm proceeding as above and it's working for both testing and seeding.

Populating Yii2 Model via behavior

I'm trying to assign a value to the property with behavior, but no matter what value I pass to $this->owner->property the model assigns this number "127" to the property and saves it. I can't figure out where this number comes from.
namespace common\behaviors;
use yii\db\ActiveRecord;
use yii\base\Behavior;
class MyBehavior extends Behavior
{
public function events()
{
return [
ActiveRecord::EVENT_BEFORE_INSERT => 'test',
ActiveRecord::EVENT_BEFORE_UPDATE => 'test',
];
}
public function test()
{
$this->owner->property = 444;
}
}
If I populate this property htrough the web form on frontend the model saves correct value.
I added property to model's rules but that doesn't make any difference.
Shame on me, I made so stupid mistake while creating SQL table :( I assigned tinyint type to property field, that's why it always saves 127 - the maximum allowed value for this type of field.

eloquent-sluggable build_from is being ignored

I have a class called Vara, where i have a table field called searchname. I want to do a simple setup of cviebrock eloquent sluggable but can't figure out what the issue is.
When i save my model, nothing happens, it rewrite the old value stored.
If i change in build_from to, whatthefuckisgoingon i get the same output. I have a field called handle, also tried changing the field namne to slug but same result. If i leave build_from empty i also get the same output.
If i however change save_to to something that doesn't exist i get an error. The searchname field does have a value of "Hjordnära test 33 liter", so the output is really wierd.
My guess is that build_from is being ignored, and seen as null. How do i fix this?
My Vara.php looks like this
use Cviebrock\EloquentSluggable\SluggableInterface;
use Cviebrock\EloquentSluggable\SluggableTrait;
class Vara extends \Eloquent implements SluggableInterface {
use SluggableTrait;
protected $sluggable = array(
'build_from' => 'searchname',
'save_to' => 'handle'
);
In my VarorController.php
public function saveVara()
{
$id = Input::get('id');
$vara = Vara::find(Input::get('id'));
$vara->edited_by = Auth::user()->id;
$vara->searchname = Input::get('searchname');
$vara->save();
return $vara->getSlug();
Ok a litle update, found this function in SluggableTrait.php
public function sluggify($force=false)
{
$config = \App::make('config')->get('eloquent-sluggable::config');
$this->sluggable = array_merge( $config, $this->sluggable );
if ($force || $this->needsSlugging())
{
$source = $this->getSlugSource();
$slug = $this->generateSlug($source);
$slug = $this->validateSlug($slug);
$slug = $this->makeSlugUnique($slug);
$this->setSlug($slug);
}
return $this;
}
so if i add $vara->sluggify(true); to my controller the slug is being saved, so now the questions is why it does not sluggify automaticly on $vara->save();
Most probably, it's an issue of validation because you're using Ardent:
Ardent is a package that "provides self-validating smart models for Laravel Framework 4's Eloquent ORM"
Check your rules and use if statement:
if(! $vara->save()) // if model is invalid
dd($vara->errors());
If you don't need to check validation , you may use
$vara->forceSave();
To integrate Eloquent sluggable with Ardent, take a look at this link

Categories