I'm developing an application for publishing and consulting real estate rents, using Laravel 9 with Jetstream.
Since I'm pretty new to Laravel, I'm following a tutorial in Youtube (in Spanish) which explains the usage of this framework from zero. That tutorial, it first explains how to make a basic CRUD into a table, using Eloquent ORM. It shows there, how to use a form with it's correspondent data validation, to insert a new register in the correspondent table of the database, with the data received from the form, using mass assignment.
Later, in subsequent chapters (videos), explains how to make the database design. It starts explaining how to make the design of the conceptual model, by drawing an ER diagram. Then, it explains how to map that ER diagram into a relational model, with an EER diagram on MySql Workbench, by placing the correspondent tables with it's correspondent foreign keys, and by placing a pivot table if necessary (for n:m cardinality), and also how to make polimorphic relationships. Finally, it explains how to implement that relational model in Eloquent ORM, by adding the correspondent foreign key constraints, and by coding the methods 'hasOne','hasMany','belongsTo', and those of that type, into Eloquent models.
So then, I've started to design the database relational model on MySql Workbench. In my design, I have a table named 'alquileres' (rents), with every building published for being rented. Each one these publications has an user associated, which is who posted that offer. Also, beside other fields, it has and an address, which is a multivalued field. The address contains the correspondent jurisdictions where the building is placed (something like district and borough, since I live in Argentina I have jurisdictions according to this country). Also, it contains the correspondent street name, the address number, and the department number (if needed). So then, the address fields are stored into a separated table named 'direccion' (address).
The issue is this one: At the very begining of my project, I've started to code it just by following the tutorial steps. Then, I had a form in which I've included just a few fields, corresponding to a rent publication. With that form, I could add a new register in a table, without considering any relationship, since I coded it just for testing. Until that moment, I could use mass assignment without any problem. However, when I started to implement a little more complexity in the database, by making table relationships, I couldn't use mass assignment anymore, since I couldn't found a way to make Eloquent recognize automatically the fields corresponding at the address, which are sent in the form. Something similar happened when I tried to associate the posted offer with the user who published it (it's always the user who is logged in, with Jetstream).
At the controller, in the 'store' method, I previously had this line which worked fine, to make mass assignment with the data recieved from the form:
$alquiler = Alquiler::create($request->all());
However, when I tried to implement the multivalued attribute (the address), and the correspondent user association, I couldn't do that anymore.
Now, my code of the 'store' method in controller ('AlquilerController.php') it's like this:
//I have a form request called 'StoreAlquiler', which just validates every field as required
public function store(StoreAlquiler $request){
$alquiler = Alquiler::create([
'formal' => $request->formal,
'slug' => $request->calle,
'tipo_inmueble' => $request->tipo_inmueble,
'descripcion' => $request->descripcion,
'user_id'=> auth()->user()->id
]);
$direccion = Direccion::create([
'provincia' => $request->provincia,
'departamento_partido' => $request->departamento_partido,
'municipio' => $request->municipio,
'barrio_localidad' => $request->barrio_localidad,
'calle' => $request->calle,
'altura' => $request->altura,
'departamento_piso' => $request->departamento_piso,
'alquiler_id' => $alquiler->id
]);
//Here's the line I had previously
//$alquiler = Alquiler::create($request->all());
return redirect()->route('alquileres.show',$alquiler);
}
My code of the form ('create.blade.php') is this one:
@extends('layouts.plantilla')
@section('title','Vista principal')
@section('content')
<h1>Bienvenido a la vista para crear</h1>
<form action="{{ route('alquileres.store')}}" method="POST">
@csrf
<label>
Formalidad:
<br>
<select name="formal" value="{{old('formal',"formal")}}">
<option value="formal">Formal</option>
<option value="informal">Informal</option>
</select>
</label>
@error('formal')
<br>
<small>*{{$message}}</small>
<br>
@enderror
<br>
<label>
Tipo inmueble:
<br>
<input type="text" name="tipo_inmueble" value="{{old('tipo_inmueble')}}">
</label>
@error('tipo_inmueble')
<br>
<small>*{{$message}}</small>
<br>
@enderror
<br>
<label>
Descripción:
<br>
<textarea name="descripcion" rows=15>{{old('descripcion')}}</textarea>
</label>
@error('descripcion')
<br>
<small>*{{$message}}</small>
<br>
@enderror
<br>
<label>
Provincia:
<br>
<input type="text" name="provincia" value="{{old('provincia')}}">
</label>
@error('provincia')
<br>
<small>*{{$message}}</small>
<br>
@enderror
<br>
<label>
Departamento/Partido:
<br>
<input type="text" name="departamento_partido" value="{{old('departamento_partido')}}">
</label>
@error('departamento_partido')
<br>
<small>*{{$message}}</small>
<br>
@enderror
<br>
<label>
Municipio:
<br>
<input type="text" name="municipio" value="{{old('municipio')}}">
</label>
@error('municipio')
<br>
<small>*{{$message}}</small>
<br>
@enderror
<br>
<label>
Barrio/Localidad:
<br>
<input type="text" name="barrio_localidad" value="{{old('barrio_localidad')}}">
</label>
@error('barrio_localidad')
<br>
<small>*{{$message}}</small>
<br>
@enderror
<br>
<label>
Calle:
<br>
<input type="text" name="calle" value="{{old('calle')}}">
</label>
@error('calle')
<br>
<small>*{{$message}}</small>
<br>
@enderror
<br>
<label>
Altura:
<br>
<input type="number" name="altura" value="{{old('altura')}}">
</label>
@error('altura')
<br>
<small>*{{$message}}</small>
<br>
@enderror
<br>
<label>
Departamento y Piso:
<br>
<input type="text" name="departamento_piso" value="{{old('departamento_piso')}}">
</label>
@error('departamento_piso')
<br>
<small>*{{$message}}</small>
<br>
@enderror
<br>
<button type="submit">Enviar formulario</button>
</form>
@endsection()
As you can see, in the form, all address fields are sent individually (without any grouping).
Then, I have this view, which shows a posted rent offer, within a database register ('show.blade.php'):
@extends('layouts.plantilla')
@section('title','Mostrar ' . $alquiler->id)
@section('content')
<h1>Muestro {{$alquiler->id}}</h1>
<a href={{route('alquileres.index')}}>Volver a la lista de alquileres</a>
<br>
<a href={{route('alquileres.edit',$alquiler)}}>Editar alquiler</a>
<p><strong>Formalidad: </strong> {{$alquiler->formal}} </p>
<p><strong>Tipo: </strong> {{$alquiler->tipo_inmueble}} </p>
<p><strong>Descripcion: </strong> {{$alquiler->descripcion}} </p>
<h2>Direccion</h2>
<p><strong>Provincia: </strong> {{$alquiler->direccion->provincia}} </p>
<p><strong>Departamento/Partido: </strong> {{$alquiler->direccion->departamento_partido}} </p>
<p><strong>Municipio: </strong> {{$alquiler->direccion->municipio}} </p>
<p><strong>Barrio/Localidad: </strong> {{$alquiler->direccion->barrio_localidad}} </p>
<p><strong>Calle: </strong> {{$alquiler->direccion->calle}} </p>
<p><strong>Altura: </strong> {{$alquiler->direccion->altura}} </p>
<p><strong>Departamento/Piso: </strong> {{$alquiler->direccion->departamento_piso}} </p>
<form action="{{route('alquileres.destroy',$alquiler)}}" method="POST">
@csrf
@method('delete')
<button type="submit">Eliminar alquiler</button>
</form>
@endsection()
This works fine, since allows me to associate the published offer with the user who's logged in, and storing the address fields into a separated table. Then, I can access the fields that correspondent to the address by doing, for example:
$alquiler->direccion->calle
//In english: $rent->address->street
Despite this works, I found it a little bit sloppy. I could achieve this after a lot of try and failure. However, I think there must be a better way to do the same, since I'm sending a lot of fields in the form. I wonder if there's a way to use mass assignment to achive the same result, since now I'm having nearly 15 lines of code in the 'store' method at the controller when I previously had only one.
Does anyone knows if there's a better way to achive the same result, or if there's a better solution for this issue? I'd need to do something similar with the 'update' method in the controller, beside the 'store' method.
I add the MySql Workbench EER diagram I have until now to show the design:
It will be much more complex, including also polimorphic relationships. However, until now I'm just trying to make a relationship between just only three tables: 'users', 'alquileres' (rents) and 'direccion' (address).
My code on the migrations is this one:
'...create_users_table.php' (comes directly implemented with Laravel Jetstream):
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->foreignId('current_team_id')->nullable();
$table->string('profile_photo_path', 2048)->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
}
};
'...alquileres.php':
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('alquileres', function (Blueprint $table) {
$table->id();
$table->string('slug');
$table->string('formal');
$table->string('tipo_inmueble');
$table->text('descripcion');
$table->unsignedBigInteger('user_id');
$table->foreign('user_id')->references('id')->on('users');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('alquileres');
}
};
'...direccion.php':
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('direccion', function (Blueprint $table) {
$table->id();
$table->string('provincia');
$table->string('departamento_partido');
$table->string('municipio');
$table->string('barrio_localidad');
$table->string('calle');
$table->integer('altura');
$table->string('departamento_piso');
$table->unsignedBigInteger('alquiler_id');
$table->foreign('alquiler_id')->references('id')->on('alquileres');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('direccion');
}
};
Then, my code of the models is this one:
'User.php':
<?php
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Jetstream\HasProfilePhoto;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens;
use HasFactory;
use HasProfilePhoto;
use Notifiable;
use TwoFactorAuthenticatable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
'two_factor_recovery_codes',
'two_factor_secret',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
/**
* The accessors to append to the model's array form.
*
* @var array<int, string>
*/
protected $appends = [
'profile_photo_url',
];
public function alquiler () {
return $this->hasOne('App/Models/Alquiler');
}
}
'Alquiler.php':
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
class Alquiler extends Model
{
use HasFactory;
protected $guarded = [];
protected $table = 'alquileres';
//Here I had also an issue with the slug, since it uses data placed in the address table
//Since it deserves another question just for it, until now we just comment this method, and don't use any slug
/*
public function getRouteKeyName()
{
return 'slug';
}
*/
public function user () {
return $this->belongsTo('App\Models\User','user_id');
}
public function direccion () {
return $this->hasOne('App\Models\Direccion','alquiler_id');
}
}
'Direccion.php':
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Direccion extends Model
{
use HasFactory;
//Pero nos conviene hacerlo mediante $guarded
protected $guarded = [];
//Como tenemos el nombre de la tabla en plural y en español (Laravel no lo reconoce automáticamente), lo asociamos con la siguiente línea
protected $table = 'direccion';
public function alquiler () {
return $this->belongsTo('App\Models\Alquiler', 'alquiler_id');
}
}
Finally, my code in the 'web.php' for routing the form and the controller is this one (just one line):
Route::resource('alquileres',AlquilerController::class)->parameters(['alquileres' => 'alquiler']);
Here is a post with a similar issue, which says that's impossible to use mass assignment in a case like this one: Laravel saving mass assignment with a morphMany relation
However it's quite old (from 8 years ago), and I think that it must be a tidier or better way to do this, even without using mass assignment.
If there's any more information needed, or there's something in english that is not well understood (I'm not a native speaker), please tell me.
Thanks a lot! Greetings from Argentina
EDIT:
I'm posting my code of the 'update' method at the controller, which I've written it after the 'store' one.
Despite I could make it work after a lot of attempts, I find it pretty ugly, even worse than my 'store' method. I've left some code that I tried before as comments, just for showing the kind of things I've been trying.
I wonder if some of the commented code, it would be a better approach to achieve the same result. At first, when I've tried some of that code, it added a new 'direccion' (address) register, instead of updating the existing one. I've solved that issue by using the 'where' method.
In the other hand, since I can access the 'direccion' fields by doing, for example:
$alquiler->direccion->calle
I thought I could do something similar to create or update those fields, using then a nested PHP array (the way that PHP handles a JSON object).
So then, here's my 'update' code:
//I've both the 'Alquiler' and 'Direccion' models imported above in the controller
//I previously had this at the method definition:
//public function update(Request $request, Alquiler $alquiler,Direccion $direccion) {
public function update(Request $request, Alquiler $alquiler){
$request->validate([
'formal' => 'required',
'tipo_inmueble' => 'required',
'descripcion' => 'required|min:10',
'provincia' => 'required',
'departamento_partido' => 'required',
'municipio' => 'required',
'barrio_localidad' => 'required',
'calle' => 'required',
'altura' => 'required',
'departamento_piso' => 'required'
]);
$alquiler->formal = $request->formal;
$alquiler->slug = $request->calle;
$alquiler->tipo_inmueble = $request->tipo_inmueble;
$alquiler->descripcion = $request->descripcion;
$alquiler->user_id= auth()->user()->id;
/*
$alquiler->direccion->provincia = $request->provincia;
$alquiler->direccion->departamento_partido = $request->departamento_partido;
$alquiler->direccion->municipio = $request->municipio;
$alquiler->direccion->barrio_localidad = $request->barrio_localidad;
$alquiler->direccion->calle = $request->calle;
$alquiler->direccion->altura = $request->altura;
$alquiler->direccion->departamento_piso = $request->departamento_piso;
*/
//$direccion->alquiler_id = $alquiler->id;
//$direccion->save();
//$alquiler->direccion=$direccion;
//$direccion->alquiler_id=$alquiler->id;
$direccion = Direccion::where('alquiler_id',$alquiler->id)->first();
$direccion->provincia = $request->provincia;
$direccion->departamento_partido = $request->departamento_partido;
$direccion->municipio = $request->municipio;
$direccion->barrio_localidad = $request->barrio_localidad;
$direccion->calle = $request->calle;
$direccion->altura = $request->altura;
$direccion->departamento_piso = $request->departamento_piso;
//$direccion->alquiler()->save($alquiler);
//$direccion->alquiler()->associate($alquiler);
$direccion->save();
//$alquiler->save();
//$alquiler->direccion()->save($direccion);
$alquiler->save();
//$direccion->save();
//Here's the mass assignment I had previously.
//$asignacion=$request->all();
//This line previously generated the 'slug' field when 'direccion' was only a string field, instead of a table containing a multivalued attribute
//$asignacion['slug'] = Str::slug($request->direccion,'-');
//$alquiler->update($asignacion);
//Here's a line I've used just for testing, which showed me the content
//of $alquiler in the browser, when I made the form submit:
//return $alquiler;
return redirect()->route('alquileres.show',$alquiler);
}
Here's the links of some of the Laracasts and StackOverflow post I've consulted:
https://laracasts.com/discuss/channels/laravel/update-hasone-relationship
https://laracasts.com/discuss/channels/eloquent/call-to-undefined-method-associate-method
Here's the documentation page which the last StackOverflow post linked refers:
https://laravel.com/docs/10.x/eloquent-relationships#inserting-and-updating-related-models
If it's useful, here's my 'StoreAlquiler.php' Form Request:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str;
class StoreAlquiler extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
//Again, I've commented the code for the slug, since I'm not using any slug at the moment.
//I'll try to resolve that issue later
/*
protected function prepareForValidation()
{
$this->merge([
'slug' => Str::slug($this->calle,'-')
]);
}
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'formal' => 'required',
'tipo_inmueble' => 'required',
'descripcion' => 'required|min:10',
'provincia' => 'required',
'departamento_partido' => 'required',
'municipio' => 'required',
'barrio_localidad' => 'required',
'calle' => 'required',
'altura' => 'required',
'departamento_piso' => 'required'
];
}
public function attributes()
{
return [
'tipo' => 'tipo de inmueble',
'descripcion' => 'descripcion del inmueble'
];
}
public function messages()
{
return [
'barrio.required' => 'Debe especificar el barrio',
'barrio.max:20' => 'El nombre del barrio es muy largo, de mas de 20 caracteres',
];
}
}