I'm trying to provide a Select list with only records that are related to the model via a pivot table.
While building a time tracker/budgeting software for a client I have two models I'm working with called Budgets and Projects that are joined together with a pivot table. (So budgets, projects, and budget_project)
I'm trying to display all projects that are related to a selected Budget (from the Budget resource when calling an action) on a Select field. I can't figure out how to pass the model->id into the fields function. I will then be running some code to analyze the Projects associated with the given Budget and creating a bunch of records that extend across the date range and other relationships.
Please help!
I'm looking for something like this...
class CreateSchedule extends Action
{
use InteractsWithQueue, Queueable, SerializesModels;
/**
* Perform the action on the given models.
*
* #param \Laravel\Nova\Fields\ActionFields $fields
* #param \Illuminate\Support\Collection $models
* #return mixed
*/
public function handle(ActionFields $fields, Collection $models)
{
return Action::message('all good');
}
/**
* Get the fields available on the action.
*
* #return array
*/
public function fields()
{
$budgetProject = BudgetProject::where('budget_id',$this->id)->get();
foreach($budgetProject as $bp){
$projectArray[$bp->project_id] = $bp->project->name;
}
return [
Select::make('Project','project_id')->options($projectArray),
];
}
}
For me it it works like this
pass the id in the Resource class
(new LogsDiffAction($this->id))
Create a constructor in Action class to receive this parameter
protected $model;
public function __construct($model)
{
$this->model = $model;
}
and in fields you can do
if ($this->model) {
$entity = Activity::find($this->model);
$model = json_decode($entity->properties, true);
$partial = view('admin.nova.log-diff-partial', ['model' => $model])->toHtml();
return [
Heading::make($partial)->asHtml(),
];
}
I face a similar issue when I was working in the newsletter section where I have Template and Campaign models
You can add your model by doing this if you want to get recorded data
Notice onlyOneTabeRow function is an inline action and it's Mandatory to pass model
public function actions(Request $request)
{
return [
(new CreateCampaign($this))
->onlyOnTableRow()
];
}
Now you can receive them in CreateCampaign action contractor
public function __construct($model)
{
$this->template = $model;
}
And without making any request to the database you can get current record data like this
public function fields()
{
return [
Text::make('Name', 'name')
->default(function ($request) {
return $this->template->name;
})
->rules('required'),
Text::make('Subject', 'subject')
->default(function ($request) {
return $this->template->subject;
})->rules('required'),
Textarea::make('Content', 'content')
->default(function ($request) {
return $this->template->content;
})->rules('required')->showOnIndex(),
Hidden::make('Template Id', 'template_id')
->default(function ($request) {
return $this->template->id;
})
];
}
Here is a photo of what I want once I clicked on inline action Create Campaign in the first record I get a form with the specific record I want displaying in action form
For anyone who will struggle with Nova, I had to do something very similar.
Just as other have suggested, You need to do the following steps.
Pass whatever you need in the Action class as parameter while registering action.
( new MyAction($this->myParam) )
Make a constructor in your action class and accept the parameter.
protected $receivedParam;
public function __construct($param)
{
$this->receivedParam = $param;
}
You can then use your parameter in your fields() to do whatever you need to do with it.
Note For Actions initiated From Lens
However, While initiating Action from Lens, this will not work as you won't simply get the parameter while doing ( new MyAction($this->someParam) ). In the context of Lens, You need to first beat the Std Object that pops up and then you need to dig down the resource and then its attributes from $this.
// pass entire $this here and filter down in the Action Class
return [(new GiveMediaAccountAccessViaLens($this))]; // registering action from Lens
// Action Class
protected $receivedParam;
public function __construct($receivedParam)
{
$this->receivedParam = $receivedParam; // $receivedParam is $this from MediaAccountFilterByClient Lens
}
public function fields()
{
if(!$this->receivedParam->resource instanceof Model)
{
// If you are trying to generate a dropdown/select then you should set your options to
// empty array. $options = [];
$options = [];
}
else
{
// Here, $this->receivedParam->resource contains the Model Object, by calling
// getRawOriginal() on it, You will get the Original Attributes array which contains
// key value pair, like "some Attribute => "value"
// $this->receivedParam->resource->getRawOriginal()['someAttributeOnModel']
// Finally in case you are making a dropdown, you can your data to the array that
// will be passed to Select field.
$options = Arr::add($options, "value", "label");
}
// finally return the fields with custom filled Dropdown
return [
// key will be used as key to get the field value in handle()
Select::make('Label', 'key')->options($options),
];
}
For my use case, I ended up using two Action Classes (one to use while registering action from Lens, another one for Action registration from Resource Class because I was generating a custom dropdown.
Nova does some weird stuff and I was getting an invalid argument for foreach while executing action from Resource.
public function fields(Request $request)
{
$budgetProject = BudgetProject::where('budget_id',$request->budget_id)->get();
dd($budgetProject);
foreach($budgetProject as $bp){
$projectArray[$bp->project_id] = $bp->project->name;
}
return [
Select::make('Project','project_id')->options($projectArray),
];
}
Related
I am creating a new API call for our project.
We have a table with different locales. Ex:
ID Code
1 fr_CA
2 en_CA
However, when we are calling the API to create Invoices, we do not want to send the id but the code.
Here's a sample of the object we are sending:
{
"locale_code": "fr_CA",
"billing_first_name": "David",
"billing_last_name": "Etc"
}
In our controller, we are modifying the locale_code to locale_id using a function with an extension of FormRequest:
// This function is our method in the controller
public function createInvoice(InvoiceCreateRequest $request)
{
$validated = $request->convertLocaleCodeToLocaleId()->validated();
}
// this function is part of ApiRequest which extend FormRequest
// InvoiceCreateRequest extend ApiRequest
// So it goes FormRequest -> ApiRequest -> InvoiceCreateRequest
public function convertLocaleCodeToLocaleId()
{
if(!$this->has('locale_code'))
return $this;
$localeCode = $this->input('locale_code');
if(empty($localeCode))
return $this['locale_id'] = NULL;
$locale = Locale::where(Locale::REFERENCE_COLUMN, $localeCode)->firstOrFail();
$this['locale_id'] = $locale['locale_id'];
return $this;
}
If we do a dump of $this->input('locale_id') inside the function, it return the proper ID (1). However, when it goes through validated();, it doesn't return locale_id even if it's part of the rules:
public function rules()
{
return [
'locale_id' => 'sometimes'
];
}
I also tried the function merge, add, set, etc and nothing work.
Any ideas?
The FormRequest will run before it ever gets to the controller. So trying to do this in the controller is not going to work.
The way you can do this is to use the prepareForValidation() method in the FormRequest class.
// InvoiceCreateRequest
protected function prepareForValidation()
{
// logic here
$this->merge([
'locale_id' => $localeId,
]);
}
I am getting the records from my database in two different points, using "get" and "find" methods. The problem is that when I am using "get", "first" or "last" the hidden fields aren't displayed (Its ok), but when I am using "find" they are still there.
<?php
//My Plugin in /plugins/Comunica/Files/src/Model/Entity/File.php
namespace Comunica\Files\Model\Entity;
use Cake\ORM\Entity;
class File extends Entity
{
protected $_hidden = ['password'];
protected $_virtual = ['protected'];
protected function _getProtected(){
return empty($this->_properties['protected']) ? false : true;
}
}
The Call Method:
<?php
$this->Files->find()->toArray();
Again. It is right when calling just one record (first, last, call), It's just wrong when trying with method "find". Any one knows how to solve this?
I have found an answer for this problem. The find returns an object that owns the entities of every result, so that you can convert them by using the "findAll" method inside the table's class.
<?php
//My Plugin in /plugins/Comunica/Files/src/Model/Entity/File.php
namespace Comunica\Files\Model\Entity;
use Cake\ORM\Entity;
use Cake\ORM\Query;//Include this class to manipulate the results
class File extends Entity
{
protected $_hidden = ['password'];
protected $_virtual = ['protected'];
protected function _getProtected(){
return empty($this->_properties['protected']) ? false : true;
}
//New formatation code
public function findAll(Query $query, array $options)
{
return $query->formatResults(function ($results) {
return $results->map(function($row) {
$row['upload_date'] = $this->dateTimeConvert($row['upload_date']);
return $row->toArray();
});
});
}
}
I solved it like this:
My main aim was to exclude hidden fields by default and have a way to explicitly get Entitys including hidden fields if I need them.
ModelsTable.php
public function beforeFind(Event $event, Query $query){
//ATTENTION: if password field is excluded we have to bypass for Auth-Component to work
if(array_key_exists('password',$_REQUEST)){
return $event;
}
$protected = $this->newEntity()->hidden;
$tableSchema = $this->schema();
$fields = $tableSchema->columns();
foreach($fields as $key => $name){
if(in_array($name,$protected)){
unset($fields[$key]);
}
}
$query->select($fields);
return $event;
}
Model.php
protected $_hidden = [
'password',
'otherSecret'
];
protected function _getHidden(){
return $this->_hidden;
}
To receive hidden fields you can simple add ->select('password') to your query, but to make it more nice I added a custom finder
ModelsTable.php
public function findSecrets(Query $query, array $options)
{
$tableSchema = $this->schema();
$fields = $tableSchema->columns();
return $query->select($fields);
}
Now you can build a query like this to receive Entity including hidden fields:
ModelsController.php
$secretModels = $this->Models->find()->find('secrets');
or whatever query you loke, simply add the custom finder
NOTE: is does not work with ->get($id) so you have to use ->findById($id)->find('secrets')->first()
I'm happy to know what you think about this solution or what you would change - feel free to commend :-)
What I'm trying to do is to append the comments of each article to the articles object, but the problem is that I need to request different number of comments each time.
and for some reason I need to use mutators for that, because some times I request 50 articles and I don't want to loop through the result and append the comments.
So is it possible to do something like the following and how to pass the extra argument.
This the Model:
class Article extends Model
{
protected $appends = ['user', 'comments', 'media'];
public function getCommentsAttribute($data, $maxNumberOfComments = 0)
{
// I need to set maxNumberOfComments
return $this->comments()->paginate($maxNumberOfComments);
}
}
Here is the controller:
class PostsController extends Controller
{
public function index()
{
//This will automatically append the comments to each article but I
//have no control over the number of comments
$posts = Post::user()->paginate(10);
return $posts;
}
}
What I don't want to do is:
class PostsController extends Controller
{
public function index()
{
$articles = Post::user()->all();
$number = 5;
User::find(1)->articles()->map(function(Article $article) {
$article['comments'] = $article->getCommnets($number);
return $article;
});
return Response::json($articles);
}
}
Is there a better way to do it? because I use this a lot and it does not seams right.
Judging from the Laravel source code, no – it's not possible to pass an extra argument to this magic accessor method.
The easiest solution is just to add another, extra method in your class that does accept any parameters you wish – and you can use that method instead of magic property.
Eg. simply rename your getCommentsAttribute() to getComments() and fire ->getComments() instead of ->comments in your view, and you are good to go.
I just set a public property on the model. At the accessing point, I update that property to my desired value. Then, in the attribute method, I read the desired arguments from that property. So, putting all of that together,
// Model.php
public $arg1= true;
public function getAmazingAttribute () {
if ($this->arg1 === false)
$this->relation()->where('col', 5);
else $this->relation()->where('col', 15);
}
// ModelController.php
$instance->arg1 = false;
$instance->append('amazing');
It is been a while for this question, but maybe someone will need it too.
Here is my way
{
/**
* #var string|null
*/
protected ?string $filter = null;
/**
* #return UserSettings[]|null
*/
public function getSettingsAttribute(): ?array
{
return services()->tenants()->settings($this)->getAll();
}
/**
* #return FeatureProperty[]|null
*/
public function getFeaturePropertiesAttribute(): ?array
{
return services()->tenants()->featureProperty($this)->getListByIds($this->filter);
}
/**
* #param string|null $filter
* #return Tenant
*/
public function filter(string $filter = null): Model
{
$this->filter = $filter;
return $this;
}
Accessor is using some service to get values. Service accepts parameters, in my case string, that will be compared with featureProperty->name
Magic happens when you return $this in filter method.
Regular way to call accessor would be:
$model->feature_properties
Extended way:
$model->filter('name')->feature_properties
Since filter argument can be null, we can have accessor like this:
$filter = null
$model->filter($filter)->feature_properties
In case you would like to play with it a little more you can think about overriding models getAttribute or magic __call methods implementing filter in manner which will be similar to laravel scopes
I know its an old question, but there is another option, but maybe not the best:
$articles = Post::user()->all();
$number = 5;
$articles->map(function($a) use($number){
$a->commentsLimit = $number;
return $a;
});
And then in getCommentsAttribute():
return $this->comments()->paginate($this->commentsLimit);
I have two models:
class Product extends Eloquent {
...
public function defaultPhoto()
{
return $this->belongsTo('Photo');
}
public function photos()
{
return $this->hasMany('Photo');
}
}
class Photo extends Eloquent {
...
public function getThumbAttribute() {
return 'products/' . $this->uri . '/thumb.jpg';
}
public function getFullAttribute() {
return 'products/' . $this->uri . '/full.jpg';
}
...
}
This works fine, I can call $product->defaultPhoto->thumb and $product->defaultPhoto->full and get the path to the related image, and get all photos using $product->photos and looping through the values.
The problem arises when the product does not have a photo, I can't seem to figure out a way to set a default value for such a scenario.
I have tried doing things such as
public function photos()
{
$photos = $this->hasMany('Photo');
if ($photos->count() === 0) {
$p = new Photo;
$p->url = 'default';
$photos->add($p);
}
return $photos;
}
I have also creating a completely new Collection to store the new Photo model in, but they both return the same error:
Call to undefined method Illuminate\Database\Eloquent\Collection::getResults()
Has anyone done anything similar to this?
Thanks in advance!
You could create an accessor on the Product model that did the check for you. Works the same if you just wanted to define it as a method, also (good for if you want to abstract some of the Eloquent calls, use an interface for your Product in case you change it later, etc.)
/**
* Create a custom thumbnail "column" accessor to retrieve this product's
* photo, or a default if it does not have one.
*
* #return string
*/
public function getThumbnailAttribute()
{
$default = $this->defaultPhoto;
return ( ! is_null($default))
? $default->thumb
: '/products/default/thumb.jpg';
}
You might also want to look into Presenters. A bit overkill for some situations, but incredibly handy to have (and abstract things like this away from your models).
I'd like to be able to add a custom attribute/property to an Laravel/Eloquent model when it is loaded, similar to how that might be achieved with RedBean's $model->open() method.
For instance, at the moment, in my controller I have:
public function index()
{
$sessions = EventSession::all();
foreach ($sessions as $i => $session) {
$sessions[$i]->available = $session->getAvailability();
}
return $sessions;
}
It would be nice to be able to omit the loop and have the 'available' attribute already set and populated.
I've tried using some of the model events described in the documentation to attach this property when the object loads, but without success so far.
Notes:
'available' is not a field in the underlying table.
$sessions is being returned as a JSON object as part of an API, and therefore calling something like $session->available() in a template isn't an option
The problem is caused by the fact that the Model's toArray() method ignores any accessors which do not directly relate to a column in the underlying table.
As Taylor Otwell mentioned here, "This is intentional and for performance reasons." However there is an easy way to achieve this:
class EventSession extends Eloquent {
protected $table = 'sessions';
protected $appends = array('availability');
public function getAvailabilityAttribute()
{
return $this->calculateAvailability();
}
}
Any attributes listed in the $appends property will automatically be included in the array or JSON form of the model, provided that you've added the appropriate accessor.
Old answer (for Laravel versions < 4.08):
The best solution that I've found is to override the toArray() method and either explicity set the attribute:
class Book extends Eloquent {
protected $table = 'books';
public function toArray()
{
$array = parent::toArray();
$array['upper'] = $this->upper;
return $array;
}
public function getUpperAttribute()
{
return strtoupper($this->title);
}
}
or, if you have lots of custom accessors, loop through them all and apply them:
class Book extends Eloquent {
protected $table = 'books';
public function toArray()
{
$array = parent::toArray();
foreach ($this->getMutatedAttributes() as $key)
{
if ( ! array_key_exists($key, $array)) {
$array[$key] = $this->{$key};
}
}
return $array;
}
public function getUpperAttribute()
{
return strtoupper($this->title);
}
}
The last thing on the Laravel Eloquent doc page is:
protected $appends = array('is_admin');
That can be used automatically to add new accessors to the model without any additional work like modifying methods like ::toArray().
Just create getFooBarAttribute(...) accessor and add the foo_bar to $appends array.
If you rename your getAvailability() method to getAvailableAttribute() your method becomes an accessor and you'll be able to read it using ->available straight on your model.
Docs: https://laravel.com/docs/5.4/eloquent-mutators#accessors-and-mutators
EDIT: Since your attribute is "virtual", it is not included by default in the JSON representation of your object.
But I found this: Custom model accessors not processed when ->toJson() called?
In order to force your attribute to be returned in the array, add it as a key to the $attributes array.
class User extends Eloquent {
protected $attributes = array(
'ZipCode' => '',
);
public function getZipCodeAttribute()
{
return ....
}
}
I didn't test it, but should be pretty trivial for you to try in your current setup.
I had something simular:
I have an attribute picture in my model, this contains the location of the file in the Storage folder.
The image must be returned base64 encoded
//Add extra attribute
protected $attributes = ['picture_data'];
//Make it available in the json response
protected $appends = ['picture_data'];
//implement the attribute
public function getPictureDataAttribute()
{
$file = Storage::get($this->picture);
$type = Storage::mimeType($this->picture);
return "data:" . $type . ";base64," . base64_encode($file);
}
Step 1: Define attributes in $appends
Step 2: Define accessor for that attributes.
Example:
<?php
...
class Movie extends Model{
protected $appends = ['cover'];
//define accessor
public function getCoverAttribute()
{
return json_decode($this->InJson)->cover;
}
you can use setAttribute function in Model to add a custom attribute
Let say you have 2 columns named first_name and last_name in your users table and you want to retrieve full name. you can achieve with the following code :
class User extends Eloquent {
public function getFullNameAttribute()
{
return $this->first_name.' '.$this->last_name;
}
}
now you can get full name as:
$user = User::find(1);
$user->full_name;
In my subscription model, I need to know the subscription is paused or not.
here is how I did it
public function getIsPausedAttribute() {
$isPaused = false;
if (!$this->is_active) {
$isPaused = true;
}
}
then in the view template,I can use
$subscription->is_paused to get the result.
The getIsPausedAttribute is the format to set a custom attribute,
and uses is_paused to get or use the attribute in your view.
in my case, creating an empty column and setting its accessor worked fine.
my accessor filling user's age from dob column. toArray() function worked too.
public function getAgeAttribute()
{
return Carbon::createFromFormat('Y-m-d', $this->attributes['dateofbirth'])->age;
}