Laravel 5 cannot update relationship - php

I've been trying for countless hours now, but still having issues updating a models relationship, the closest I've got to is a 'Method fill does not exist.' error.
Listing model:
class Listing extends Model
{
protected $fillable = [
'uid', 'start_date',...........
];
public function locations()
{
return $this->hasMany('App\ListingLocation');
}
}
Location (relationship to listing - hasMany):
class ListingLocation extends Model
{
protected $fillable = [
'listing_id', 'location',
];
public function listing()
{
return $this->belongsTo('App\Listing');
}
}
This returns my model and relationship, which I can view with dd($listing)
$listing = Listing::with('locations')->findOrFail($id);
This will update my listing model, which I can see the changes after calling dd($listing) again
$listing->fill($array);
However when I attempt to fill the relationship as per below, I get 'Method fill does not exist.'
$listing->locations->fill($array['locations']);
How can I update the relationship successfully before calling $listing->push();?

Change your location to a single record, not a collection
For example:
$listings->locations->first()->fill($array['locations']);
to fill every record use foreach
#foreach($listings->locations as $location)
$location->fill(do_something);
#endforeach

I ended up creating a new class to extend hasMany which allowed me to use sync as per alexweissman at https://laracasts.com/discuss/channels/general-discussion/syncing-one-to-many-relationships.
Extract from forum:
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* #link https://github.com/laravel/framework/blob/5.4/src/Illuminate/Database/Eloquent/Relations/HasMany.php
*/
class HasManySyncable extends HasMany
{
public function sync($data, $deleting = true)
{
$changes = [
'created' => [], 'deleted' => [], 'updated' => [],
];
$relatedKeyName = $this->related->getKeyName();
// First we need to attach any of the associated models that are not currently
// in the child entity table. We'll spin through the given IDs, checking to see
// if they exist in the array of current ones, and if not we will insert.
$current = $this->newQuery()->pluck(
$relatedKeyName
)->all();
// Separate the submitted data into "update" and "new"
$updateRows = [];
$newRows = [];
foreach ($data as $row) {
// We determine "updateable" rows as those whose $relatedKeyName (usually 'id') is set, not empty, and
// match a related row in the database.
if (isset($row[$relatedKeyName]) && !empty($row[$relatedKeyName]) && in_array($row[$relatedKeyName], $current)) {
$id = $row[$relatedKeyName];
$updateRows[$id] = $row;
} else {
$newRows[] = $row;
}
}
// Next, we'll determine the rows in the database that aren't in the "update" list.
// These rows will be scheduled for deletion. Again, we determine based on the relatedKeyName (typically 'id').
$updateIds = array_keys($updateRows);
$deleteIds = [];
foreach ($current as $currentId) {
if (!in_array($currentId, $updateIds)) {
$deleteIds[] = $currentId;
}
}
// Delete any non-matching rows
if ($deleting && count($deleteIds) > 0) {
$this->getRelated()->destroy($deleteIds);
$changes['deleted'] = $this->castKeys($deleteIds);
}
// Update the updatable rows
foreach ($updateRows as $id => $row) {
$this->getRelated()->where($relatedKeyName, $id)
->update($row);
}
$changes['updated'] = $this->castKeys($updateIds);
// Insert the new rows
$newIds = [];
foreach ($newRows as $row) {
$newModel = $this->create($row);
$newIds[] = $newModel->$relatedKeyName;
}
$changes['created'][] = $this->castKeys($newIds);
return $changes;
}
/**
* Cast the given keys to integers if they are numeric and string otherwise.
*
* #param array $keys
* #return array
*/
protected function castKeys(array $keys)
{
return (array) array_map(function ($v) {
return $this->castKey($v);
}, $keys);
}
/**
* Cast the given key to an integer if it is numeric.
*
* #param mixed $key
* #return mixed
*/
protected function castKey($key)
{
return is_numeric($key) ? (int) $key : (string) $key;
}
}
You can then override Eloquent's hasMany method in your model class:
/**
* Overrides the default Eloquent hasMany relationship to return a HasManySyncable.
*
* {#inheritDoc}
*/
public function hasMany($related, $foreignKey = null, $localKey = null)
{
$instance = $this->newRelatedInstance($related);
$foreignKey = $foreignKey ?: $this->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
return new HasManySyncable(
$instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey
);
}
/**
* Get all of a user's phone numbers.
*/
public function phones()
{
return $this->hasMany('App\Phone');
}
A sync method will now be available to any hasMany relationships you have on this model:
$user->phones()->sync([
[
'id' => 21,
'label' => "primary",
'number' => "5555551212"
],
[
'id' => null,
'label' => "mobile",
'number' => "1112223333"
]
]);

Related

Laravel excel, update rows after save in DB

Project in Laravel (9), and PHP (8.1).
I want to import an excel file and use maatwebsite/excel (3.1) package.
I can import a file, and save the file into the model, like this:
import class:
class BankTransfersHistoryImport implements ToModel, WithHeadingRow, WithValidation, WithBatchInserts
{
use Importable;
private $rows;
public function __construct()
{
$this->rows = collect();
}
/**
* #param array $row
*
* #return \Illuminate\Database\Eloquent\Model|null
*/
public function model(array $row)
{
$bankTransferHistory = new BankTransfersHistory([
'loanId' => $row['loanId'],
'actionDate' => transformDate($row['actionDate']),
'worth' => $row['worth'],
.
.
]);
$this->rows->push($bankTransferHistory);
return $bankTransferHistory;
}
/**
* Returns Imported Data
*
* #return \Illuminate\Support\Collection
*/
public function getImportedData(): \Illuminate\Support\Collection
{
return $this->rows;
}
public function headingRow(): int
{
return 2;
}
public function rules(): array
{
return [
'*.loanId' => ['required', 'numeric'],
... some roles ...
];
}
}
controller:
public function store(Request $request)
{
$request->validate([
'file' => 'required|mimes:xls,xlsx',
]);
$file = $request->file('file');
$import = new BankTransfersHistoryImport;
try {
// date validation
$collection = $import->toCollection($file);
... some validation about the date ...
$import->import($file);
$getImportedData = import->getImportedData();
... check and update rows ...
return [
"message" => some message,
"data" => [
some data
],
];
} catch (\Maatwebsite\Excel\Validators\ValidationException$e) {
$failures = $e->failures();
foreach ($failures as $failure) {
$failure->row(); // row that went wrong
$failure->attribute(); // either heading key (if using heading row concern) or column index
$failure->errors(); // Actual error messages from Laravel validator
$failure->values(); // The values of the row that has failed.
}
return $failures;
}
My question is:
If I can get the response of the file after saving the data, that will give me the data with the id of the row that was saved.
In some cases, I will have to update a row. That's why I would like to get the ID.
Now, in the check and update rows section, I update row by loanId + actionDate. I want it to be done by only ID.
something like this:
code:
$getImportedData = import->getImportedData();
data will be like:
[
{
"id": 1,
"loanId": 21001,
"actionDate": "2020-01-02T00:00:00.000000Z",
"worth": 2997.09,
"offerId": 1,
},
{
"id": 2,
"loanId": 21002,
"actionDate": "2020-01-02T00:00:00.000000Z",
"worth": 3000,
"offerId": 10,
},
]
The solution to my problem.
To save the information and get the ID of each saved row, I did a few things.
I changed my import class.
First I changed the create from ToModel to ToCollection
I deleted WithBatchInserts because this method does not work with ToCollection.
Next, I called the getImportedData function.
That's how I got all the rows I create in the DB with their ID.
This solved the problem for me to get the information saved with the ID of each line, and perform validation + update if necessary.
The code is below.
A small note:
I changed the word rows to data in the `getImportedData' function.
I save all the files in the system.
import class:
class BankTransfersHistoryImport implements ToCollection, WithHeadingRow, WithValidation
{
use Importable;
private $data;
public function __construct()
{
$this->data = collect();
}
/**
* #param array $row
*
* #return \Illuminate\Database\Eloquent\Model|null
*/
public function collection(Collection $rows)
{
foreach ($rows as $row) {
$bankTransferHistory = BankTransfersHistory::create([
'loanId' => $row['loanId'],
'actionDate' => transformDate($row['actionDate']),
'worth' => $row['worth'],
.
.
]);
$this->data->push($bankTransferHistory);
}
}
/**
* Returns Imported Data
*
* #return \Illuminate\Support\Collection
*/
public function getImportedData(): \Illuminate\Support\Collection
{
return $this->data;
}
public function headingRow(): int
{
return 2;
}
public function rules(): array
{
return [
'*.loanId' => ['required', 'numeric'],
... some roles ...
];
}
}
store controller:
public function store(Request $request)
{
$request->validate([
'file' => 'required|mimes:xls,xlsx',
]);
$file = $request->file('file');
$import = new BankTransfersHistoryImport;
try {
// date validation
$collection = $import->toCollection($file);
... some validation about the date ...
// save the file in the system
$fileName = time() . '-' . $file->getClientOriginalName();
$file->storeAs('import bank transfers history', $fileName);
$import->import($file);
$importedData = $import->getImportedData(); // data after save in DB
... check and update rows ...
return [
"message" => some message,
"data" => [
some data
],
];
} catch (\Maatwebsite\Excel\Validators\ValidationException$e) {
$failures = $e->failures();
foreach ($failures as $failure) {
$failure->row(); // row that went wrong
$failure->attribute(); // either heading key (if using heading row concern) or column index
$failure->errors(); // Actual error messages from Laravel validator
$failure->values(); // The values of the row that has failed.
}
return $failures;
}
}

How can I retrieve only filtered records in a Child Model?

I have a table that I want to use to show records of timesheet logs, I've been able to do a filter using whereHas which works, but when I try to filter by employee I still get the logs for all employees attribtued to those jobs instead of the one I'm searching for.
My controller:
$request = json_decode(json_encode($request->all(), true))->params;
$jobs = Job::whereHas('timesheets', function($query) use ($request) {
if (count($request->selected_employees) > 0) {
$query->wherein('employee_id', $request->selected_employees);
}
if (count($request->selected_clients) > 0) {
$query->wherein('client_id', $request->selected_clients);
}
if (!empty($request->start_date)) {
$query->where('date','>=',$request->start_date);
}
if (!empty($request->end_date)) {
$query->where('date','<=',$request->end_date);
}
});
$jobs = (new Job)->generateReport($jobs->get(), $request->selected_employees);
$result = array_merge_recursive($jobs);
return $result;
My Model which iterates through the job. So far everything is accurate except for the child relationship called 'timesheets', it's not defined here, but laravel auto populates it and I am not able to overwrite/replace anything with that attribute. Any ideas?
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Job extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ["client_id", "job_number", "job_date", "payment_type", "rate", "description"];
public $totalHoursArray = array();
/**
* #var array|mixed
*/
private $report_totals;
public function client() {
return $this->belongsTo(Client::class);
}
public function timesheets() {
return $this->hasMany(TimesheetLog::class);
}
public function creator(){
return $this->belongsTo(User::class,'created_by');
}
public function editor(){
return $this->belongsTo(User::class,'edited_by');
}
/**
*
* Returns a count of Employee Hours per job for timesheet entries
* currently selected in the Job model
*
* #return array
*/
public function getEmployeeHoursPerJob($employee_ids){
$i = 0;
$hours_per_job = array();
$timesheets = empty($employee_ids) ? $this->timesheets : $this->timesheets->whereIn('employee_id',$employee_ids);
foreach ( $timesheets as $trow) {
$trow->employee_code = Employee::find($trow->employee_id)->code;
$date = new \DateTime($trow->date);
$trow->date = date_format($date, 'd-m-Y');
//find if the employee exists in the hours per job array if not, push a new row
$found = array_search($trow->employee_id,array_column($hours_per_job, 'employee_id', isset($hours_per_job['employee_id']) ? 'employee_id' : null));
if($i > 0 && $found !== false){
$hours_per_job[$found]['total_time'] += $trow->total_time;
} else {
array_push($hours_per_job, ['employee_id' => $trow->employee_id, 'employee_code' => $trow->employee_code, 'total_time' => ($trow->total_time)]);
}
$i++;
}
return $hours_per_job;
}
public function generateReport($jobs, Array $employee_ids){
$report_totals = array();
$filtered_timesheets = array();
foreach ($jobs AS $jobrow) {
$i = 0;
$jobrow->client_name = Client::find($jobrow->client_id)->name;
$jobrow->attention = Client::find($jobrow->client_id)->attention;
$jobrow->rate = "$".$jobrow->rate ." ". $jobrow->payment_type;
$dateT = new \DateTime($jobrow->job_date);
$jobrow->job_date = date_format($dateT, 'd-m-Y');
$hours = $jobrow->getEmployeeHoursPerJob($employee_ids);
$jobrow->employee_hours = $hours;
foreach ($filtered_timesheets as $timesheetf){
array_push($timesheets_filtered, $timesheetf);
}
foreach($hours AS $hoursRow){
$found = array_search($hoursRow['employee_id'],array_column($report_totals, 'employee_id',
isset($report_totals['employee_id']) ? 'employee_id' : null));
if($found !== false){
$report_totals[$found]['total_time'] += $hoursRow['total_time'];
} else {
array_push($report_totals, $hoursRow);
$i++;
}
}
}
return compact('jobs','report_totals');
}
}
In the foreach loop I assigned a new property of the row to a wherein query and this was accurate and what I wanted. But again, I couldn't replace or assign the original property that I want to send to the view.
$jobrow->timesheets_filtered = $jobrow->timesheets->wherein('employee_id',$employee_ids)->toArray();

How to sync Many To Many relation in pagekit?

I'm working with PageKit CMS. I have 2 tables with Many To Many relation (item and type).
Item model:
class Item implements \JsonSerializable
{
...
/**
* #ManyToMany(targetEntity="Type", tableThrough="#prefix_item_type", keyThroughFrom="item_id", keyThroughTo="type_id")
*/
public $types;
...
}
Type model:
class Type implements \JsonSerializable
{
...
/**
* #ManyToMany(targetEntity="Item", tableThrough="#prefix_item_type", keyThroughFrom="type_id", keyThroughTo="item_id")
*/
public $items;
...
}
In backend interface on item edit page I created multi select with all types. When I send item save request, I get type ids.
My save item method have a look:
public function saveAction($data, $id = 0, $selected_types = [])
{
/*
* $selected_types = array(2) {
* [0]=>int(1)
* [1]=>int(2)
* }
*/
if (!$id || !$item = Item::query()->related(['types'])) {
if ($id) {
App::abort(404, __('Item not found'));
}
$item = Item::create();
}
if (!$data['slug'] = App::filter($data['slug'] ?: $data['title'], 'slugify')) {
App::abort(400, __('Invalid alias'));
}
if(!App::user()->hasAccess('ext_name: manage all items')) {
$data['user_id'] = App::user()->id;
}
if(!App::user()->hasAccess('ext_name: manage all items') && !App::user()->hasAccess('ext_name: manage own items') && $item->user_id !== App::user()->id) {
App::abort(403, __('Access denied'));
}
$item->save($data);
/*
* Here I need to sync $item->types with $selected_types ids
*/
return [
'message' => 'success',
'entity' => $item,
];
}
How can I sync this relation if I have current item id and new type ids?

Laravel cant sync a many to many relationship

I am am trying to save to my database, and as part of that save I am trying to sync my many to many relationship, however I am getting the following error from my API,
"BadMethodCallException","message":"Call to undefined method Illuminate\\Database\\Query\\Builder::sync()"
I would have thought that this is because the relationships I have in my model are not many to many so cant be synced, but they look correct to me,
class Organisation extends Eloquent {
//Organsiation __has_many__ users (members)
public function users()
{
return $this->belongsToMany('User')->withPivot('is_admin');
}
//Organisation __has_many__ clients
public function clients()
{
return $this->belongsToMany('Client');
}
//Organisation __has_many__ teams
public function teams()
{
return $this->belongsToMany('Team');
}
//Organisation __has_many__ projects
public function projects()
{
return $this->hasMany('Project');
}
}
class User extends Eloquent implements UserInterface, RemindableInterface {
use UserTrait, RemindableTrait;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'users';
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
protected $hidden = array('password', 'remember_token');
public function organisations()
{
return $this->belongsToMany('Organisation')->withPivot('is_admin');
}
}
I am running the sync after a successful save,
if(isset($members)) {
$organisation->users()->sync($members);
}
and members is certainly set. The organsisation is created in the following way,
public function create()
{
//
$postData = Input::all();
$rules = array(
'name' => 'required',
);
$validation = Validator::make(Input::all(), $rules);
if($validation->fails()) {
return Response::json( $validation->messages()->first(), 500);
} else {
$organisation = new Organisation;
// Save the basic organistion data.
$organisation->name = $postData['name'];
$organisation->information = $postData['information'];
$organisation->type = 'organisation';
/*
* Create an array of users that can used for syncinng the many-to-many relationship
* Loop the array to assign admins to the organisation also.
*/
if(isset($postData['members'])) {
$members = array();
foreach($postData['members'] as $member) {
if(isset($postData['admin'][$member['id']]) && $postData['admin'][$member['id']] == "on") {
$members[$member['id']] = array(
'is_admin' => 1
);
} else {
$members[$member['id']] = array(
'is_admin' => 0
);
}
}
}
/*
* Create an array of clients so we can sync the relationship easily
*
*/
if(isset($postData['clients'])) {
$clients = array();
foreach($postData['clients'] as $client) {
$clients[] = $client['id'];
}
}
/*
* Create an array of teams so we can sync the relationship easily
*
*/
if(isset($postData['teams'])) {
$teams = array();
foreach($postData['teams'] as $team) {
$teams[] = $team['id'];
}
}
/*
* Create an array of projects so we can sync the relationship easily
*
*/
if(isset($postData['projects'])) {
$projects = array();
foreach($postData['projects'] as $project) {
$projects[] = $project['id'];
}
}
if( $organisation->save() ) {
if(isset($members)) {
$organisation->users()->sync($members);
}
if(isset($teams)) {
$organisation->teams()->sync($teams);
}
if(isset($teams)) {
$organisation->clients()->sync($clients);
}
if(isset($projects)) {
$organisation->projects()->sync($projects);
}
$organisation->load('users');
$organisation->load('teams');
$organisation->load('clients');
$organisation->load('projects');
return Response::make($organisation, 200);
} else {
return Response::make("Something has gone wrong", 500);
}
}
}
I was looking a while for the problem and I didn't see any (I was looking at first sync as you suggested) but I looked again and I think the problem is not syncing users here. Probably the problem is:
if(isset($projects)) {
$organisation->projects()->sync($projects);
}
You are trying to use sync on 1 to many relationship because you defined it this way:
return $this->hasMany('Project');
So either change hasMany here into belongsToMany if it's many to many relationship (that's probably the case) or don't use sync here for $projects because it works only for many to many relationship.

Lithium: How to call one validation filter from inside another

I have a base model that I extend from.
In it, I have defined two validation filters. One checks if a record is unique, the other checks if a record exists. They work the exact same way except ones return value will be the opposite of the other.
So, it doesn't sound right to write the same code twice to only return a different value.
I'd like to know how I can call one custom validator from another.
Here's my code for the unique validator:
<?php
Validator::add('unique', function($value, $rule, $options) {
$model = $options['model'];
$primary = $model::meta('key');
foreach ($options['conditions'] as $field => $check) {
if (!is_numeric($field)) {
if (is_array($check)) {
/**
* array(
* 'exists',
* 'message' => 'You are too old.',
* 'conditions' => array(
*
* 'Users.age' => array('>' => '18')
* )
* )
*/
$conditions[$field] = $check;
}
} else {
/**
* Regular lithium conditions array:
* array(
* 'exists',
* 'message' => 'This email already exists.',
* 'conditions' => array(
* 'Users.email' //no key ($field) defined
* )
* )
*/
$conditions[$check] = $value;
}
}
/**
* Checking to see if the entity exists.
* If it exists, record exists.
* If record exists, we make sure the record is not checked
* against itself by matching with the primary key.
*/
if (isset($options['values'][$primary])) {
//primary key value exists so it's probably an update
$conditions[$primary] = array('!=' => $options['values'][$primary]);
}
$exists = $model::count($conditions);
return ($exists) ? false : true;
});
?>
exists should work like this:
<?php
Validator::add('exists', function($value, $rule, $options) {
$model = $options['model'];
return !$model::unique($value, $rule, $options);
});
?>
But obviously, it can't be done that way. Would I have to define the validation function as an anonymous function, assign it to a variable and pass that in instead of the closure?
Or is there a way I can call unique from within exists?
The anonymous function method would work. And then you could use that variable in another anonymous function you define for the 'exists' validator. Here's another idea that incorporates it into your base model class:
<?php
namespace app\data\Model;
use lithium\util\Validator;
class Model extends \lithium\data\Model {
public static function __init() {
static::_isBase(__CLASS__, true);
Validator::add('unique', function($value, $rule, $options) {
$model = $options['model'];
return $model::unique(compact('value') + $options);
});
Validator::add('exists', function($value, $rule, $options) {
$model = $options['model'];
return !$model::unique(compact('value') + $options);
});
parent::__init();
}
// ... code ...
public static function unique($options) {
$primary = static::meta('key');
foreach ($options['conditions'] as $field => $check) {
if (!is_numeric($field)) {
if (is_array($check)) {
/**
* array(
* 'exists',
* 'message' => 'You are too old.',
* 'conditions' => array(
*
* 'Users.age' => array('>' => '18')
* )
* )
*/
$conditions[$field] = $check;
}
} else {
/**
* Regular lithium conditions array:
* array(
* 'exists',
* 'message' => 'This email already exists.',
* 'conditions' => array(
* 'Users.email' //no key ($field) defined
* )
* )
*/
$conditions[$check] = $options['value'];
}
}
/**
* Checking to see if the entity exists.
* If it exists, record exists.
* If record exists, we make sure the record is not checked
* against itself by matching with the primary key.
*/
if (isset($options['values'][$primary])) {
//primary key value exists so it's probably an update
$conditions[$primary] = array('!=' => $options['values'][$primary]);
}
$exists = $model::count($conditions);
return ($exists) ? false : true;
}
}
?>
I ended up creating a separate method that contains the functionality I need and then calling it from my validation filter.
I've trimmed down my base model to hold only the relevant data in it. Hope it helps someone who has a similar problem.
<?php
namespace app\extensions\data;
class Model extends \lithium\data\Model {
public static function __init() {
parent::__init();
Validator::add('unique', function($value, $rule, $options) {
$model = $options['model'];
return ($model::exists($value, $rule, $options, $model)) ? false : true;
});
Validator::add('exists', function($value, $rule, $options) {
$model = $options['model'];
return ($model::exists($value, $rule, $options, $model)) ? true : false;
});
}
public static function exists($value, $rule, $options, $model) {
$field = $options['field'];
$primary = $model::meta('key');
if (isset($options['conditions']) && !empty($options['conditions'])) {
//go here only of `conditions` are given
foreach ($options['conditions'] as $field => $check) {
if (!is_numeric($field)) {
if (is_array($check)) {
/**
* 'conditions' => array(
* 'Users.age' => array('>' => 18) //condition with custom operator
* )
*/
$conditions[$field] = $check;
}
} else {
/**
* Regular lithium conditions array:
* 'conditions' => array(
* 'Users.email' //no key ($field) defined
* )
*/
$conditions[$check] = $value;
}
}
} else {
//since `conditions` is not set, we assume
$modelName = $model::meta('name');
$conditions["$modelName.$field"] = $value;
}
/**
* Checking to see if the entity exists.
* If it exists, record exists.
* If record exists, we make sure the record is not checked
* against itself by matching with the primary key.
*/
if (isset($options['values'][$primary])) {
//primary key value exists so it's probably an update
$conditions[$primary] = array('!=' => $options['values'][$primary]);
}
return $model::count($conditions);
}
}
?>

Categories