jrdavidson's avatar

Another Shot at Clarity

I wanted to make a new post about describing what I’m trying to achieve and help explain the parts of the app that are pertinent to my goal. This application has Properties, Rooms (Kitchen, Living Room, Bedroom, etc.), A Property can have many types of Rooms. When an inspector goes to a property they can make observations about each room in the property. Observations can be either a Test (different types of tests are Mold, Air, etc.), an Image (pictures taken of the room during the observation), Comment (further text describing the observation of the room).

What I'm trying to achieve is figuring out how I save an image for a property_room.

My current database structure is the following.

PROPERTY_ROOM
id
property_id
room_id

OBSERVATIONS
id
property_room_id
observation_id
observation_type

OBSERVATION_PHOTOS
OBSERVATION_TESTS
OBSERVATION_COMMENTS

I have the following models to help with this.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Relations\Pivot;

class PropertyRoom extends Pivot
{
    //
}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class ObservationImage extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['url', 'sort_order'];

    /**
     * Get the observation associated to the image.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function observation()
    {
        return $this->morphedToMany(Observation::class, 'observable');
    }
}

The following test I’m trying to upload an image to be assigned to a property_room but will also be saved as a morphed type for the observations table.

/** @test */
    public function an_administrator_can_upload_an_image_as_an_observation()
    {
        $this->withoutExceptionHandling();
        Storage::fake('s3');
        $file = UploadedFile::fake()->image('observation.jpg');

        $signedInUser = factory(User::class)->states('administrator')->create();
        $observation = factory(Observation::class)->create();

        $response = $this->actingAs($signedInUser)->json('POST', '/observations/'.$observation->id.'/images', $this->validParams([
            'image' => $file,
        ]));

        $response->assertStatus(200);
        tap($observation->images->first(), function ($image) use ($observation, $file) {
            dd($image->observation);
            $this->assertEquals('observations/'.$observation->id.'/'.$file->hashName(), $image->url);
            $this->assertEquals(1, $image->sort_order);
        });
    }

The code needs modified to work toward the desired solution.

<?php

namespace App\Http\Controllers;

use App\Models\Observation;
use App\Jobs\ProcessObservation;
use App\Http\Requests\StoreObservationImageRequest;
use App\Http\Resources\Observation as ObservationResource;

class ObservationImagesController extends Controller
{
    /**
     * @param  \App\Http\Requests\StoreObservationImageRequest  $request
     * @param  \App\Models\Observation  $observation
     * @return
     */
    public function store(StoreObservationImageRequest $request, Property $property, Room $room)
    {
        $path = request()->file('image')->store('property-rooms/images');

        $file = Storage::disk('s3')->put('property-rooms/', request()->image, 'public');

        $max = $this->observation->images()->max('sort_order');

        $this->observation->images()->create([
            'url' => $file,
            'sort_order' => $max + 1,
        ]);

        return new ObservationResource($observation);
    }
}
0 likes
8 replies
jrdavidson's avatar

I hope this time around I have been to clarify any misunderstanding from the previous post. If there is anything that could use further clarification please tag team and I will do my best to explain.

jlrdw's avatar

What I'm trying to achieve is figuring out how I save an image for a property_room

Probably using the property_room id in the child table where you store the image names.

Like the docs post example: a post has an id, and the child table has post_id as fk.

Study up on the various relationship examples, I am not sure if it's a

  • Has Many Through
  • many to many

You need. I myself try to reduce all down to at most a one to many, even if it means at times one extra query to get top record.

See http://laravel.io/forum/05-12-2015-has-many-through-relationship-depth

Snapey's avatar

Use spatie media library. It makes it dead easy to attach images to any model just using traits. No changes are required to your models to implement other than to include the trait.

jrdavidson's avatar

@jlrdw and @Snapey I still haven't been able to get this issue fixed and would like to revisit my post to seek further guidance.

I am attempting to pass the test below. I have included the Models associated with this test as well as the controller that is hit from the endpoint to save a new observation for the property room. I have not included the route because I can guarantee that the endpoint DOES hit the store method for the ObservationsController.

With the current code provided below I am getting a General error: 1 no such column observations.

I don't know why it is looking for observations as a column when its a relationship method.

To help explain what is supposed happen.

  1. A business unit when created has copies of the default rooms added to their business unit.
  2. When a business unit creates a new property it takes the rooms assigned to that business unit and attaches them to the property.
  3. Then an authorized user can go into a room at that property and create an observation for that room through a comment about the room, a test on the room, or take a photo of the room or a combination of any of those.

My main focus is getting my relationships correct for the following models:Property, Room, Observation, and PropertyRoom solving this problem I'm certain will solve my current issue about the no such column observations.

Changes to DB structure

PROPERTIES

  • id
  • address
  • ...

ROOMS

  • id
  • name
  • ...

PROPERTY_ROOM

  • id
  • property_id
  • room_id
  • room_name

OBSERVATIONS

  • id
  • property_room_id
  • sort_order
/** @test */
    public function an_administrator_can_create_a_new_observation_for_a_room_at_a_property_belonging_to_their_business_unit()
    {
        // $this->withoutExceptionHandling();
        $signedInUser = factory(User::class)->states('administrator')->create();
        $property = factory(Property::class)->create(['business_unit_id' => $signedInUser->businessUnit->id]);
        $property->rooms()->attach($property->businessUnit->rooms()->first()->id, ['room_name' => Room::default()->first()->name]);

        $response = $this->actingAs($signedInUser)->json('POST', '/properties/'.$property->id.'/rooms/'.$property->rooms->first()->id.'/observations', []);

        $response->assertStatus(201);
        $this->assertCount(1, $property->rooms->first()->observations);
    }
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Relations\Pivot;

class PropertyRoom extends Pivot
{
    protected $table = 'property_room';

    protected $fillable = ['property_id', 'room_id', 'room_name'];

    /**
     * Get the rooms that are assigned to a property.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function property()
    {
        return $this->belongsTo(Property::class);
    }

    /**
     * Get the rooms that are assigned to a property.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function room()
    {
        return $this->belongsTo(Room::class);
    }

    /**
     * Get the observations that are assigned to a property room.
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function observations()
    {
        return $this->hasMany(Observation::class);
    }
}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Room extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name', 'is_active', 'uses_wysiwyg', 'protocol', 'creator_id'];


    /**
     * Get the business unit that belongs to the room.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function businessUnit()
    {
        return $this->belongsTo(BusinessUnit::class);
    }

    /**
     * Get the properties that are assigned to the room.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
     */
    public function properties()
    {
        return $this->belongsToMany(Property::class, 'property_room', 'room_id', 'property_id')->using(PropertyRoom::class)->withPivot(['room_name']);
    }
}
<?php

namespace App\Models;

use App\Models\PropertyRoom;
use Illuminate\Database\Eloquent\Model;

class Property extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['business_unit_id', 'customer_id', 'appointment_date', 'notes', 'main_photo_url', 'office_phone', 'inspector_id', 'creator_id'];

    /**
     * The attributes that should be mutated to dates.
     *
     * @var array
     */
    protected $dates = ['appointment_date'];

    /**
     * Get the business unit associated to the property.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function businessUnit()
    {
        return $this->belongsTo(BusinessUnit::class);
    }

    /**
     * Get the rooms that are assigned to a property.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
     */
    public function rooms()
    {
        return $this->belongsToMany(Room::class, 'property_room', 'property_id', 'room_id')->using(PropertyRoom::class)->withPivot(['room_name']);
    }
}
<?php

namespace App\Http\Controllers;

use App\Models\Room;
use App\Models\Property;
use App\Models\Observation;
use App\Models\PropertyRoom;
use App\Http\Requests\StoreObservationRequest;
use App\Http\Resources\Observation as ObservationResource;

class ObservationsController extends Controller
{
    /**
     *
     * @param  \App\Http\Requests\StoreObservationRequest  $request
     * @param  \App\Models\Property  $property
     * @param  \App\Models\Room  $room
     * @return \App\Http\Resources\Observation
     */
    public function store(StoreObservationRequest $request, Property $property, Room $room)
    {
        $propertyRoom = PropertyRoom::where(['property_id' => $property->id, 'room_id' => $room->id])->first();
        dd($propertyRoom->observations);
        $max = $propertyRoom->observations()->max('sort_order');

        $observation = $propertyRoom->observations()->create([
            'sort_order' => $max + 1,
            'creator_id' => auth()->user()->id,
        ]);

        return new ObservationResource($observation);
    }
}
MikeMacDowell's avatar

@XTREMER360 - You can't do this

$propertyRoom = PropertyRoom::where(['property_id' => $property->id, 'room_id' => $room->id])->first();

As PropertyRoom extends Pivot, which can't be instantiated in this way.

Make PropertyRoom a full Model if you want to have access to it without going through either of its parents. Obviously you'll lose the easiness of a ManyToMany Relationship between Property and Room, but having PropertyRoom as a full Model makes it easier to deal with if you're trying to attach Observations to it and treat it like a 1st class citizen (which it appears you are by your code).

jrdavidson's avatar

@mikemacdowell So if I remove that line as suggested and make PropertyRoom its own model. Would I need to do any other changes to make this code work?

I make those suggested changes and updated the $property->rooms relationship to remove the using(PropertyRoom::class) and the same for the $room->property relationship as well.

I have also changed inside the controller to do the following but it says it says its a call to an undefined method BelongsToMany::observations but not sure why.

$max = $property->rooms()->observations()->max('sort_order');
MikeMacDowell's avatar

@XTREMER360 - Yes, your relationships for Room and Property would have to change.

class Room extends Model
{
    /**
     * Get the properties that are assigned to the room.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
     */
    public function propertyRooms()
    {
        return $this->hasMany(PropertyRoom::class, 'room_id');
    }
}
class Property extends Model
{
    /**
     * Get the properties that are assigned to the room.
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
     */
    public function propertyRooms()
    {
        return $this->hasMany(PropertyRoom::class, 'property_id');
    }

And then you'd have to modify the test to something like:

/** @test */
    public function an_administrator_can_create_a_new_observation_for_a_room_at_a_property_belonging_to_their_business_unit()
    {
        // $this->withoutExceptionHandling();
        $signedInUser = factory(User::class)->states('administrator')->create();
        $property = factory(Property::class)->create(['business_unit_id' => $signedInUser->businessUnit->id]);
        $property->propertyRooms()->create([
        'room_id' => $property->businessUnit->rooms()->first()->id,
        'room_name' => Room::default()->first()->name
    ]);

        $response = $this->actingAs($signedInUser)->json('POST', '/properties/'.$property->id.'/rooms/'.$property->propertyRooms->first()->room_id.'/observations', []);

        $response->assertStatus(201);
        $this->assertCount(1, $property->rooms->first()->observations);
    }

Those changes would make it work I think.

However I'd consider just passing through the ID of the PropertyRoom to the controller rather than the Property and Room IDs.

Personally I'd rename PropertyRoom to just Room, and Room to RoomType. Because your "Room" model currently describes a type of room, not an actual, physical room, whereas PropertyRoom is currently the physical room on which Observations are being made.

1 like
jrdavidson's avatar

I'll do some changes and circle back and update on this conversation.

1 like

Please or to participate in this conversation.