DEV Community

pirvanm
pirvanm

Posted on

How do you make FullStack Demo App for Interviews using Laravel + React with Dockerize Part 2

Previously, I stepped into the world of DDD. Now I’ll continue on the backend side with more DTOs and Infrastructure, which form the main Part 1 with its second subpart.

For the business logic, we’ll continue with Mentor and User. So, let’s move to the next level:
backend/app/Application/Mentor/

Here, we start with the DTOs, and the first class we have is MentorDTO, with the following code:

<?php

namespace App\Application\Mentor\DTOs;

readonly class MentorDTO
{
    public function __construct(
        public int $id,
        public string $title,
        public array $expertise,
        public string $availability,
        public ?string $avatar = null,
        public string $bio,
        public string $technicalBio,
        public string $mentoringStyle,
        public string $audience,
    ) {}
}
Enter fullscreen mode Exit fullscreen mode

In the second DTO, we have:

<?php

namespace App\Application\Mentor\DTOs;

readonly class MentorProfileDTO
{
    public function __construct(
        public int $id,
        public string $title,
        public string $fullName,
        public ?string $avatar,
        public array $expertise,
        public string $availability,
        public string $bio,
        public string $email,
        public string $technicalBio,
        public string $mentoringStyle,
        public string $audience,
    ) {}
}
Enter fullscreen mode Exit fullscreen mode

Now, let’s continue with the Queries for this business logic application.

<?php

namespace App\Application\Mentor\Queries;

use Illuminate\Support\Collection;
use Illuminate\Database\ConnectionInterface;
use App\Application\Mentor\DTOs\MentorProfileDTO;

class GetMentorsWithUsersQuery
{
    public function __construct(private ConnectionInterface $db) {}

    /**
     * Fetch the list of mentors with user data.
     *
     * @return Collection<int, MentorListDto>
     */
    public function execute(): Collection
    {
        return $this->db->table('mentors')
            ->join('users', 'mentors.user_id', '=', 'users.id')
            ->where('mentors.availability', '!=', 'paused')
            ->orderBy('mentors.created_at', 'desc')
            ->select([
                'mentors.id',
                'mentors.title',
                'mentors.expertise',
                'mentors.availability',
                'mentors.avatar as mentor_avatar',
                'mentors.bio',
                'mentors.technical_bio',
                'mentors.mentoring_style',
                'mentors.audience',
                'users.first_name',
                'users.last_name',
                'users.email',
            ])
            ->get()
            ->map(function ($row) {
                return new MentorProfileDTO(
                    id: $row->id,
                    title: $row->title,
                    fullName: "$row->first_name $row->last_name",
                    avatar: $row->mentor_avatar ?? null,
                    expertise: json_decode($row->expertise, true),
                    availability: $row->availability,
                    bio: $row->bio,
                    technicalBio: $row->technical_bio,
                    mentoringStyle: $row->mentoring_style,
                    audience: $row->audience,
                    email: $row->email,
                );
            });
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, we have the Query class:

<?php

namespace App\Application\Mentor\Queries;

use Illuminate\Support\Facades\DB;
use Illuminate\Database\ConnectionInterface;
use App\Application\Mentor\DTOs\MentorProfileDTO;

class MentorProfileQuery
{
    public function __construct(private ConnectionInterface $db) {}

    public function execute(int $mentorId): ?MentorProfileDTO
    {
        $record = $this->db->table('mentors')
            ->join('users', 'mentors.user_id', '=', 'users.id')
            ->where('mentors.id', $mentorId)
            ->where('mentors.availability', '!=', 'paused') // only active
            ->select([
                'mentors.id as mentor_id',
                'users.first_name',
                'users.last_name',
                'users.email',
                'mentors.title',
                'mentors.bio',
                'mentors.technical_bio',
                'mentors.mentoring_style',
                'mentors.audience',
                'mentors.avatar as mentor_avatar',
                'mentors.expertise',
                'mentors.availability',
                'mentors.created_at as mentor_created_at',
            ])
            ->first();

        if (! $record) {
            return null;
        }

        return new MentorProfileDTO(
            id: $record->mentor_id,
            fullName: $record->first_name . ' ' . $record->last_name,
            email: $record->email,
            title: $record->title,
            bio: $record->bio,
            technicalBio: $record->technical_bio,
            mentoringStyle: $record->mentoring_style,
            audience: $record->audience,
            avatar: $record->mentor_avatar ?? $record->user_avatar,
            expertise: json_decode($record->expertise, true),
            availability: $record->availability,
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we move on to the User business logic, which is more advanced. A new feature in this project is the use of Actions, with:

<?php

namespace App\Application\User\Actions;

use App\Domain\User\Entities\User;
use App\Application\User\DTOs\LoginUserDTO;
use App\Domain\User\Repositories\UserRepositoryInterface;

class AuthenticateUserAction
{
    public function __construct(
        private UserRepositoryInterface $userRepository
    ) {}

    public function execute(LoginUserDTO $dto): User
    {

    }
}
Enter fullscreen mode Exit fullscreen mode

To wrap things up, here’s the Action class:
`
<?php

namespace App\Application\User\Actions;

use App\Application\User\DTOs\RegisterUserDTO;
use App\Domain\User\Entities\User;
use App\Domain\User\Repositories\UserRepositoryInterface;

class RegisterUserAction
{
public function __construct(
private UserRepositoryInterface $userRepository
) {}

public function execute(RegisterUserDTO $dto): User
{

}
Enter fullscreen mode Exit fullscreen mode

}`

Now we step back one level to define a DTO class for it:

<?php

namespace App\Application\User\DTOs;

class LoginUserDTO
{
    public function __construct(
        public readonly string $email,
        public readonly string $password
    ) {}
}
Enter fullscreen mode Exit fullscreen mode

Continuing in the DTO folder:

<?php

namespace App\Application\User\DTOs;

class RegisterUserDTO
{
    public function __construct(
        public readonly string $name,
        public readonly string $email,
        public readonly string $password
    ) {}
}
Enter fullscreen mode Exit fullscreen mode

And lastly, we have the DTOs

<?php

namespace App\Application\User\DTOs;

class UserDTO
{
    public function __construct(
        public readonly int $id,
        public readonly string $name,
        public readonly string $email,
        public readonly string $password,
    ) {}
}
Enter fullscreen mode Exit fullscreen mode

Well, I can’t say that Resources are something new in Laravel—I think I first used them back in a Laravel 7 project, which was personal and is still running online for everyone who loves chill music.

Here’s an example I found, which I think is a more modern way to avoid code conflicts. It feels almost like a TypeScript-style version of PHP. :))

<?php

namespace App\Application\User\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id(),
            'name' => $this->name(),
            'email' => $this->email()->value(),
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we go back two levels and continue with Infrastructure. At first, it may look like a slightly ‘hellish’ DDD concept, but after years of working with it, I find it closer to Symfony. And even though it’s considered dead, it still reminds me of the Yii framework.

<?php

namespace App\Infrastructure\Factories;

use App\Application\Mentor\DTOs\MentorDTO;
use App\Domain\Mentor\Entities\Mentor;
use App\Domain\Mentor\Enums\Availability;
use App\Domain\Mentor\Factories\MentorFactoryInterface;
use App\Domain\Mentor\ValueObjects\ExpertiseList;

class MentorFactory implements MentorFactoryInterface
{
    public function from(object|array $data): Mentor
    {
        return new Mentor(
            id: $record->id,
            title: $record->title,
            expertise: new ExpertiseList(json_decode($record->expertise, true)),
            availability: Availability::from($data->availability),
            avatar: $record->avatar,
            bio: $record->bio
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

A smaller example compared to the previous one:

<?php

namespace App\Infrastructure\Factories;

use App\Domain\Mentor\ValueObjects\ExpertiseList;
use App\Domain\User\ValueObjects\MentorValueObjectsFactoryInterface;

class MentorValueObjectsFactory implements MentorValueObjectsFactoryInterface
{
    public function expertiseList(array $value): ExpertiseList
    {
        return new ExpertiseList($value);
    }
}
Enter fullscreen mode Exit fullscreen mode

I think this is more of an extension of the basic factory, but it’s a well-structured UserFactory.

<?php

namespace App\Infrastructure\Factories;

use App\Application\User\DTOs\UserDTO;
use App\Domain\User\ValueObjects\UserValueObjectsFactoryInterface;

class UserFactory
{
    public function __construct(
        private readonly UserValueObjectsFactoryInterface $voFactory
    ) {}

    public function from(object|array $data): Entity
    {
        return new Entity(
            id: $record->id,
            first_name: $record->first_name,
            last_name: $record->last_name,
            email: $this->voFactory->email($record->email),
            password: $this->voFactory->password($record->password),
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

And lastly, the Factory:

<?php

namespace App\Infrastructure\Factories;

use App\Domain\User\ValueObjects\Email;
use App\Domain\User\ValueObjects\Password;
use App\Domain\User\ValueObjects\UserValueObjectsFactoryInterface;

class UserValueObjectsFactory implements UserValueObjectsFactoryInterface
{
    public function email(string $value): Email
    {
        return new Email($value);
    }

    public function password(string $value): Password
    {
        return new Password($value);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we continue with one of the oldest and most common patterns in Laravel—the Controller. It’s simply been moved to another folder and given a more modern structure. As usual, we’ll keep the business logic for Mentor and User.

Inside API/Mentor:

<?php

namespace App\Infrastructure\Http\Controllers\API\Mentor;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Infrastructure\Http\Controllers\Controller;
use App\Application\Mentor\Queries\GetMentorsWithUsersQuery;

class MentorListController extends Controller
{    
    public function __invoke(GetMentorsWithUsersQuery $query): JsonResponse
    {
       $mentors = $query->execute();

        return response()->json([
            'data' => $mentors,
            'total' => $mentors->count(),
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

And to wrap up, here’s the Mentor controller:

<?php

namespace App\Infrastructure\Http\Controllers\API\Mentor;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Infrastructure\Http\Controllers\Controller;
use App\Application\Mentor\Queries\MentorProfileQuery;
use App\Application\Mentor\Queries\GetMentorsWithUsersQuery;

class MentorProfileController extends Controller
{    
    public function __invoke(MentorProfileQuery $query, int $id): JsonResponse
    {
        $mentor = $query->execute($id);

        return response()->json(['data' => $mentor ?? null]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Something rare I’ve seen in Laravel is a refactored LoginController. I think 90% of Laravel projects end up with a lot of code inside it, almost like a Singleton class.

<?php

namespace App\Infrastructure\Http\Controllers\API\User;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Application\User\DTOs\LoginUserDTO;
use App\Application\User\Resources\UserResource;
use App\Infrastructure\Requests\LoginRequest;
use App\Infrastructure\Http\Controllers\Controller;
use App\Application\User\Actions\AuthenticateUserAction;

class LoginController extends Controller
{
    public function __construct(
        private readonly AuthenticateUserAction $authAction
    ) {}

    public function __invoke(
        LoginRequest $request
    ): JsonResponse {
        $user = $authAction->execute($request->dto());

        // Start session
        \Auth::loginUsingId($user->id());

        return UserResource::make($user)->response();
    }
}
Enter fullscreen mode Exit fullscreen mode

Here’s another example from the category of web development pleasures:

<?php

namespace App\Infrastructure\Http\Controllers\API\User;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Infrastructure\Http\Controllers\Controller;

class LogoutController extends Controller
{    
    public function __invoke(Request $request): JsonResponse
    {
        \Auth::guard('web')->logout();
        $request->session()->invalidate();
        $request->session()->regenerateToken();

        return response()->json(['message' => 'Logged out']);
    }
}
Enter fullscreen mode Exit fullscreen mode

In general, most Laravel versions use a facade or a custom class to retrieve the current user. I think this is a good example of optimization:

<?php

namespace App\Infrastructure\Http\Controllers\API\User;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Application\User\Resources\UserResource;
use App\Infrastructure\Http\Controllers\Controller;

class MeController extends Controller
{    
    public function __invoke(Request $request): JsonResponse
    {
        $user = $request->user();

        if (! $user) {
            return response()->json(['message' => 'Unauthenticated'], 401);
        }

        return UserResource::make($user)->response();
    }
}
Enter fullscreen mode Exit fullscreen mode

Now let’s look at the longest controller in our project:

<?php

namespace App\Infrastructure\Http\Controllers\API\User;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Application\User\DTOs\RegisterUserDTO;
use App\Application\User\Resources\UserResource;
use App\Infrastructure\Http\Controllers\Controller;
use App\Application\User\Actions\RegisterUserAction;
use App\Infrastructure\Requests\RegisterRequest;

class RegisterController extends Controller
{
    public function __construct(
        private readonly RegisterUserAction $registerUserAction
    ) {}

    public function __invoke(
        RegisterRequest $request,
    ): JsonResponse {

        $user = $this->registerUserAction->execute($request->dto());

        return response()->json([
            'message' => 'User registered successfully',
        ], 201);
    }
}
Enter fullscreen mode Exit fullscreen mode

Personally, I’m a fan of the Service concept. For me, relying only on controllers feels limiting in the PHP world.

To take another step in the request lifecycle, I’ll continue with the Request concept.

<?php

namespace App\Infrastructure\Requests;

use DomainException;

use App\Application\User\DTOs\LoginUserDTO;
use Illuminate\Foundation\Http\FormRequest;

class LoginRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;

    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'email' => ['required', 'email'],
            'password' => ['required', 'string'],
        ];
    }

    public function dto(): LoginUserDTO
    {
        return new LoginUserDTO(
            email: $this->string('email')->trim()->toString(),
            password: $this->string('password')->trim()->toString()
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

I think this is a new function, dto(). I didn’t see it in Laravel 11.

It looks like another Symfony pattern adapted to Laravel. I got this code:

<?php

namespace App\Infrastructure\Requests;

use App\Application\User\DTOs\RegisterUserDTO;
use Illuminate\Foundation\Http\FormRequest;

class RegisterRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'first_name' => ['required', 'string', 'max:255'],
            'last_name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:8'],
        ];
    }

    public function dto(): RegisterUserDTO
    {
        return new RegisterUserDTO(
            first_name: $this->string('first_name')->trim()->toString(),
            last_name: $this->string('last_name')->trim()->toString(),
            email: $this->string('email')->trim()->toString(),
            password: $this->string('password')->trim()->toString(),
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

The model isn’t new in Laravel, but here it’s simply placed in the Infrastructure folder. In my view, compared with other frameworks (except for the big switch in version 12), Laravel is still quite easy to adapt across versions—probably thanks to its strong documentation. So, I don’t really see anything new here.

<?php

namespace App\Infrastructure\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    /** @use HasFactory<\Database\Factories\UserFactory> */
    use HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var list<string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var list<string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * Get the attributes that should be cast.
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

In this project, we don’t need to use any providers, so we can skip that step. This brings us to one of the most loved features in Laravel when it comes to project optimization: repositories.

<?php

namespace App\Infrastructure\Repositories;

use App\Domain\Mentor\Entities\Mentor;
use App\Domain\Mentor\Repositories\MentorRepositoryInterface;
use App\Domain\Mentor\Factories\MentorEntityFactoryInterface;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Support\Collection;

class MentorRepository implements MentorRepositoryInterface
{
    public function __construct(
        private ConnectionInterface $db,
        private MentorEntityFactoryInterface $entityFactory
    ) {}

    public function findAll(): Collection
    {
        $records = $this->db->table('mentors')
            ->where('availability', '!=', 'paused')
            ->orderBy('created_at', 'desc')
            ->get();

        return $records->map(fn ($r) => $this->toEntity($r));
    }

    public function findById(int $id): ?Mentor
    {
        $record = $this->db->table('mentors')->where('id', $id)->first();

        return $record ? $this->toEntity($record) : null;
    }

    private function toEntity(array $record): Mentor
    {
        return $this->entityFactory->from([
            'id' => $record['id'],
            'title' => $record['title'],
            'expertise' => json_decode($record['expertise'], true),
            'availability' => $record['availability'],
            'avatar' => $record['avatar'] ?? null,
            'bio' => $record['bio'] ?? null
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, regarding the UserRepository class:

<?php

namespace App\Infrastructure\Repositories;

use App\Domain\User\Entities\User;
use App\Domain\User\ValueObjects\Email;
use Illuminate\Database\ConnectionInterface;
use App\Infrastructure\Factories\UserFactory;
use App\Domain\User\Factories\UserEntityFactoryInterface;
use App\Domain\User\Repositories\UserRepositoryInterface;

class UserRepository implements UserRepositoryInterface
{
    public function __construct(
        private ConnectionInterface $db,
        private UserEntityFactoryInterface $userFactory,
        private UserFactory $dataMapper
    ) {}

    /**
     * Create a new user in the database
     */
    public function create(User $user): User
    {
        $id = $this->db->table('users')->insertGetId([
            'first_name' => $user->firstName(),
            'last_name' => $user->lastName(),
            'email' => $user->email()->value(),
            'password' => $user->password()->value(),
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        return $this->findById($id);
    }

    /**
     * Find user by email
     */
    public function findByEmail(Email $email): ?User
    {
        $record = $this->db->table('users')->where('email', $email->value())->first();

        if (! $record) {
            return null;
        }

        return $this->userFactory->from($record);
    }

    /**
     * Find user by ID
     */
    public function findById(int $id): ?User
    {
        $record = $this->db->table('users')->where('id', $id)->first();

        if (! $record) {
            return null;
        }

        return $this->userFactory->from($record);
    }
}
Enter fullscreen mode Exit fullscreen mode

For now, we’ll end with this part. In the next section, we’ll continue with another Laravel concept to reach our final goal: enabling communication between the React and Laravel app in a dockerized environment.

If you like my work, you can find me on levelcoding.com
or on LinkedIn (levelcoding). A share or follow on LinkedIn would really help!

Top comments (0)