In this article I would like to describe how you can refactor a method that contains multiple if-statements by creating objects with a single responsibility, each making their own assertions. Precondition checks might for example validate the arguments that are passed to the function, prevent failure scenarios, or make assertions about the state of the data in your system.

For each precondition check, we can can now create an implementation of the CourseAttemptRejectionContract: use App\Assertions\Contracts\CourseAttemptRejectionContract; use App\Models\Course; use Carbon\Carbon; class CourseExpired implements CourseAttemptRejectionContract { public function handle(Course $course): bool { return Carbon::parse($course->expiration_date)->lt(Carbon::now())} public function message(): string { return 'Course expired'; }} use App\Assertions\Contracts\CourseAttemptRejectionContract; use App\Models\Course; class TooManyAttemptsForCourse implements CourseAttemptRejectionContract { public function handle(Course $course): bool { return count($course->attempts) >= $course->max_attempts; } public function message(): string { return 'Too many attempts'; }} use App\Assertions\Contracts\CourseAttemptRejectionContract; use App\Models\Course; class CourseAlreadyFinished implements CourseAttemptRejectionContract { public function handle(Course $course): bool { return $course->hasCertificate(); } public function message(): string { return 'Course already finished'; }}

It is possible to create a stub object for the precondition checks, for example one that always returns true: use App\Assertions\Contracts\CourseAttemptRejectionContract; use App\Models\Course; class CourseAttemptRejectionStub implements CourseAttemptRejectionContract { public function handle(Course $course): bool { return true; } public function message(): string { return 'Failure'; }} use PHPUnit\Framework\TestCase; use App\Actions\CreateCourseAttempt; use App\Exceptions\CreateCourseAttemptException; use App\Models\Course; use App\Repositories\CourseAttemptRepository; use Tests\Stubs\CourseAttemptRejectionStub; class CreateCourseAttemptTest extends TestCase { public function testActionPreconditionFailed(): void { $course = new Course(); $courseAttemptRepository = new CourseAttemptRepository(); $createCourseAttemptAction = new CreateCourseAttempt( $courseAttemptRepository, [ CourseAttemptRejectionStub::class, ]); $this->expectException(CreateCourseAttemptException::class); $createCourseAttemptAction->handle($course, ['answer']); } public function testActionSuccessfulWithoutChecks(): void { $course = new Course(); $courseAttemptRepository = new CourseAttemptRepository(); $createCourseAttemptAction = new CreateCourseAttempt( $courseAttemptRepository, [ ]); $this->assertTrue($createCourseAttemptAction->handle($course, ['answer'])); }}

A unit test for a single precondition check could look like this: use PHPUnit\Framework\TestCase; use App\Assertions\TooManyAttemptsForCourse; class CreateCourseAttemptTest extends TestCase { public function testTooManyAttemptsPreconditionFailed(): void { $course = new Course([ 'attempts' => 3, 'max_attempts' => 3, ]); $condition = new TooManyAttemptsForCourse(); $this->assertEquals(true, $rejection->handle($course)); } public function testTooManyAttemptsPreconditionPassed(): void { $course = new Course([ 'attempts' => 3, 'max_attempts' => 3, ]); $condition = new TooManyAttemptsForCourse(); $this->assertEquals(true, $rejection->handle($course)); }}
Newsletter

Get the latest Laravel/PHP jobs, events and curated articles straight to your inbox, once a week

Glimpse streamlines Laravel development by seamlessly deploying GitHub pull requests to preview environments with the help of Laravel Forge. Glimpse streamlines Laravel development by seamlessly deploying GitHub pull requests to preview environments with the help of Laravel Forge.
Fathom Analytics | Fast, simple and privacy-focused website analytics. Fathom Analytics | Fast, simple and privacy-focused website analytics.
Shirts painstakingly handcrafted by under-caffeinated developers. Shirts painstakingly handcrafted by under-caffeinated developers.
Community Partners