shahriar_shaon's avatar

I’m extremely confused about my code structure and I’m hoping a senior developer can help me out

My confusion: I’m using PHPStan at the maximum level, which means I have to write a lot of heavy annotations just to satisfy PHPStan. Also, I’ve never used DTOs before — this is my first project using them — so I’m not even sure if I’m following the correct approach.

What’s troubling me the most is that after validating the data, when I pass it into the DTO, PHPStan keeps giving warnings because of mixed values.

I’m sharing my project’s GitHub link below. If anyone kind-hearted could take a look at my code, point out where I’m going wrong, and guide me toward the proper way to structure things, I would really appreciate it.

github.com/shahriarshaon1993/myfinance

My DTO: RoleDto.php

Request validation: StoreAccountTypeRequest.php

public function rules(): array
    {
        return [
            'code' => ['required', 'string', 'min:2', 'max:50', 'regex:/^[A-Z_]+$/', 'unique:account_types,code'],
            'name' => ['required', 'string', 'min:2', 'max:50'],
            'description' => ['nullable', 'string', 'min:2', 'max:500'],
            'normal_balance_debit' => ['required', 'boolean'],
            'is_active' => [Rule::enum(ActiveStatus::class)],
        ];
    }

    /**
     * @return string[]
     */
    public function messages(): array
    {
        return [
            'code.regex' => 'The code may only contain uppercase letters and underscores.',
        ];
    }

Controller is: AccountTypeController.php

public function store(StoreAccountTypeRequest $request, CreateType $action): RedirectResponse
    {
        Gate::authorize('create', AccountType::class);

        /** @var array{code: string, name: string, normal_balance_debit: bool, is_active: string, description: string|null} $data */
        $data = $request->validated();

        $action->handle(AccountTypeDto::fromArray($data));

        return to_route('accounting.types.index')
            ->with('success', 'Account type created successfully.');
    }

Store action class: CreateAccountType.php

public function handle(AccountTypeDto $type): AccountType
    {
        return AccountType::create($type->toArray());
    }
1 like
8 replies
DigitalArtisan's avatar

I have one question: Why is every class final, like every single class in your whole project?

shahriar_shaon's avatar

I intentionally use final on classes. If a class is not intended to be extended, marking it as final provides an explicit indicator of that intent. It prevents accidental inheritance, keeps the architecture predictable, and encourages composition over unnecessary subclassing.

It also contributes to ease of codebase maintenance, as those classes cannot have their behavior changed through inheritance, thus avoiding any unexpected side effects in the future.

DigitalArtisan's avatar

I wouldn't say your getting it wrong, just need more information to get rid of the warning. With this:

@param  array{
	code: string, 
	name: string, 
	normal_balance_debit: bool, 
	is_active: string, description: string|null}  $data
     */
    public static function fromArray(array $data): self

I believe this is the mixed type warning you are getting. I do not use PHPStan, but I can assume you need to explicitly state what type of $data array you have, not just an array of $data.

Something like this may work for you:

/**
 * @phpstan-type AccountTypeData array{
 *     code: string,
 *     name: string,
 *     normal_balance_debit: bool,
 *     is_active: string,
 *     description: string|null
 * }
 */
final readonly class AccountTypeDTO
{
    /**
     * @param AccountTypeData $data
     */
    public static function fromArray(array $data): self
    {
shahriar_shaon's avatar

Yes, thank you so much, it's work

I want to know how I can handle that section

public function store(StoreAccountTypeRequest $request, CreateType $action): RedirectResponse
    {
        Gate::authorize('create', AccountType::class);

        /** @var array{code: string, name: string, normal_balance_debit: bool, is_active: string, description: string|null} $data */
        $data = $request->validated();

You see, I have to write a lot of heavy annotations just to satisfy PHPStan. cause FormRequest returns me mixed value

DigitalArtisan's avatar

with this maybe?:

 /** @var AccountTypeData $data */
    $data = $request->validated();
1 like

Please or to participate in this conversation.