Laravel: Updating hasMany/BelongTo relation - php

I have a master table jobs with multiple location in separate table job_location. Now I am not able to update/delete, if extra rows found from job_location. Now why I am saying DELETE is because sync() did this, but it's related to many-to-many relation. I am new to laravel, just trying to get eloquent approach to achieve this, otherwise deleting all rows and inserting can be done easily OR updating each and delete remaining is also an option but I wonder Laravel has something for this.
In every request I get multiple job locations(with unchanged/changed city,phone_number,address) which is creating trouble.
Some codeshots:
Model: [Job.php]
class Jobs extends Model
{
protected $fillable = [
'job_id_pk', 'job_name','salary'
];
public function joblocation() {
return $this->hasMany('\App\JobLocation', 'job_id_fk', 'job_id_pk');
}
}
Model:[JobLocation.php]
class JobLocation extends Model
{
protected $fillable = [
'jobl_id_pk', 'job_id_fk','city', 'address', 'phone_number'
];
public function job() {
return $this->belongsTo('\App\Jobs', 'job_id_fk', 'job_id_pk');
}
}
Controller:[JobController.php]
function jobDetail() {
if($params['jid']) {
// update
$obj = \App\Jobs::find($params['jid']);
$obj->job_name = $params['name'];
$obj->salary = $params['salary'];
$obj->save();
} else {
// create new
$data = array(
'job_name' => $params['name'],
'salary' => $params['salary'],
);
$obj = \App\Jobs::create($data);
}
// don't bother how this $objDetail has associative array data, it is processed so
foreach ($params['jobLocations'] AS $key => $objDetail) {
$jobLoc = new \App\JobLocation;
$jobLoc->city = $objDetail['city'];
$jobLoc->phone_number = $objDetail['phone_number'];
$jobLoc->address = $objDetail['address'];
$jobLoc->job()->associate($obj);
$obj->jobLoc()->save($jobLoc);
}
}
In this approach I am able to save all job locations, but I am using same function to update also. Please tell how I can update jobLocations if present. I am ok to loose previous entries, but it would be good if previous gets updated and new get entered OR if we have extra entries they get deleted. I know sounds weird but still guide me a way.

Yea, you cannot use the same function, do this
$jobs = \App\Jobs::find($params['jid']);
foreach ($params['jobLocations'] as $key => $objDetail) {
$joblocation = $jobs->joblocation->where('jobl_id_pk', $objDetail['some_id'])->first();
//here update you job location
$joblocation->save();
}

Something like this:
Controller:[JobController]
public function jobDetail() {
if( !empty($params['jid']) ) {
// update
$job = \App\Jobs::find($params['jid']);
$job->job_name = $params['name'];
$job->salary = $params['salary'];
$job->save();
} else {
// create new
$data = array(
'job_name' => $params['name'],
'salary' => $params['salary'],
);
$job = \App\Jobs::create($data);
}
$locationDetails = !empty($params['jobLocations']) ? $params['jobLocations'] : [];
$jobLocations = array_map(function($location) use($job) {
$location = array_merge($location, [ 'job_id_fk' => $job->job_id_pk ]);
return \App\JobLocation::firstOrNew($location);
}, $locationDetails);
$job->jobLocations()->saveMany($jobLocations);
}

Related

Laravel 5.5 Method save does not exist when updating entries with modified primary key

I am working with laravel 5.5 to update entries. The problem is after changing the primary key 'id', which is elequoent default pk to 'project_id'. adding an item works fine but updating an item is not working properly. Here is the error I am getting.
Method save does not exist.
Here is my Model.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Project extends Model
{
protected $primaryKey = 'project_id';
public function user()
{
return $this->belongsTo(User::class);
}
public function tasks()
{
return $this->hasMany(Task::class);
}
}
Here is my controller function.
public function editProject($id){
$project = Project::where('project_id', $id)->firstOrFail();
$data = ["project_info" => $project];
return view('projects.edit')->with($data);
}
public function updateProject(Request $request){
$data = $request->all();
$validator = Validator::make($data, [
'project_title' => 'required',
'project_description' => 'required',
'project_start_date' => 'required',
'project_end_date' => 'required',
'project_status' => 'required',
]);
$response = [];
if ($validator->fails()){
$response["errors"] = [$validator->messages()->first()];
$response["success"] = false;
return json_encode($response);
}
else{
$project = Project::where("project_id", $request->input('project_id'))->get();
$project->project_title = $request->project_title;
$project->user_id = Session::get('user_id');
$project->project_description = $request->project_description;
$project->project_start_date = $request->project_start_date;
$project->project_end_date = $request->project_end_date;
$project->project_status = $request->project_status;
$project->save();
return redirect('/listProjects');
}
}
Using get() returns a collection. Despite the fact you are passing in a 'unique' ID, the project_id, it will still return a collection - the collection will simply have one element in it.
Subsequently, your code will not work as you have experienced, or at least not without a few changes to make $project reference the first element in the collection.
It's a quick fix though, just change this:
$project = Project::where("project_id", $request->input('project_id'))->get();
to this:
$project = Project::where("project_id", $request->input('project_id'))->first();
By using first(), eloquent will return the first element that matches the query and actually return the element (as opposed to a collection with one element) and so you can directly edit and save it.
Here is the solution I found.
$project_id = $request->input('project_id');
$project = Project::find($project_id);
$project->save();
You can find it by id using
Project::find($id);
Or get the first element like James said:
$project = Project::where("project_id", $request->input('project_id'))->first();

Laravel and a While Loop

I'm new to Laravel and at the moment I have a piece of code in a Controller which without the while loop it works, it retrieves my query from the database.
public function dash($id, Request $request) {
$user = JWTAuth::parseToken()->authenticate();
$postdata = $request->except('token');
$q = DB::select('SELECT * FROM maps WHERE user_id = :id', ['id' => $id]);
if($q->num_rows > 0){
$check = true;
$maps = array();
while($row = mysqli_fetch_array($q)) {
$product = array(
'auth' => 1,
'id' => $row['id'],
'url' => $row['url'],
'locationData' => json_decode($row['locationData']),
'userData' => json_decode($row['userData']),
'visible' => $row['visible'],
'thedate' => $row['thedate']
);
array_push($maps, $product);
}
} else {
$check = false;
}
return response()->json($maps);
}
I am trying to loop through the returned data from $q and use json_decode on 2 key/val pairs but I can't even get this done right.
Don't use mysqli to iterate over the results (Laravel doesn't use mysqli). Results coming back from Laravel's query builder are Traversable, so you can simply use a foreach loop:
$q = DB::select('...');
foreach($q as $row) {
// ...
}
Each $row is going to be an object and not an array:
$product = array(
'auth' => 1,
'id' => $row->id,
'url' => $row->url,
'locationData' => json_decode($row->locationData),
'userData' => json_decode($row->userData),
'visible' => $row->visible,
'thedate' => $row->thedate
);
You're not using $postdata in that function so remove it.
Do not use mysqli in Laravel. Use models and/or the DB query functionality built in.
You're passing the wrong thing to mysqli_fetch_array. It's always returning a non-false value and that's why the loop never ends.
Why are you looping over the row data? Just return the query results-- they're already an array. If you want things like 'locationData' and 'userData' to be decoded JSON then use a model with methods to do this stuff for you. Remember, with MVC you should always put anything data related into models.
So a better way to do this is with Laravel models and relationships:
// put this with the rest of your models
// User.php
class User extends Model
{
function maps ()
{
return $this->hasMany ('App\Map');
}
}
// Maps.php
class Map extends Model
{
// you're not using this right now, but in case your view needs to get
// this stuff you can use these functions
function getLocationData ()
{
return json_decode ($this->locationData);
}
function getUserData ()
{
return json_decode ($this->userData);
}
}
// now in your controller:
public function dash ($id, Request $request) {
// $user should now be an instance of the User model
$user = JWTAuth::parseToken()->authenticate();
// don't use raw SQL if at all possible
//$q = DB::select('SELECT * FROM maps WHERE user_id = :id', ['id' => $id]);
// notice that User has a relationship to Maps defined!
// and it's a has-many relationship so maps() returns an array
// of Map models
$maps = $user->maps ();
return response()->json($maps);
}
You can loop over $q using a foreach:
foreach ($q as $row) {
// Do work here
}
See the Laravel docs for more information.

Laravel visitors counter

I'm trying to build a visitors counter in Laravel....
I don't know what the best place is to put the code inside so that it loads on EVERY page... But I putted it inside of the routes.php....
I think I'll better place it inside of basecontroller?
But okay, My code looks like this now:
//stats
$date = new \DateTime;
$check_if_exists = DB::table('visitor')->where('ip', $_SERVER['REMOTE_ADDR'])->first();
$get_visit_day = DB::table('visitor')->select('visit_date')->where('ip', $_SERVER['REMOTE_ADDR'])->first();
$value = date_create($get_visit_day->visit_date);
if(!$check_if_exists)
{
DB::table('visitor')->insert(array('ip' => $_SERVER['REMOTE_ADDR'], 'hits' => '1', 'visit_date' => $date));
}else{
DB::table('visitor')->where('ip', $_SERVER['REMOTE_ADDR'])->increment('hits');
}
$value = date_create($get_visit_day->visit_date);
if ($check_if_exists && date_format($value, 'd') != date('d')) {
DB::table('visitor')->insert(array('ip' => $_SERVER['REMOTE_ADDR'], 'hits' => '1', 'visit_date' => $date));
}
That works fine, but the problem is, my database columns always add a new value.
So this is my database:
From the table 'visitor'.
It keeps adding a new IP, hit and visit_date...
How is it possible to just update the hits from today (the day) and if the day is passed, to set a new IP value and count in that column?
I'm not 100% sure on this, but you should be able to do something like this. It's not tested, and there may be a more elegant way to do it, but it's a starting point for you.
Change the table
Change the visit_date (datetime) column into visit_date (date) and visit_time (time) columns, then create an id column to be the primary key. Lastly, set ip + date to be a unique key to ensure you can't have the same IP entered twice for one day.
Create an Eloquent model
This is just for ease: make an Eloquent model for the table so you don't have to use Fluent (query builder) all the time:
class Tracker extends Eloquent {
public $attributes = [ 'hits' => 0 ];
protected $fillable = [ 'ip', 'date' ];
protected $table = 'table_name';
public static function boot() {
// Any time the instance is updated (but not created)
static::saving( function ($tracker) {
$tracker->visit_time = date('H:i:s');
$tracker->hits++;
} );
}
public static function hit() {
static::firstOrCreate([
'ip' => $_SERVER['REMOTE_ADDR'],
'date' => date('Y-m-d'),
])->save();
}
}
Now you should be able to do what you want by just calling this:
Tracker::hit();
Looking at your code and reading your description, I’m assuming you want to calculate number of hits from an IP address per day. You could do this using Eloquent’s updateOrNew() method:
$ip = Request::getClientIp();
$visit_date = Carbon::now()->toDateString();
$visitor = Visitor::findOrNew(compact('ip', 'visit_date'));
$visitor->increment('hits');
However, I would add this to a queue so you’re not hitting the database on every request and incrementing your hit count can be done via a background process:
Queue::push('RecordVisit', compact('ip', 'visit_date'));
In terms of where to bootstrap this, the App::before() filter sounds like a good candidate:
App::before(function($request)
{
$ip = $request->getClientIp();
$visit_date = Carbon::now()->toDateString();
Queue::push('RecordVisit', compact('ip', 'visit_date'));
);
You could go one step further by listening for this event in a service provider and firing your queue job there, so that your visit counter is its own self-contained component and can be added or removed easily from this and any other projects.
Thanks to #Joe for helping me fulley out!
#Martin, you also thanks, but the scripts of #Joe worked for my problem.
The solution:
Tracker::hit();
Inside my App::before();
And a new class:
<?php
class Tracker Extends Eloquent {
public $attributes = ['hits' => 0];
protected $fillable = ['ip', 'date'];
public $timestamps = false;
protected $table = 'visitor';
public static function boot() {
// When a new instance of this model is created...
static::creating(function ($tracker) {
$tracker->hits = 0;
} );
// Any time the instance is saved (create OR update)
static::saving(function ($tracker) {
$tracker->visit_date = date('Y-m-d');
$tracker->visit_time = date('H:i:s');
$tracker->hits++;
} );
}
// Fill in the IP and today's date
public function scopeCurrent($query) {
return $query->where('ip', $_SERVER['REMOTE_ADDR'])
->where('date', date('Y-m-d'));
}
public static function hit() {
static::firstOrCreate([
'ip' => $_SERVER['REMOTE_ADDR'],
'date' => date('Y-m-d'),
])->save();
}
}
Named 'tracker' :)
public $attributes = ['hits' => 0];
protected $fillable = ['ip', 'date'];
public $timestamps = false;
protected $table = 'trackers';
public static function boot() {
// When a new instance of this model is created...
parent::boot();
static::creating(function ($tracker) {
$tracker->hits = 0;
} );
// Any time the instance is saved (create OR update)
static::saving(function ($tracker) {
$tracker->visit_date = date('Y-m-d');
$tracker->visit_time = date('H:i:s');
$tracker->hits++;
} );
}
// Fill in the IP and today's date
public function scopeCurrent($query) {
return $query->where('ip', $_SERVER['REMOTE_ADDR'])
->where('date', date('Y-m-d'));
}
public static function hit() {
/* $test= request()->server('REMOTE_ADDR');
echo $test;
exit();*/
static::firstOrCreate([
'ip' => $_SERVER['REMOTE_ADDR'],
'date' => date('Y-m-d'),
// exit()
])->save();
}
In laravel 5.7 it required parent::boot() otherwise it will show Undefined index: App\Tracker
https://github.com/laravel/framework/issues/25455
This is what i did its very basic but can easily build it up and add filters on visitors per day month year etc..
i added the following code to the web.php file above all the routes to run on each request on the site so no matter what page the visitor landed on it will save the ip addess to the database only if its unique so one visitor wont keep addding to the visitor count
// Web.php
use App\Models\Visitor
$unique_ip = true;
$visitors = Visitor::all();
foreach($visitors as $visitor){
if($visitor->ip_address == request()->ip()){
$unique_ip = false;
}
}
if($unique_ip == true){
$visitor = Visitor::create([
'ip_address' => request()->ip(),
]);
}
Routes...
the model is straight forward just has a ip addess field

How I can save data in two tables simultaneously in laravel?

I have the following data mode
l
Detalle_Servicio has many Material_Usado and Material_Usado belongs only Detalle_Servicio
When I save values ​​in Material_Usado should know the ID Detelle_Servido, but as I can not do that. I have the following code
Route
Route::post('/upload', function(){
$path = public_path().'/servicios';
try{
$upload_success1 = Input::file('foto1')->move($path,'1');
$upload_success2 = Input::file('foto2')->move($path,'2');
$upload_success3 = Input::file('foto3')->move($path,'3');
$upload_success4 = Input::file('foto4')->move($path,'4');
}catch(Exception $e) {
$e->getMessage();
}
$input = Input::get('json');
$json = json_decode($input);
if($upload_success1 && $upload_success2 && $upload_success3 && $upload_success4) {
//DB::insert("INSERT INTO Detalle_Servicio (RutaFoto1, RutaFoto2, RutaFoto3, RutaFoto4, FechaTermino, Latitud, Longitud, Servicio_idServicio) values(?,?,?,?,?,?,?,?)", array($path.'1', $path.'2', $path.'3', $path.'4',$json->termino, $json->latitud, $json->longitud, $json->idServicio));
$entradas = array(
'RutaFoto1' => $path.'1',
'RutaFoto2' => $path.'2',
'RutaFoto3' => $path.'3',
'RutaFoto4' => $path.'4',
'FechaTermino' => $json->termino,
'Latitud' => $json->latitud,
'Longitud' => $json->longitud,
'Servicio_idServicio' => $json->idServicio
);
Detalle_Servicio::create($entradas);
$array = array('Code' => '202', 'Message' => 'Done');
return Response::json($array);
} else {
$array = array('Code');
return Response::json('error', 400);
}
});
You can see that I get a JSON containing the values ​​that I store in the database
I keep the data in the database of the Detalle_Servicio table but then I need to save some data in Material _Usado but I need the ID that was generated when you save the data in the table Detalle_Servicio
Model
class Detalle_Servicio extends Eloquent{
protected $table = 'Detalle_Servicio';
protected $primaryKey = 'idDetalle_Servicio';
protected $fillable = array('RutaFoto1', 'RutaFoto2', 'RutaFoto3', 'RutaFoto4', 'FechaTermino', 'Latitud', 'Longitud', 'Servicio_idServicio');
public function servicio(){
return $this->belongsTo('Servicio', 'idServicio'); //le pertenece a
}
public function material_usado(){
return $this->hasMany('Material_Usado', 'idMaterial_Usado');
}
}
and
class Material_Usado extends Eloquent{
protected $table = 'Material_Usado';
protected $primaryKey = 'idMaterial_Usado';
public function detalleServicio(){
return $this->belongsTo('Detalle_Servicio', 'idDetalle_Servicio');
}
}
how can I do it?
When you use this:
Detalle_Servicio::create($entradas);
It returns the model instance that was just created so you should do it like this way:
$Detalle_Servicio = Detalle_Servicio::create($entradas);
Now you can get the id of the created model using:
$Detalle_Servicio->id;
So, you may do something like this:
if($Detalle_Servicio = Detalle_Servicio::create($entradas)) {
$id = $Detalle_Servicio->id;
// ...
}
I don't really understand why you have this function in routes.php. Unless this is for example only...then you should have this a controller which will save the models in order.
First save the first model and then simply retrieve the foreign key that you need using something like this:
$serviceDetail = new Detalle_Servicio;
$serviceDetail->rutaFoto1 = Input::get('upload_success1');
etc..
$serviceDetail->save();
if ($serviceDetail->id)
{
$serviceDetail->material_usado()->attach($serviceDetail->id);
}
hope I understood the question :)

How do you remove specific fields from all entities of a record set in Lithium?

I am using MySQL as the database connection adapter for all my models. I have a downloads model and controller with an index function that renders either an HTML table or a CSV file depending on the type passed from the request. I also have a CSV media type to handle an array of data, which is working as expected (outputs array keys as headers then array values for each row of data).
I wish to do the same find query but then remove ID fields from the record set if a CSV file is going to be rendered. You'll notice that the download ID is being fetched even though it is not in the fields array, so simply changing the fields array based on the request type will not work.
I have tried the following in the index action of my downloads controller:
<?php
namespace app\controllers;
use app\models\Downloads;
class DownloadsController extends \lithium\action\Controller {
public function index() {
// Dynamic conditions
$conditions = array(...);
$downloads = Downloads::find('all', array(
'fields' => array('user_id', 'Surveys.name'),
'conditions' => $conditions,
'with' => 'Surveys',
'order' => array('created' => 'desc')
));
if ($this->request->params['type'] == 'csv') {
$downloads->each(function ($download) {
// THIS DOES NOT WORK
unset($download->id, $download->user_id);
// I HAVE TRIED THIS HERE AND THE ID FIELDS STILL EXIST
// var_dump($download->data());
// exit;
return $download;
});
return $this->render(array('csv' => $downloads->to('array')));
}
return compact('downloads');
}
}
?>
I thought there was an __unset() magic method on the entity object that would be called when you call the standard PHP unset() function on an entity's field.
It would be great if there was a $recordSet->removeField('field') function, but I can not find one.
Any help would be greatly appreciated.
Perhaps you should do $downloads = $downloads->to('array');, iterate the array with a for loop, remove those fields from each row, then return that array. If you have to do this same thing for a lot of actions, you could setup a custom Media handler that could alter the data without needing logic for it in your controller.
Take a look at this example in the Lithium Media class unit test.
You can also avoid having much logic for it in your controller at all through the use of a custom handler. This example also auto-generates a header row from the keys in your data.
In config/bootstrap/media.php:
Media::type('csv', 'application/csv', array(
'encode' => function($data, $handler, $response) {
$request = $handler['request'];
$privateKeys = null;
if ($request->privateKeys) {
$privateKeys = array_fill_keys($request->privateKeys, true);
}
// assuming your csv data is the first key in
// the template data and the first row keys names
// can be used as headers
$data = current($data);
$row = (array) current($data);
if ($privateKeys) {
$row = array_diff_key($row, $privateKeys);
}
$headers = array_keys($row);
ob_start();
$out = fopen('php://output', 'w');
fputcsv($out, $headers);
foreach ($data as $record) {
if (!is_array($record)) {
$record = (array) $record;
}
if ($privateKeys) {
$record = array_diff_key($record, $privateKeys);
}
fputcsv($out, $record);
}
fclose($out);
return ob_get_clean();
}
));
Your controller:
<?php
namespace app\controllers;
use app\models\Downloads;
class DownloadsController extends \lithium\action\Controller {
public function index() {
$this->request->privateKeys = array('id', 'user_id');
// Dynamic conditions
$conditions = array(...);
$downloads = Downloads::find('all', array(
'fields' => array('user_id', 'Surveys.name'),
'conditions' => $conditions,
'with' => 'Surveys',
'order' => array('created' => 'desc')
));
return compact('downloads');
}
}
?>
Why not then just dynamically set your $fields array?
public function index() {
$type = $this->request->params['type'];
//Exclude `user_id` if request type is CSV
$fields = $type == 'csv' ? array('Surveys.name') : array('user_id', 'Surveys.name');
$conditions = array(...);
$with = array('Surveys');
$order = array('created' => 'desc');
$downloads = Downloads::find('all', compact('conditions', 'fields', 'with', 'order'));
//Return different render type if CSV
return $type == 'csv' ? $this->render(array('csv' => $downloads->data())) : compact('downloads');
}
You can see in this example how I send the array for your CSV handler, otherwise it's the $downloads RecordSet object that goes to the view.

Categories