Ok, now our users can manage activities. Time to show them on the public front-facing website, starting with the homepage.
Of course, in a real-life scenario, we would need a visual design here, but HTML/CSS things are outside this course's scope. We will stick to the default Laravel Breeze design and build a grid view here. As "homework", you can try to find and apply some Tailwind/Bootstrap theme instead or create a custom design.
This is what we'll build in this lesson:
Here's the list of topics that we'll cover below:
- Modifying Breeze layout for quickly building frontend features.
- Creating a thumbnail image for the activity.
- Showing activities on the homepage and showing the activity itself.
- Adding dummy data so the client can better see how the homepage will look.
- Writing tests.
Modifying Breeze Layout
Before using Breeze Layout for the homepage, we must make it work for non-authenticated guest users.
First, create a new invokable HomeController
and rename the resources/views/dashboard.blade.php
into resources/views/home.blade.php
.
php artisan make:controller HomeController --invokable
app/Http/Controllers/HomeController.php:
class HomeController extends Controller{ public function __invoke() { return view('home'); }}
Next, we need to change the Routes to use HomeController
.
routes/web.php:
use App\Http\Controllers\HomeController; Route::get('/', function () { return view('welcome');}); Route::get('/dashboard', function () { return view('dashboard');})->middleware(['auth', 'verified'])->name('dashboard'); Route::get('/', HomeController::class)->name('home'); // ...
Because we removed the dashboard
route, we need to change this route to home
route instead everywhere.
app/Http/Controllers/Auth/AuthenticatedSessionController.php:
class AuthenticatedSessionController extends Controller{ // ... public function store(LoginRequest $request): RedirectResponse { $request->authenticate(); $request->session()->regenerate(); return redirect()->intended(route('dashboard', absolute: false)); return redirect()->intended(route('home', absolute: false)); } // ...}
app/Http/Controllers/Auth/ConfirmablePasswordController.php:
class ConfirmablePasswordController extends Controller{ // ... public function store(Request $request): RedirectResponse { if (! Auth::guard('web')->validate([ 'email' => $request->user()->email, 'password' => $request->password, ])) { throw ValidationException::withMessages([ 'password' => __('auth.password'), ]); } $request->session()->put('auth.password_confirmed_at', time()); return redirect()->intended(route('dashboard', absolute: false)); return redirect()->intended(route('home', absolute: false)); }}
app/Http/Controllers/Auth/RegisteredUserController.php:
class RegisteredUserController extends Controller{ // ... public function store(Request $request): RedirectResponse { // ... return redirect()->intended(route('dashboard', absolute: false)); return redirect()->intended(route('home', absolute: false)); }}
app/Http/Controllers/Auth/VerifyEmailController.php:
class VerifyEmailController extends Controller{ public function __invoke(EmailVerificationRequest $request): RedirectResponse { if ($request->user()->hasVerifiedEmail()) { return redirect()->intended(route('home', absolute: false).'?verified=1'); } if ($request->user()->markEmailAsVerified()) { event(new Verified($request->user())); } return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); return redirect()->intended(route('home', absolute: false).'?verified=1'); }}
And in the navigation, besides changing the route name, we need to wrap links only for authenticated users with the @auth
Blade directive.
resources/views/layouts/navigation.blade.php:
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100"> <!-- Primary Navigation Menu --> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="flex justify-between h-16"> <div class="flex"> <!-- Logo --> <div class="shrink-0 flex items-center"> <a href="{{ route('dashboard') }}"> <a href="{{ route('home') }}"> <x-application-logo class="block h-9 w-auto fill-current text-gray-800" /> </a> </div> <!-- Navigation Links --> <div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex"> <x-nav-link :href="route('home')" :active="request()->routeIs('home')"> {{ __('Dashboard') }} </x-nav-link> @auth @if(auth()->user()->role_id === \App\Enums\Role::ADMINISTRATOR->value) <x-nav-link :href="route('companies.index')" :active="request()->routeIs('companies.index')"> {{ __('Companies') }} </x-nav-link> @endif @if(auth()->user()->role_id === \App\Enums\Role::COMPANY_OWNER->value) <x-nav-link :href="route('companies.users.index', auth()->user()->company_id)" :active="request()->routeIs('companies.users.*')"> {{ __('Administrators') }} </x-nav-link> <x-nav-link :href="route('companies.guides.index', auth()->user()->company_id)" :active="request()->routeIs('companies.guides.*')"> {{ __('Guides') }} </x-nav-link> <x-nav-link :href="route('companies.activities.index', auth()->user()->company_id)" :active="request()->routeIs('companies.activities.*')"> {{ __('Activities') }} </x-nav-link> @endif @endauth </div> </div> <!-- Settings Dropdown --> <div class="hidden sm:flex sm:items-center sm:ml-6"> @auth <x-dropdown align="right" width="48"> <x-slot name="trigger"> <button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150"> <div>{{ Auth::user()->name }}</div> <div class="ml-1"> <svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" /> </svg> </div> </button> </x-slot> <x-slot name="content"> <x-dropdown-link :href="route('profile.edit')"> {{ __('Profile') }} </x-dropdown-link> <!-- Authentication --> <form method="POST" action="{{ route('logout') }}"> @csrf <x-dropdown-link :href="route('logout')" onclick="event.preventDefault(); this.closest('form').submit();"> {{ __('Log Out') }} </x-dropdown-link> </form> </x-slot> </x-dropdown> @else <x-nav-link :href="route('login')" :active="request()->routeIs('login')"> {{ __('Login') }} </x-nav-link> <x-nav-link :href="route('register')" :active="request()->routeIs('register')"> {{ __('Register') }} </x-nav-link> @endauth </div> <!-- Hamburger --> <div class="-mr-2 flex items-center sm:hidden"> <button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out"> <svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24"> <path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> <path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> </svg> </button> </div> </div> </div> <!-- Responsive Navigation Menu --> <div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden"> <div class="pt-2 pb-3 space-y-1"> <x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')"> <x-responsive-nav-link :href="route('home')" :active="request()->routeIs('home')"> {{ __('Dashboard') }} </x-responsive-nav-link> </div> <!-- Responsive Settings Options --> @auth <div class="pt-4 pb-1 border-t border-gray-200"> <div class="px-4"> <div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div> <div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div> </div> <div class="mt-3 space-y-1"> <x-responsive-nav-link :href="route('profile.edit')"> {{ __('Profile') }} </x-responsive-nav-link> <!-- Authentication --> <form method="POST" action="{{ route('logout') }}"> @csrf <x-responsive-nav-link :href="route('logout')" onclick="event.preventDefault(); this.closest('form').submit();"> {{ __('Log Out') }} </x-responsive-nav-link> </form> </div> </div> @endauth </div></nav>
Making Thumbnail for Activity
Every activity should have a thumbnail to be shown on the homepage.
We will use the intervention/image
package to make a thumbnail. Yes, we could use spatie/laravel-medialibrary,
but I think it would be overkill for only one image per activity for such a small project, at least for now.
composer require intervention/image
Because making thumbnail needs to be done both when creating and editing the activity, we can create a separate private method to avoid repeating the code.
So the Controller changes would be:
app/Http/Controllers/CompanyActivityController.php:
use Intervention\Image\ImageManager; class CompanyActivityController extends Controller{ // ... public function store(StoreActivityRequest $request, Company $company) { $this->authorize('create', $company); if ($request->hasFile('image')) { $path = $request->file('image')->store('activities', 'public'); } $filename = $this->uploadImage($request); $activity = Activity::create($request->validated() + [ 'company_id' => $company->id, 'photo' => $path ?? null, 'photo' => $filename, ]); $activity->participants()->sync($request->input('guides')); return to_route('companies.activities.index', $company); } // ... public function update(UpdateActivityRequest $request, Company $company, Activity $activity) { $this->authorize('update', $company); if ($request->hasFile('image')) { $path = $request->file('image')->store('activities', 'public'); if ($activity->photo) { Storage::disk('public')->delete($activity->photo); } } $filename = $this->uploadImage($request); $activity->update($request->validated() + [ 'photo' => $path ?? $activity->photo, 'photo' => $filename ?? $activity->photo, ]); return to_route('companies.activities.index', $company); } // ... private function uploadImage(StoreActivityRequest|UpdateActivityRequest $request): string|null { if (! $request->hasFile('image')) { return null; } $filename = $request->file('image')->store(options: 'activities'); $thumb = ImageManager::imagick()->read(Storage::disk('activities')->get($filename)) ->scaleDown(274, 274) ->toJpeg() ->toFilePointer(); Storage::disk('activities')->put('thumbs/' . $request->file('image')->hashName(), $thumb); return $filename; } }
As you can see, we are using a disk called activities
for file upload. We need to add this custom disk to the config/filesystems.php
.
config/filesystems.php:
return [ // ... 'disks' => [ // ... 'activities' => [ 'driver' => 'local', 'root' => storage_path('app/public/activities'), 'url' => env('APP_URL').'/storage', 'visibility' => 'public', ], // ... ], // ...];
Next, we must delete the image when a new one is uploaded to the edit page. I think Observer would be a perfect fit.
php artisan make:observer ActivityObserver
app/Models/Activity.php:
use App\Observers\ActivityObserver;use Illuminate\Database\Eloquent\Attributes\ObservedBy; #[ObservedBy(ActivityObserver::class)] class Activity extends Model{ // ...}
app/Observers/ActivityObserver.php:
use App\Models\Activity;use Illuminate\Support\Facades\Storage; class ActivityObserver{ public function updating(Activity $activity): void { if ($activity->isDirty('photo') && $activity->getOriginal('photo')) { Storage::disk('activities')->delete($activity->getOriginal('photo')); Storage::disk('activities')->delete('thumbs/' . $activity->getOriginal('photo')); } } public function deleting(Activity $activity): void { if ($activity->photo) { Storage::disk('activities')->delete($activity->photo); Storage::disk('activities')->delete('thumbs/' . $activity->photo); } }}
After uploading images, we also have thumbnail images in the activites/thumbs
directory.
Showing Activities on the Homepage
Now we can show activities on the homepage by paginating them and showing No activities
if there are none.
On the homepage, we will show upcoming activities and order them by start_time
in a simple 3x3 grid layout, 9 records per page.
app/Http/Controllers/HomeController.php:
use App\Models\Activity; class HomeController extends Controller{ public function __invoke() { $activities = Activity::where('start_time', '>', now()) ->orderBy('start_time') ->paginate(9); return view('home', compact('activities')); }}
resources/views/home.blade.php:
<x-app-layout> <x-slot name="header"> <h2 class="text-xl font-semibold leading-tight text-gray-800"> {{ __('Dashboard') }} </h2> </x-slot> <div class="py-12"> <div class="mx-auto max-w-7xl sm:px-6 lg:px-8"> <div class="overflow-hidden bg-white shadow-sm sm:rounded-lg"> <div class="p-6 text-gray-900"> <div class="grid grid-cols-4 gap-x-5 gap-y-8"> @forelse($activities as $activity) <div> <img src="{{ asset($activity->thumbnail) }}" alt="{{ $activity->name }}"> <h2> <a href="#" class="text-lg font-semibold">{{ $activity->name }}</a> </h2> <time>{{ $activity->start_time }}</time> </div> @empty <p>No activities</p> @endforelse </div> <div class="mt-6">{{ $activities->links() }}</div> </div> </div> </div> </div></x-app-layout>
This is what we will see:
In the Blade file, I used thumbnail
for showing thumbnails, but we don't have such a field in the DB. To display the thumbnail, we will use Accessor. Also, if there is no image for the activity, we will show a default image I took from the first Google result.
app/Models/Activity.php:
use Illuminate\Database\Eloquent\Casts\Attribute; class Activity extends Model{ // ... public function thumbnail(): Attribute { return Attribute::make( get: fn() => $this->photo ? '/activities/thumbs/' . $this->photo : '/no_image.jpg', ); }}
Now it is showing a thumbnail image or the default no_image.jpg
.
Show Activity
Now that we have the list of activities, we can make images and titles clickable to show the detail page for the activity. But first, we need a Controller and Route.
php artisan make:controller ActivityController
routes/web.php:
use App\Http\Controllers\ActivityController; Route::get('/', HomeController::class)->name('home');Route::get('/activities/{activity}', [ActivityController::class, 'show'])->name('activity.show'); // ...
For the Route, I am using Route Model Binding, meaning we can return a View and pass the activity to it in the Controller.
app/Http/Controllers/ActivityController.php:
use App\Models\Activity; class ActivityController extends Controller{ public function show(Activity $activity) { return view('activities.show', compact('activity')); }}
And in the Blade file, we just show all the information for now.
resources/views/activities/show.blade.php:
<x-app-layout> <x-slot name="header"> <h2 class="text-xl font-semibold leading-tight text-gray-800"> {{ $activity->name }} </h2> </x-slot> <div class="py-12"> <div class="mx-auto max-w-7xl sm:px-6 lg:px-8"> <div class="overflow-hidden bg-white shadow-sm sm:rounded-lg"> <div class="p-6 text-gray-900 space-y-3"> <img src="{{ asset($activity->thumbnail) }}" alt="{{ $activity->name }}"> <div>${{ $activity->price }}</div> <time>{{ $activity->start_time }}</time> <div>Company: {{ $activity->company->name }}</div> <p>{{ $activity->description }}</p> </div> </div> </div> </div></x-app-layout>
All that is left is to add a link to the homepage.
resources/views/home.blade.php:
// ...<div class="grid grid-cols-4 gap-5"> @forelse($activities as $activity) <div> <a href="{{ route('activity.show', $activity) }}"> <img src="{{ asset($activity->thumbnail) }}" alt="{{ $activity->name }}"> </a> <h2> <a href="#" class="text-lg font-semibold">{{ $activity->name }}</a> <a href="{{ route('activity.show', $activity) }}" class="text-lg font-semibold">{{ $activity->name }}</a> </h2> <time>{{ $activity->start_time }}</time> </div> @empty <p>No activities</p> @endforelse</div> // ...
Now, after visiting an activity, we should see a similar result:
Seeding Dummy Data
We need to add some "fake" data to show this homepage to the client. Of course, it will be a Seeder.
php artisan make:seeder ActivitySeeder
database/seeders/ActivitySeeder.php:
class ActivitySeeder extends Seeder{ public function run(): void { Activity::factory(20)->create(); }}
And we need to call it.
database/seeders/DatabaseSeeder.php:
class DatabaseSeeder extends Seeder{ public function run(): void { $this->call([ RoleSeeder::class, ActivitySeeder::class, ]); }}
Tests
Before adding tests for the homepage, we need to fix the seeding process. In the TestCase
, we added a $seed
variable to run the main Seeder, but we don't need to seed activities every time. However, we can change it to seed only the roles.
tests/TestCase.php:
use Database\Seeders\RoleSeeder; abstract class TestCase extends BaseTestCase{ use CreatesApplication; protected bool $seed = true; protected string $seeder = RoleSeeder::class; }
Tests also must be adjusted to the home
route change.
tests/Feature/Auth/AuthenticationTest.php:
class AuthenticationTest extends TestCase{ // ... public function test_users_can_authenticate_using_the_login_screen(): void { $user = User::factory()->create(); $response = $this->post('/login', [ 'email' => $user->email, 'password' => 'password', ]); $this->assertAuthenticated(); $response->assertRedirect(route('dashboard', absolute: false)); $response->assertRedirect(route('home', absolute: false)); } // ...}
tests/Feature/Auth/EmailVerificationTest.php:
class EmailVerificationTest extends TestCase{ // ... public function test_email_can_be_verified(): void { $user = User::factory()->create([ 'email_verified_at' => null, ]); Event::fake(); $verificationUrl = URL::temporarySignedRoute( 'verification.verify', now()->addMinutes(60), ['id' => $user->id, 'hash' => sha1($user->email)] ); $response = $this->actingAs($user)->get($verificationUrl); Event::assertDispatched(Verified::class); $this->assertTrue($user->fresh()->hasVerifiedEmail()); $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); $response->assertRedirect(route('home', absolute: false).'?verified=1'); } // ...}
tests/Feature/Auth/RegistrationTest.php:
class RegistrationTest extends TestCase{ // ... public function test_new_users_can_register(): void { $response = $this->post('/register', [ 'name' => 'Test User', 'password' => 'password', 'password_confirmation' => 'password', ]); $this->assertAuthenticated(); $response->assertRedirect(route('dashboard', absolute: false)); $response->assertRedirect(route('home', absolute: false)); } public function test_user_can_register_with_token_for_company_owner_role() { // ... $response->assertRedirect(route('dashboard', absolute: false)); $response->assertRedirect(route('home', absolute: false)); } public function test_user_can_register_with_token_for_guide_role() { // ... $response->assertRedirect(route('dashboard', absolute: false)); $response->assertRedirect(route('home', absolute: false)); }}
First, let's add one more assertion for the image upload test. We added a feature to create a thumbnail, so let's check if a thumbnail has been created. Also, let's change the disk from public
to activities
.
tests/Feature/CompanyActivityTest.php:
use Tests\TestCase;use App\Models\User;use App\Models\Company;use App\Models\Activity;use Illuminate\Http\UploadedFile;use Illuminate\Support\Collection;use Illuminate\Support\Facades\Storage;use Illuminate\Foundation\Testing\RefreshDatabase; class CompanyActivityTest extends TestCase{ // ... public function test_can_upload_image() { Storage::fake('activities'); $company = Company::factory()->create(); $user = User::factory()->companyOwner()->create(['company_id' => $company->id]); $guide = User::factory()->guide()->create(); $file = UploadedFile::fake()->image('avatar.jpg'); $this->actingAs($user)->post(route('companies.activities.store', $company), [ 'name' => 'activity', 'description' => 'description', 'start_time' => '2023-09-01 10:00', 'price' => 9999, 'guides' => $guide->id, 'image' => $file, ]); Storage::disk('public')->assertExists('activities/' . $file->hashName()); Storage::disk('activities')->assertExists($file->hashName()); Storage::disk('activities')->assertExists('thumbs/' . $file->hashName()); } public function test_cannon_upload_non_image_file() { Storage::fake('activities'); $company = Company::factory()->create(); $user = User::factory()->companyOwner()->create(['company_id' => $company->id]); $guide = User::factory()->guide()->create(); $file = UploadedFile::fake()->create('document.pdf', 2000, 'application/pdf'); $response = $this->actingAs($user)->post(route('companies.activities.store', $company), [ 'name' => 'activity', 'description' => 'description', 'start_time' => '2023-09-01 10:00', 'price' => 9999, 'guides' => $guide->id, 'image' => $file, ]); $response->assertSessionHasErrors(['image']); Storage::disk('public')->assertMissing('activities/' . $file->hashName()); Storage::disk('activities')->assertMissing($file->hashName()); } // ...}
Now let's test the homepage:
- Page can be accessed for both guests and authenticated users.
- If there are no activities message
No activities
is shown. - When there aren't enough activities pagination link isn't shown.
- On the second page, the correct activity is shown.
- And activities are shown in the correct order.
php artisan make:test HomePageTest
tests/Feature/HomePageTest.php:
use Tests\TestCase;use App\Models\User;use App\Models\Activity;use Illuminate\Foundation\Testing\RefreshDatabase; class HomePageTest extends TestCase{ use RefreshDatabase; public function test_unauthenticated_user_can_access_home_page() { $response = $this->get(route('home')); $response->assertOk(); } public function test_authenticated_user_can_access_home_page() { $user = User::factory()->create(); $response = $this->actingAs($user)->get(route('home')); $response->assertOk(); } public function test_show_no_activities_when_theres_no_upcoming_activities() { $response = $this->get(route('home')); $response->assertSeeText('No activities'); } public function test_pagination_isnt_shown_when_activities_are_9() { Activity::factory(9)->create(); $response = $this->get(route('home')); $response->assertDontSee('Next'); } public function test_pagination_shows_correct_results() { Activity::factory(9)->create(); $activity = Activity::factory()->create(['start_time' => now()->addYear()]); $response = $this->get(route('home')); $response->assertSee('Next'); $response = $this->get(route('home') . '/?page=2'); $response->assertSee($activity->name); } public function test_order_by_start_time_is_correct() { $activity = Activity::factory()->create(['start_time' => now()->addWeek()]); $activity2 = Activity::factory()->create(['start_time' => now()->addMonth()]); $activity3 = Activity::factory()->create(['start_time' => now()->addMonths(2)]); $response = $this->get(route('home')); $response->assertSeeTextInOrder([ $activity->name, $activity2->name, $activity3->name, ]); }}
And another test for the activity show. For now, we will just test that page return status 200
if activity exists and 404
if it doesn't exist.
php artisan make:test ActivityShowTest
tests/Feature/ActivityTest.php:
use Tests\TestCase;use App\Models\Activity;use Illuminate\Foundation\Testing\RefreshDatabase; class ActivityShowTest extends TestCase{ use RefreshDatabase; public function test_can_view_activity_page() { $activity = Activity::factory()->create(); $response = $this->get(route('activity.show', $activity)); $response->assertOk(); } public function test_gets_404_for_unexisting_activity() { $response = $this->get(route('activity.show', 69)); $response->assertNotFound(); }}
I think we need to remove the image when deleting the activity
I suggest this in destroy method:
Hello. You forgot to describe the ActivityObserver file.
THis has to be outdated with Laravel 10. Ive tried everything to get use Intervention\Image\Facades\Image recognized, and its just not working for the life of me.
No it is outdated as it was made with laravel 10. Can you be more specific what isn't working? What errors you get?
Error: Class "Intervention\Image\Facades\Image" not found in /Applications/XAMPP/xamppfiles/htdocs/reservations/app/Http/Controllers/CompanyActivityController.php:105 This happens whenever I run my CompanyActivityTest.Test_can_upload_image wont pass because Image::make in the CompanyActivitiyController isn't recognized.
Ive tried adjusting providers array in app.php, as well as aliases, ive tried php artisan cache:clear, and config, tried php artisan optimize:clear, composer dump-autoload, verifiying its installed, reinstalling it, pretty much everything.
Can you properly format your comments?
Well i think i finally solved it. I was installing intervention/image version 3.4. They totally changed how it works if you visit their Github (looking at the README.md: https://github.com/Intervention/image . So I looked at their revision history and tried using version 3.0 , still no dice. I then decided to use version 2.7. That fixed it, although i got a bunch of nasty warnings when i updated my composer.json intervention/image in "require" to 2.7. But that fixed it. I think the syntax might need updating to the modern version that Github reflects, however i could be wrong.
You are rigth. If you would look at the repository at the time of writing the 2.7 version was used. Maybe this course will get updated with the release of Laravel 11.
Anyone with php8.3 on Windows, did you managed to pass the "can upload test"? I didn't have imagick enabled, so I embarked on a quest to get it enable, and it looks like is not working with php 8.3 yet.
I do have php 8.1 installed, but since I started the tutorial with php 8.3 I can't switch back and forth. I'm curios if someone in my situation managed to move forward.
Thanks!
You must enable the extension otherw it won't
That's a bummer :( . Looks like the support for imagick on php 8.3 still requries some development efforts form php dev team, and I really don't want to start from scratch under a different version.
I really doubt its the extensions problem. It's how to enable it. And you don't need to start over just because of php version. But laravel 11 requires 8.2 minimum if you are using that version
Can you expand on the part where I don't need to start over if I switch to php 8.1? I am interested to move forward without having write the whole thing from ground up, but when I tried to switch from php8.3 to php8.1, and atempted to run php artisan serve I had a different error requiring at least php 8.2. The Laravel version I'm using is 10.3.
Run
composer update
and you should be fine. But I would suggest figur the 8.3 problem as 8.1 isn't maintained anymoreI managed to make it work on MacOS with Laravel 10 and php 8.3, now all test are passing with flying colors. I will try your suggestion on Windows tomorrow as I don't access to one from home. Thank you.
Maan, this was a long journey. There is no official release of imagick for PHP 8.3 due to issues with the build machine (according to their statement on Github) however there are a few nice people that made it their mission to build imagick for PHP 8.3 and can be downloaded form here: https://github.com/Imagick/imagick/issues/573#issuecomment-1827616798 Now I have all test passing on Windows as well.
@teebee thanks for your link. I made it work on my Windows 10 machine in PHP 8.3.4 with Laragon.
I knew someone else was bound to run into the same issue :). Glad to hear it helped you.
If have an issue displaying the thumbnails. I configured filesystems.php as in this lesson, but my files are getting stored in public/storage/activities instead of public/activities. For this reason the images don't show up.
I could fix this by changing the method to:
Or by changing following in the the filesystems.php
to
You should not store images in the
public
folder. Yourpublic
folder should have a symlink calledstorage
there (after runningphp artisan storage:link
) and you should access all of your images from/storage/XXX/XXX.png
.Changing your filesystem to write to public path is really bad as that will push all images to GIT, which will cause issues.
Hi Modestas, thank you for your reply. Writing to the public path seemed indeed as a bad practice. I do have a storage folder in my public folder, but in the code above the activities folder is a direct child of the public folder. Maybe its from an older Laravel version.
I think that you are misunderstanding things a little bit :)
/public/storage
will symlink to/storage/app/public
- which means that all the uploads can be accessed publicly.There's another, private storage, which would write into
/storage/app
and it would not be accessible via url. It will be uploaded, but there will not be any access from URL endpoint.So to answer your question - you have to upload all your files to
/storage/app/public
which will be accessible viadomain.com/storage/PATH_TO_FILE
That was what I meant, but clearly I haven't explained it correctly. We are on the same wavelength. Thank you