skip to content

After php artisan fortify:install, which classes in app/Actions/Fortify does a Laravel app own, and how does Fortify call them?

level: middleimportance: should knowfreq 35%

answer

  1. published stubs, now your code
  2. CreateNewUser, ResetUserPassword and two more
  3. PasswordValidationRules trait
  4. wired in FortifyServiceProvider::boot
  5. each implements a Fortify contract

basics

~10 s

fortify:install publishes CreateNewUser, ResetUserPassword, UpdateUserPassword, UpdateUserProfileInformation and a PasswordValidationRules trait into app/Actions/Fortify. They are your code; FortifyServiceProvider registers them with Fortify::createUsersUsing() and friends, and Fortify's controllers call them.

solid answer

~30 s

The command publishes four action classes and one trait into `app/Actions/Fortify`: `CreateNewUser` (validates input and creates the user), `ResetUserPassword`, `UpdateUserPassword`, `UpdateUserProfileInformation`, and `PasswordValidationRules`, which holds the shared password rules. Each class implements a Fortify contract such as `Laravel\Fortify\Contracts\CreatesNewUsers`. The published `App\Providers\FortifyServiceProvider` wires them up in `boot()` with `Fortify::createUsersUsing(CreateNewUser::class)`, `updateUserProfileInformationUsing`, `updateUserPasswordsUsing` and `resetUserPasswordsUsing`; Fortify's controllers resolve the contract and call the method. Once published they are application code: `composer update` never touches them, so adding a field, say a taxpayer reference at registration, means editing `CreateNewUser`, and upstream fixes to the stubs reach you only if you port them by hand.

code

php · 31 lines
php
<?php

namespace App\Actions\Fortify;

use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Laravel\Fortify\Contracts\CreatesNewUsers;

class CreateNewUser implements CreatesNewUsers
{
    use PasswordValidationRules;

    public function create(array $input): User
    {
        Validator::make($input, [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', Rule::unique(User::class)],
            'taxpayer_ref' => ['required', 'string', 'size:10', Rule::unique(User::class)],
            'password' => $this->passwordRules(),
        ])->validate();

        return User::create([
            'name' => $input['name'],
            'email' => $input['email'],
            'taxpayer_ref' => $input['taxpayer_ref'],
            'password' => Hash::make($input['password']),
        ]);
    }
}

go deeper

for a junior

Know the four published action classes and that you edit CreateNewUser to change what registration validates and stores.

for a middle

Explain the contract-plus-container binding set up in FortifyServiceProvider, and why the actions are testable without HTTP.

for a senior

Treat published stubs as owned code: track upstream changes, keep rules in PasswordValidationRules, and disable features in config rather than deleting classes.

for a principal

Set a policy for how far the team may diverge from Fortify's stubs before the maintenance cost argues for owning the whole auth layer.

## What `fortify:install` publishes `php artisan fortify:install` runs `vendor:publish` for Fortify's service provider and registers `App\Providers\FortifyServiceProvider` in `bootstrap/providers.php`. The published files are: - `config/fortify.php` - `app/Providers/FortifyServiceProvider.php` - `app/Actions/Fortify/CreateNewUser.php` - `app/Actions/Fortify/ResetUserPassword.php` - `app/Actions/Fortify/UpdateUserPassword.php` - `app/Actions/Fortify/UpdateUserProfileInformation.php` - `app/Actions/Fortify/PasswordValidationRules.php` - migrations: Fortify's two-factor columns and the `laravel/passkeys` migration Everything in `app/` is now **your code**. That is the whole design: Fortify owns the HTTP layer (routes, controllers, request classes, responses), and you own the business rules of each flow. ## The action classes and their contracts | Class | Contract it implements | Called when | |---|---|---| | `CreateNewUser` | `CreatesNewUsers` | `POST /register` | | `ResetUserPassword` | `ResetsUserPasswords` | `POST /reset-password` after the token checks out | | `UpdateUserPassword` | `UpdatesUserPasswords` | `PUT /user/password` | | `UpdateUserProfileInformation` | `UpdatesUserProfileInformation` | `PUT /user/profile-information` | `PasswordValidationRules` is a trait the classes share; in the Fortify 1.40 stub its `passwordRules()` returns the `Password::default()` rule plus `required`, `string` and `confirmed`, so one edit changes the password policy everywhere. The stub `CreateNewUser::create(array $input)` runs `Validator::make(...)->validate()` for `name`, a unique `email` and the password rules, then calls `User::create()` with `Hash::make($input['password'])`. ## How Fortify finds them The published provider's `boot()` method registers each class: 1. `Fortify::createUsersUsing(CreateNewUser::class)` 2. `Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class)` 3. `Fortify::updateUserPasswordsUsing(UpdateUserPassword::class)` 4. `Fortify::resetUserPasswordsUsing(ResetUserPassword::class)` Each call binds the contract to your class in the container. Fortify's controller type-hints the contract, the container hands it your class, and the controller calls `create()`, `reset()` or `update()` and then returns Fortify's response object. Because the binding goes through the container, your action can type-hint its own dependencies in a constructor. ## What owning them means - **You change behaviour by editing them.** A tax-filing app that needs a taxpayer reference and a country of residence at sign-up adds those rules and columns in `CreateNewUser`, not in a Fortify config key. - **They are not updated by Composer.** A security fix or a new default in Fortify's stubs does not reach an existing app; read Fortify's upgrade notes and port changes yourself. - **They are ordinary classes you can test.** Call `app(CreatesNewUsers::class)->create([...])` in a test without going through HTTP. - **Deleting one breaks its route**, because the controller still resolves the contract. Remove the feature from `features` instead. ## What you do not own The controllers, the login pipeline, the two-factor challenge and the route definitions stay inside the package. To change those you use Fortify's hooks rather than editing vendor code: `Fortify::authenticateUsing()` for credential checks, `Fortify::authenticateThrough()` for the login pipeline, the `*View()` methods for screens, and container bindings for response contracts such as `LoginResponse`. ## Starter kits The current starter kits ship the same kind of `app/Actions/Fortify` classes already published and adapted to the kit, so the ownership rule applies there too. ## A worked change: the tax-filing sign-up Suppose sign-up must capture a ten-character taxpayer reference and reject duplicates: 1. Add a migration for a unique `taxpayer_ref` column on `users`. 2. Add `taxpayer_ref` to the model's fillable attributes, otherwise `User::create()` silently drops it under the default mass-assignment settings. 3. Add the validation rule and the attribute in `CreateNewUser::create()`. 4. Add the field to the registration screen that posts to `/register`. 5. Test the action directly by resolving `CreatesNewUsers` from the container, and test the route once end to end. Nothing in `config/fortify.php` changes, which is the point: flow switches live in config, business rules live in your actions.

  • How would you stop users registering in a Fortify app without deleting CreateNewUser?
    Remove `Features::registration()` from the `features` array in `config/fortify.php`. The `/register` routes are then never registered, and the action class can stay or go without breaking anything.
  • Why does a Fortify upgrade not change your registration validation?
    The rules live in `app/Actions/Fortify/CreateNewUser.php` and `PasswordValidationRules.php`, published once by `fortify:install`. Composer updates `vendor/laravel/fortify`, not `app/`, so any change to the upstream stubs must be copied across by hand.

saying these in an interview costs you the question

  • The files in app/Actions/Fortify are updated whenever Fortify is upgraded.
  • You customise registration by editing Fortify's controllers in vendor.
  • Fortify finds the actions automatically by folder name, without registration.
  • Deleting CreateNewUser is the way to turn registration off.
  • Password rules must be repeated separately in every action class.