DigitalArtisan wrote a reply+100 XP
5d ago
Fair points on convenience — I’m not against tools that reduce setup time. I think where I’m pushing back is more on the wording and positioning than the idea itself.
Phrases like “pre-bundled native binaries” and “more optimized for Windows” are a bit unclear from a technical standpoint. Most of these components already ship as native Windows builds, so it’s hard to tell what’s actually being improved or optimized beyond packaging them together.
For newer developers or people who just want to get up and running quickly, I can absolutely see the appeal of something like this. My perspective is just that understanding how the stack fits together (Nginx/Apache, PHP, database, etc.) pays off pretty quickly once you move beyond simple setups.
So yeah — different tools for different workflows. If the goal is speed and reducing repetition, I get why something like Larabox exists. I just think the technical claims could be explained a bit more clearly.
DigitalArtisan wrote a reply+100 XP
6d ago
I develop on a Mac. Tag line:
Escape the complexity of Docker and WSL. Larabox provides isolated, pre-bundled native binaries for Nginx, PHP, MariaDB, Node.js and more optimized for Windows and live in seconds.
Pre-bundled native binaries...who is compiling these on or for Windows, there already native binaries.
Docker isn't complex, neither is WSL.
I am a bit old school, if you use Nginx or Apache, you should know how to install and configure them.
More optimized for windows, as to 'less optimized'? How...
DigitalArtisan liked a comment+100 XP
6d ago
DigitalArtisan was awarded Best Answer+1000 XP
1w ago
DigitalArtisan wrote a reply+100 XP
1w ago
DigitalArtisan wrote a reply+100 XP
1w ago
DigitalArtisan wrote a reply+100 XP
2w ago
DigitalArtisan wrote a reply+100 XP
2w ago
I’d recommend using PostgreSQL with PostGIS for GIS points.
- You get real location types (no need to store lat/lon separately)
- Easy queries like “find points within X distance”
- Fast with built-in spatial indexing
- Works well with GIS tools
Basically, it’s much simpler and more powerful than trying to handle coordinates yourself.
DigitalArtisan wrote a reply+100 XP
2w ago
80% of the work takes 20% of the time. The other 20% of the work takes 80% of the time.
What you are saying to me is that you have 5-10% of your project complete by AI, by stating that 90-95% of the goals are met, which is the easy stuff, and what about the other 5-10%?
Who is going to spend the rest of the 90-95% of the time that still needs to be done?
DigitalArtisan wrote a reply+100 XP
3w ago
DigitalArtisan wrote a reply+100 XP
4w ago
I would rename your Dashboard, as I think I know what is happening. The default dashboard is being loaded, as well as your dashboard, but they have the same name "Dashboard".
Change to the following and should work:
namespace App\Filament\Pages;
use Filament\Pages\Dashboard;
class CustomDashboard extends Dashboard
{
protected static ?string $title = 'Custom Dashboard';
protected ?string $heading = 'Custom Dashboard'';
}
This could be a bug or by design, I'm not sure. If the names do not match, it appears to load the dashboard twice.
Make sure the name of your Dashboard matches the title/heading, as I have shown above with CustomDashboard => "Custom Dashboard" will only show once in the menu, and the title/heading gets changed. If no heading is set, it will default to using title.
DigitalArtisan wrote a reply+100 XP
4w ago
DigitalArtisan wrote a reply+100 XP
1mo ago
MacOS Firefox 150.0b1 (aarch64)
Broken for a week for me - NS_ERROR_CORRUPTED_CONTENT:
The resource at “https://laracasts.com/fonts/shadow-noir/ShadowNoir.woff2” preloaded with link preload was not used within a few seconds. Make sure all attributes of the preload tag are set correctly. laracasts.com
The resource at “https://laracasts.com/build/assets/js/app-HWIx8gQn.js” preloaded with link preload was not used within a few seconds. Make sure all attributes of the preload tag are set correctly. laracasts.com
GET
https://laracasts.com/build/assets/js/Index-Z2l_0yzy.js
NS_ERROR_CORRUPTED_CONTENT
Loading module from “https://laracasts.com/build/assets/js/Index-Z2l_0yzy.js” was blocked because of a disallowed MIME type (“text/html”). laracasts.com
Uncaught (in promise) TypeError: error loading dynamically imported module: https://laracasts.com/build/assets/js/Index-Z2l_0yzy.js
vue https://laracasts.com/build/assets/js/app-HWIx8gQn.js:2
xe https://laracasts.com/build/assets/js/components-BxsoGmWQ.js:2
app-HWIx8gQn.js:2:21205
DigitalArtisan wrote a reply+100 XP
1mo ago
DigitalArtisan wrote a reply+100 XP
1mo ago
DigitalArtisan wrote a reply+100 XP
1mo ago
DigitalArtisan was awarded Best Answer+1000 XP
2mos ago
DigitalArtisan wrote a reply+100 XP
2mos ago
To clarify, I said replication is very common when you want a copy of your data. I wasn’t suggesting that syncing dev → prod in a shared hosting environment is common practice.
My point was simply that replication is widely used for maintaining data copies, and it’s a more appropriate term than “sync” in this context.
There are multiple ways to setup replication from a dev box without a static IP: VPN, SSH tunneling, Master/Slave, Pull/Push based, etc.
Anyways, I do not recommend what the OP is doing, nor would I want to.
I would use migrations and seeding to populate the production db.
DigitalArtisan wrote a reply+100 XP
2mos ago
DigitalArtisan wrote a reply+100 XP
2mos ago
DigitalArtisan wrote a reply+100 XP
2mos ago
DigitalArtisan wrote a reply+100 XP
2mos ago
I can see why it feels like a change, but the first-time confirmation actually makes sense. PsySH had a security issue (CVE‑2026‑25129) where it could unintentionally load and execute a .psysh.php file from the current directory. The dialog helps make users aware of this risk before running Tinker interactively.
It’s really just a one-time prompt, so after confirming, your usual workflow continues unchanged. I think it’s a small adjustment for a legitimate security reason, and having the library handle the prompt seems appropriate.
DigitalArtisan wrote a reply+100 XP
2mos ago
When a PHP bool is echo'd, it is automatically type casted to a string.
Now for database storage:
- PostgreSQL: Has a true, native BOOLEAN data type (1 byte). Accepts values like
true/false,yes/no,1/0,'t'/'f','y'/'n', and supportsNULL. - MySQL: No native boolean type. BOOLEAN/BOOL are aliases for TINYINT(1). Uses
1for true and0for false; any non-zero value evaluates as true. - SQLite: No boolean storage class. Uses INTEGER (NUMERIC affinity), where
1= true and0= false.TRUEandFALSEare just aliases for1and0.
Key takeaway: PostgreSQL is SQL-standard with a real boolean type, while MySQL and SQLite represent booleans using integers—important to remember for cross-database compatibility.
PHP stores booleans as booleans, not numbers
-
echo converts them to strings:
-
true → "1"
-
false → ""
-
Use var_dump() to see the real type
Cast to (int) if you truly want 0 / 1
DigitalArtisan wrote a reply+100 XP
3mos ago
The best place to deploy your application depends on several factors:
- Budget
- Your experience, knowledge, and comfort level with hosting and deploying Laravel applications
- Your test benchmarks and performance results
To move forward, here are a few questions for you:
- What is your budget?
- What is your experience / comfort level with hosting and deploying Laravel apps?
- What do your performance and load-testing benchmarks show so far?
DigitalArtisan wrote a reply+100 XP
3mos ago
When you prompt AI correclty, you should get this:
The Correct Way to Throttle Emails in Laravel with Jobs and Queues
Step 1: Use Queue Middleware (RateLimited)
This is the core part of the solution. To throttle emails, you add middleware to your job to enforce the rate limit:
use Illuminate\Queue\Middleware\RateLimited;
class SendEmailJob implements ShouldQueue
{
public function middleware()
{
return [
new RateLimited('mailing-list'),
];
}
public function handle()
{
// Logic to send the email
}
}
This ensures the job respects the rate limit defined later.
Step 2: Define Rate Limit in AppServiceProvider
In your AppServiceProvider@boot(), you define the rate-limiting logic:
use Illuminate\Cache\RateLimiter;
use Illuminate\Cache\RateLimiting\Limit;
public function boot()
{
RateLimiter::for('mailing-list', function (SendEmailJob $job) {
return Limit::perMinute(10)->by($job->userId);
});
}
This sets a 10 emails per minute limit for each user (based on userId).
Step 3: Use Redis as Queue and Cache Driver
Redis is required for distributed rate-limiting. Ensure your .env file is configured like this:
QUEUE_CONNECTION=redis
CACHE_DRIVER=redis
Redis ensures the limit is applied safely across multiple workers or instances.
Step 4: Run the Queue Worker
Run the queue worker to process the jobs:
php artisan queue:work redis
This worker will now respect the rate limit (10 emails per minute in this example) and retry any jobs that exceed the rate limit.
Key Points:
- Only one correct method, with 4 steps to implement.
- Redis is mandatory for safe, scalable rate-limiting.
- RateLimited middleware handles job throttling.
- No emails are dropped—they’re released back to the queue when the limit is hit.
DigitalArtisan wrote a reply+100 XP
3mos ago
Its 2026 now, that article is OLD.
Here is a current AI response:
Laravel + Next.js vs Laravel + Inertia + React for SEO
1. Laravel + Next.js
Pros:
- Server-Side Rendering (SSR): Next.js renders pages on the server before sending them to the browser, which is great for SEO.
- SEO Optimized: Built-in features like image optimization, fast page loads, and prefetching help with SEO.
- Static Site Generation (SSG): You can generate static pages, improving performance and SEO.
Cons:
- More Complex: Managing both Laravel and Next.js can be tricky. You'll need separate setups and integration.
- API Management: You'll need to set up APIs for Laravel to serve data to Next.js.
2. Laravel + Inertia + React
Pros:
- Simple Integration: Keeps everything within Laravel, no need for a separate front-end framework.
- Server-side Rendering: You can still use Laravel to serve initial HTML, making it SEO-friendly.
Cons:
- Client-Side Rendering (CSR): By default, Inertia uses React on the client-side, which can be problematic for SEO if not handled correctly.
- Manual SEO Work: You may need additional tools like React Helmet or implement SSR manually for optimal SEO.
Conclusion:
- For better SEO out of the box: Go with Laravel + Next.js. It has built-in SSR and other SEO optimizations.
- For simplicity: Laravel + Inertia + React works if you don't mind handling SEO optimization yourself, but it might require more effort to get it right.
DigitalArtisan wrote a reply+100 XP
3mos ago
First, you need a Learning Management System (LMS) that supports Learning Tools Interoperability (LTI).
LTI is a standard defined by 1EdTech (formerly IMS Global) that allows an LMS to securely integrate external learning tools. It’s often described as an API, but in practice it’s a standards-based protocol for authentication, launch, and grade/roster exchange between systems.
LTI Roles
LTI involves two roles:
- LTI Platform (formerly “Consumer”) — the LMS (e.g., Canvas, Moodle, D2L)
- LTI Tool (formerly “Provider”) — the external application being launched
LTI Versions
As of today, LTI 1.3 with LTI Advantage is the recommended version. It replaces older LTI 1.1 implementations and is based on OAuth 2.0 and OpenID Connect.
Official tutorials and specifications:
https://www.imsglobal.org/spec/lti/v1p3/
Laravel and LTI
Regarding Laravel:
There still aren’t any widely adopted, full-featured Laravel-based LMS platforms that natively act as an LTI Platform. However, Laravel works well for building LTI Tools, and there are PHP/Laravel libraries that help implement LTI 1.3 launches, deep linking, and grade passback.
In a previous role, I used Desire2Learn (D2L / Brightspace) extensively, which has solid LTI support:
https://docs.valence.desire2learn.com/res/lti.html
Common Approach Today
So today, the common approach is:
- Use an established LMS (Canvas, Moodle, D2L, etc.) as the LTI Platform
- Build custom learning tools in Laravel as LTI Tools
DigitalArtisan wrote a reply+100 XP
3mos ago
There’s a lot of debate around this.
From my experience, AI significantly reduces time to production—projects that once took months or weeks can now be completed in days or hours. Refactoring is faster as well.
That said, when AI breaks down, a developer still has to understand and fix the code. I’ve heard of teams abandoning AI-generated projects because no one fully understood the codebase.
Bottom line: AI is a productivity tool, not a replacement. It can help, but it’s not a silver bullet.
Marketing is still marketing—SEO, targeting, and messaging matter. Relying entirely on AI for marketing content can be risky if it gets things wrong.
Used carefully, AI is valuable. Used blindly, it can create problems.
DigitalArtisan wrote a reply+100 XP
3mos ago
When people say you only need a “high-level” understanding of DevOps, cloud, and system design for junior roles, they mean knowing what these things are, why they’re used, and how they fit into the web development process, not being able to fully build or operate production infrastructure yourself. You should be able to explain concepts like CI/CD, cloud hosting, authentication, scalability, and how a backend system is structured, but you’re not expected to manage Kubernetes, complex AWS networking, or large-scale distributed systems.
For projects, the backend projects on roadmap.sh are more than enough for junior roles if done properly. “Real-world” doesn’t mean huge SaaS platforms or advanced AI apps—it means projects with realistic business logic, data relationships, authentication, validation, error handling, and clear tradeoffs. A few well-built APIs (auth system, CRUD app, blog, e-commerce, task manager) that you understand deeply are far more impressive than an over-scoped SaaS or Indeed-style clone.
It may be worth considering a more structured or formal learning route to help solidify the fundamentals.
DigitalArtisan wrote a reply+100 XP
3mos ago
Hiring teams primarily look for learning ability, strong fundamentals, and practical problem-solving skills—not senior-level infrastructure expertise.
To get a junior Laravel job you should build a few solid, real-world Laravel projects that demonstrate core PHP/Laravel knowledge (CRUD, authentication, validation, relationships, testing, and Git), basic SQL, and simple deployment, while understanding DevOps, cloud, and system design concepts only at a high level rather than fully implementing them.
DigitalArtisan wrote a reply+100 XP
3mos ago
DigitalArtisan wrote a reply+100 XP
3mos ago
Exactly, not working. You have these routes:
GET|HEAD payers .............. payers.index › PayerController@index
PUT payers .............. payers.store › PayerController@store
PATCH payers/{payer} ...... payers.update › PayerController@update
DELETE payers/{payer} ...... payers.destroy › PayerController@destroy
I would think you need this:
GET|HEAD payers .............. payers.index › PayerController@index
POST payers .............. payers.store › PayerController@store
PUT|PATCH payers/{payer} ...... payers.update › PayerController@update
DELETE payers/{payer} ...... payers.destroy › PayerController@destroy
Your error message is correct for your routing.
DigitalArtisan wrote a reply+100 XP
3mos ago
DigitalArtisan wrote a reply+100 XP
3mos ago
For completeness, here is the tutorial for creating a user resource in Filament 4:
Tutorial: Creating a User Resource in Filament 4
This tutorial walks you through creating a comprehensive User Resource in Filament 4 to manage users in your admin panel, including viewing, creating, editing, and deleting users with custom fields.
Overview
We'll create a User Resource that provides a full CRUD (Create, Read, Update, Delete) interface for managing users, including:
- A table view with searchable and sortable columns
- A form with organized sections for user information and authentication
- Proper password handling (required on create, optional on edit)
- Custom fields (phone, company) from your registration form
- Navigation integration
Prerequisites
- Laravel 12.x
- Filament 4.x installed
- A User model with custom fields (phone, company) already migrated
- A Filament panel already configured
Step-by-Step Implementation
Step 1: Generate the User Resource
Use Filament's artisan command to generate a complete resource with all necessary files:
php artisan filament:make-resource User --generate
This command will create:
app/Filament/Resources/Users/UserResource.php- Main resource classapp/Filament/Resources/Users/Tables/UsersTable.php- Table configurationapp/Filament/Resources/Users/Schemas/UserForm.php- Form configurationapp/Filament/Resources/Users/Pages/ListUsers.php- List pageapp/Filament/Resources/Users/Pages/CreateUser.php- Create pageapp/Filament/Resources/Users/Pages/EditUser.php- Edit page
The --generate flag automatically generates the table, form, and page classes.
Step 2: Configure the Main Resource Class
Edit app/Filament/Resources/Users/UserResource.php to customize navigation and labels:
<?php
namespace App\Filament\Resources\Users;
use App\Filament\Resources\Users\Pages\CreateUser;
use App\Filament\Resources\Users\Pages\EditUser;
use App\Filament\Resources\Users\Pages\ListUsers;
use App\Filament\Resources\Users\Schemas\UserForm;
use App\Filament\Resources\Users\Tables\UsersTable;
use App\Models\User;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
class UserResource extends Resource
{
protected static ?string $model = User::class;
protected static ?string $navigationLabel = 'Users';
protected static ?string $modelLabel = 'User';
protected static ?string $pluralModelLabel = 'Users';
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedUserGroup;
public static function form(Schema $schema): Schema
{
return UserForm::configure($schema);
}
public static function table(Table $table): Table
{
return UsersTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListUsers::route('/'),
'create' => CreateUser::route('/create'),
'edit' => EditUser::route('/{record}/edit'),
];
}
}
Key Points:
$model- Links the resource to your User model$navigationLabel- Label shown in the sidebar navigation$modelLabel/$pluralModelLabel- Labels used in page titles and actions$navigationIcon- Icon displayed in navigation (Heroicon::OutlinedUserGroup)form()- Delegates form configuration to UserForm classtable()- Delegates table configuration to UsersTable classgetPages()- Defines the routes for list, create, and edit pages
Step 3: Configure the Table View
Edit app/Filament/Resources/Users/Tables/UsersTable.php to customize the columns displayed in the list view:
<?php
namespace App\Filament\Resources\Users\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class UsersTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->searchable()
->sortable(),
TextColumn::make('email')
->label('Email')
->searchable()
->sortable()
->copyable(),
TextColumn::make('phone')
->label('Phone')
->searchable()
->toggleable(),
TextColumn::make('company')
->label('Company')
->searchable()
->toggleable(),
TextColumn::make('email_verified_at')
->label('Verified')
->dateTime()
->sortable()
->toggleable(),
TextColumn::make('created_at')
->label('Created')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('updated_at')
->label('Updated')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}
Column Features:
searchable()- Enables search functionality for the columnsortable()- Allows sorting by clicking the column headercopyable()- Adds a copy button to quickly copy the valuetoggleable()- Allows users to show/hide the columntoggleable(isToggledHiddenByDefault: true)- Column is hidden by defaultlabel()- Custom label for the column headerdateTime()- Formats the value as a date/time
Table Actions:
recordActions()- Actions available for each row (Edit button)toolbarActions()- Bulk actions available in the toolbar (Delete multiple)
Step 4: Configure the Form
Edit app/Filament/Resources/Users/Schemas/UserForm.php to create an organized form with sections:
<?php
namespace App\Filament\Resources\Users\Schemas;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Illuminate\Validation\Rules\Password;
class UserForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Section::make('User Information')
->schema([
TextInput::make('name')
->required()
->maxLength(255),
TextInput::make('email')
->label('Email address')
->email()
->required()
->maxLength(255)
->unique(ignoreRecord: true),
TextInput::make('phone')
->label('Phone Number')
->tel()
->maxLength(20)
->unique(ignoreRecord: true),
TextInput::make('company')
->label('Company/Organization')
->maxLength(255),
])
->columns(2),
Section::make('Authentication')
->schema([
TextInput::make('password')
->password()
->label(fn ($livewire) => $livewire instanceof \App\Filament\Resources\Users\Pages\CreateUser ? 'Password' : 'New Password (leave blank to keep current)')
->required(fn ($livewire) => $livewire instanceof \App\Filament\Resources\Users\Pages\CreateUser)
->rule(Password::default())
->dehydrated(fn ($state, $livewire) => $livewire instanceof \App\Filament\Resources\Users\Pages\CreateUser ? true : filled($state))
->dehydrateStateUsing(fn ($state) => filled($state) ? \Illuminate\Support\Facades\Hash::make($state) : null),
DateTimePicker::make('email_verified_at')
->label('Email Verified At'),
])
->columns(2),
]);
}
}
Key Features:
-
Sections: Organize form fields into logical groups
Section::make('Title')- Creates a collapsible section->columns(2)- Displays fields in a 2-column layout
-
Field Validation:
->required()- Makes the field mandatory->email()- Validates email format->tel()- Optimizes input for telephone numbers->maxLength(255)- Sets maximum character length->unique(ignoreRecord: true)- Ensures uniqueness, ignoring current record on edit
-
Password Handling:
->required(fn ($livewire) => ...)- Required only when creating (not editing)->dehydrated(...)- Only saves password if provided (on edit) or always (on create)->dehydrateStateUsing(...)- Automatically hashes the password before saving- Dynamic label changes based on create vs edit context
-
Important Note on Namespaces:
Sectionis inFilament\Schemas\Components\Section(not Forms namespace)TextInputandDateTimePickerare inFilament\Forms\Components
Step 5: Customize Page Classes (Optional)
The page classes are automatically generated and work out of the box. You can customize them if needed:
CreateUser.php:
<?php
namespace App\Filament\Resources\Users\Pages;
use App\Filament\Resources\Users\UserResource;
use Filament\Resources\Pages\CreateRecord;
class CreateUser extends CreateRecord
{
protected static string $resource = UserResource::class;
}
EditUser.php:
<?php
namespace App\Filament\Resources\Users\Pages;
use App\Filament\Resources\Users\UserResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditUser extends EditRecord
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}
The EditUser class includes a delete action in the header, allowing you to delete users from the edit page.
Understanding the Structure
Resource Organization
Filament 4 uses a modular structure for resources:
app/Filament/Resources/Users/
├── UserResource.php # Main resource class
├── Tables/
│ └── UsersTable.php # Table configuration
├── Schemas/
│ └── UserForm.php # Form configuration
└── Pages/
├── ListUsers.php # List page
├── CreateUser.php # Create page
└── EditUser.php # Edit page
This separation allows for:
- Better code organization
- Reusability of table and form configurations
- Easier maintenance and testing
Auto-Discovery
Filament automatically discovers resources in app/Filament/Resources and adds them to navigation. No manual registration needed!
Customization Options
Adding More Columns to Table
Add additional columns to display more information:
TextColumn::make('phone')
->label('Phone')
->searchable()
->toggleable()
->formatStateUsing(fn ($state) => $state ?: 'N/A'), // Show "N/A" if empty
Adding Filters
Add filters to help users find specific records:
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\Filter;
// In UsersTable.php ->filters([])
->filters([
SelectFilter::make('email_verified_at')
->label('Email Status')
->options([
'verified' => 'Verified',
'unverified' => 'Unverified',
])
->query(function ($query, $state) {
if ($state['value'] === 'verified') {
return $query->whereNotNull('email_verified_at');
}
if ($state['value'] === 'unverified') {
return $query->whereNull('email_verified_at');
}
}),
])
Adding Bulk Actions
Add more bulk actions for managing multiple users:
use Filament\Actions\BulkAction;
->toolbarActions([
BulkActionGroup::make([
BulkAction::make('verify_emails')
->label('Verify Selected Emails')
->icon('heroicon-o-check-circle')
->action(function ($records) {
$records->each(function ($user) {
$user->markEmailAsVerified();
});
})
->requiresConfirmation(),
DeleteBulkAction::make(),
]),
])
Customizing Form Sections
Add more sections or reorganize fields:
Section::make('Contact Information')
->schema([
TextInput::make('phone'),
TextInput::make('company'),
])
->collapsible(), // Makes section collapsible
Adding Custom Actions
Add custom actions to the table or form:
// In UsersTable.php
->recordActions([
EditAction::make(),
Action::make('impersonate')
->label('Impersonate')
->icon('heroicon-o-user')
->action(function ($record) {
// Impersonation logic
}),
])
Customizing Navigation
Control how the resource appears in navigation:
// In UserResource.php
protected static ?int $navigationSort = 1; // Position in sidebar
protected static ?string $navigationGroup = 'User Management'; // Group resources
protected static bool $shouldRegisterNavigation = true; // Show/hide in nav
Testing
- Navigate to
/adminin your browser - Look for "Users" in the sidebar navigation
- Click it to see the list of users
- Test the following:
- List View: Verify all columns display correctly
- Search: Try searching by name, email, phone, or company
- Sorting: Click column headers to sort
- Create: Click "New User" and fill out the form
- Edit: Click the edit icon on a user row
- Delete: Delete a user from the edit page
- Password: Verify password is required on create, optional on edit
Troubleshooting
Resource Not Appearing in Navigation
- Clear Filament cache:
php artisan filament:optimize-clear - Clear config cache:
php artisan config:clear - Verify the resource is in
app/Filament/Resourcesdirectory - Check that
$shouldRegisterNavigationis not set tofalse
Form Fields Not Saving
- Verify fields are in the User model's
$fillablearray - Check that database columns exist (run migrations)
- Ensure
dehydrated()is not set tofalseunless intentional
Password Issues
- Password field should use
->dehydrateStateUsing()to hash the password - On edit, password should only be dehydrated if a value is provided
- Use
->required(fn ($livewire) => ...)to make it conditional
Namespace Errors
Sectionis inFilament\Schemas\Components\Section- Form components (
TextInput,DateTimePicker) are inFilament\Forms\Components - Table columns are in
Filament\Tables\Columns
Summary
You've successfully created a comprehensive User Resource in Filament 4 with:
- ✅ Full CRUD functionality (Create, Read, Update, Delete)
- ✅ Organized form with sections for user information and authentication
- ✅ Searchable and sortable table columns
- ✅ Custom fields (phone, company) integrated
- ✅ Smart password handling (required on create, optional on edit)
- ✅ Automatic password hashing
- ✅ Navigation integration
- ✅ Bulk actions for managing multiple users
- ✅ Professional UI with toggleable columns
The User Resource is now available in your Filament admin panel and automatically appears in the navigation sidebar. Users registered through your custom registration page will be visible and manageable through this interface.
DigitalArtisan wrote a reply+100 XP
3mos ago
Here is a tutorial that I just created:
Tutorial: Creating a Custom Registration Page in Filament 4
This tutorial walks you through creating a custom registration page in Filament 4 with additional fields beyond the standard name, email, and password.
Overview
We'll create a custom registration page that extends Filament's built-in registration functionality to include:
- Standard fields: Name, Email, Password, Password Confirmation
- Custom fields: Phone Number, Company/Organization, Terms and Conditions acceptance
Prerequisites
- Laravel 12.x
- Filament 4.x installed
- A Filament panel already configured
Step-by-Step Implementation
Step 1: Generate the Custom Registration Page
First, we need to create a custom registration page class. Filament doesn't have a specific command for auth pages, so we'll use the general page command and then customize it.
php artisan filament:make-page Auth/Register
This creates a basic page at app/Filament/Pages/Auth/Register.php. We'll modify it to extend Filament's base Register class.
Step 2: Customize the Registration Page
Edit app/Filament/Pages/Auth/Register.php to extend Filament's base Register class and add custom fields:
<?php
namespace App\Filament\Pages\Auth;
use Filament\Auth\Pages\Register as BaseRegister;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Schema;
class Register extends BaseRegister
{
public function form(Schema $schema): Schema
{
return parent::form($schema)
->components([
$this->getNameFormComponent(),
$this->getEmailFormComponent(),
$this->getPhoneFormComponent(),
$this->getCompanyFormComponent(),
$this->getPasswordFormComponent(),
$this->getPasswordConfirmationFormComponent(),
$this->getTermsFormComponent(),
]);
}
protected function getPhoneFormComponent(): Component
{
return TextInput::make('phone')
->label('Phone Number')
->tel()
->maxLength(20)
->unique($this->getUserModel());
}
protected function getCompanyFormComponent(): Component
{
return TextInput::make('company')
->label('Company/Organization')
->maxLength(255);
}
protected function getTermsFormComponent(): Component
{
return Checkbox::make('terms')
->label('I agree to the terms and conditions')
->required()
->accepted()
->dehydrated(false);
}
/**
* Remove the 'terms' field from data before registration
* since it's only used for validation, not storage.
*
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
protected function mutateFormDataBeforeRegister(array $data): array
{
// Remove terms field as it's only for validation
unset($data['terms']);
return $data;
}
/**
* Hook called after user registration is complete.
* This method is called via Filament's hook system.
* You can add custom logic here like sending welcome emails,
* assigning roles, etc.
*
* To use this hook, you can also listen to the Registered event
* in your EventServiceProvider or use Filament's hook system.
*/
protected function afterRegister(): void
{
// This hook is called after registration
// You can access the registered user via the form model
// Example: $user = $this->form->getRecord();
// Example: You could send a welcome email here
// Mail::to($user->email)->send(new WelcomeMail($user));
// Example: You could assign a default role here
// $user->assignRole('user');
}
}
Key Points:
- Extends
Filament\Auth\Pages\Registerto inherit all base functionality - Overrides
form()to add custom fields alongside standard ones getPhoneFormComponent(): Phone field with tel input type and unique validationgetCompanyFormComponent(): Optional company fieldgetTermsFormComponent(): Required checkbox withdehydrated(false)so it's not saved to databasemutateFormDataBeforeRegister(): Removes thetermsfield before saving (it's only for validation)afterRegister(): Hook for post-registration logic (emails, role assignment, etc.)
Step 3: Enable Registration in Panel Provider
Update app/Providers/Filament/AdminPanelProvider.php to enable registration and point to your custom page:
Add the import:
use App\Filament\Pages\Auth\Register;
Update the panel configuration:
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->login()
->registration(Register::class) // Add this line
->colors([
'primary' => Color::Amber,
])
// ... rest of configuration
}
The ->registration(Register::class) method enables registration and tells Filament to use your custom Register page.
Step 4: Update the User Model
Add the new fields to the $fillable array in app/Models/User.php:
protected $fillable = [
'name',
'email',
'password',
'phone', // Add this
'company', // Add this
];
Note: We don't add terms because it's only used for validation, not stored in the database.
Step 5: Create Database Migration
Create a migration to add the new columns to the users table:
php artisan make:migration add_phone_and_company_to_users_table --table=users
Edit the migration file:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('phone', 20)->nullable()->after('email');
$table->string('company')->nullable()->after('phone');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['phone', 'company']);
});
}
};
Key Points:
- Both fields are nullable (optional)
- Phone is limited to 20 characters
- Company is a standard string field
- The
down()method properly reverses the migration
Step 6: Run the Migration
Execute the migration:
php artisan migrate
Understanding the Implementation
Form Components
-
Phone Field:
- Uses
tel()input type for mobile keyboard optimization - Has
unique()validation to prevent duplicate phone numbers - Maximum length of 20 characters
- Uses
-
Company Field:
- Simple text input
- Optional (no
required()call) - Maximum length of 255 characters
-
Terms Field:
- Required checkbox using
accepted()validation dehydrated(false)prevents it from being saved to the database- Only used for validation purposes
- Required checkbox using
Data Mutation
The mutateFormDataBeforeRegister() method is crucial for fields that shouldn't be saved. Since terms is only for validation, we remove it before the user is created.
Post-Registration Hooks
The afterRegister() method is called after a user is successfully registered. This is where you can:
- Send welcome emails
- Assign default roles
- Create related records
- Trigger notifications
Alternatively, you can listen to Filament's Registered event in your EventServiceProvider.
Testing
- Navigate to
/admin/register(or your panel path +/register) - Fill out the registration form with:
- Name
- Email (must be unique)
- Phone (optional, but must be unique if provided)
- Company (optional)
- Password
- Password confirmation
- Terms acceptance (required)
- Submit the form
- Verify the user is created with all provided data
- Check that the
termsfield is NOT saved to the database
Customization Options
Adding More Fields
To add additional fields:
- Create a new component method (e.g.,
getAddressFormComponent()) - Add it to the
form()method's components array - Add the field to the User model's
$fillablearray - Create a migration if it's a database field
Making Fields Required
Add ->required() to any field component:
protected function getCompanyFormComponent(): Component
{
return TextInput::make('company')
->label('Company/Organization')
->required() // Add this
->maxLength(255);
}
Custom Validation
Add validation rules to any field:
protected function getPhoneFormComponent(): Component
{
return TextInput::make('phone')
->label('Phone Number')
->tel()
->maxLength(20)
->unique($this->getUserModel())
->rules(['regex:/^\+?[1-9]\d{1,14}$/']); // E.164 format
}
Custom Post-Registration Logic
Implement the afterRegister() method with your custom logic:
protected function afterRegister(): void
{
$user = $this->form->getRecord();
// Send welcome email
Mail::to($user->email)->send(new WelcomeMail($user));
// Assign default role
$user->assignRole('customer');
// Create related profile
$user->profile()->create([
'phone' => $user->phone,
'company' => $user->company,
]);
}
Troubleshooting
Registration Page Not Showing
- Ensure
->registration(Register::class)is added to your panel provider - Clear config cache:
php artisan config:clear - Clear route cache:
php artisan route:clear
Fields Not Saving
- Verify fields are in the User model's
$fillablearray - Check that the migration has been run
- Ensure fields are not set to
dehydrated(false)unless intentional
Validation Errors
- Check that validation rules are correct
- Ensure unique constraints match your database structure
- Verify required fields have
->required()in the component
Summary
You've successfully created a custom Filament 4 registration page with:
- ✅ Extended Filament's base Register class
- ✅ Added custom form fields (phone, company, terms)
- ✅ Configured the panel to use the custom registration page
- ✅ Updated the User model to accept new fields
- ✅ Created and ran database migrations
- ✅ Implemented custom validation and post-registration logic
The registration page is now available at /admin/register and fully integrated with Filament's authentication system.
DigitalArtisan wrote a reply+100 XP
4mos ago
DigitalArtisan wrote a reply+100 XP
4mos ago
DigitalArtisan wrote a reply+100 XP
4mos ago
Basically no, and you probably would never want a 'free' domain name, as that registrar will probably lock you into paying, and or they will keep it and resell the domain name for a fee.
A domain name is simply a human-readable address on the internet. It maps a name to an IP address (and vice versa) and does not require a hosting provider by itself.
A hosting provider, on the other hand, is what stores and serves your website’s content.
It’s generally best to keep these two separate. You can purchase a domain name from a reputable registrar for around $10–$20 per year. After that, you can choose a hosting provider (or set up your own server) and point the domain to wherever your content is hosted.
DigitalArtisan liked a comment+100 XP
4mos ago
DigitalArtisan wrote a reply+100 XP
4mos ago
It completely depends on the project and the team.
Before Laravel, in the early 2000s, I used the Repository pattern extensively for enterprise projects. Nearly 25 years later, that same codebase is still in use and has been reused across multiple frameworks. At the time, I evaluated the Active Record pattern and found that it did not align with the project’s goals.
Laravel uses Eloquent by default, which largely follows the Active Record pattern and was chosen intentionally.
If the project is a large, long-living enterprise application that predates the Laravel framework, I would agree with your senior team.
DigitalArtisan wrote a reply+100 XP
4mos ago
I created MDA to solve a real problem
No you didn't.
The Object Management Group® Standards Development Organization (OMG® SDO) did.
OMG members voted to establish the MDA (Model Driven Architecture) as the base architecture for our organization's standards in late 2001.
You just happened to implement a Laravel version of MDA, that's all.
DigitalArtisan wrote a reply+100 XP
4mos ago
Or install it the way the instructions say to: https://docs.vapor.build/introduction#installing-the-vapor-cli
Either globally as you have done, or per-project.
DigitalArtisan wrote a reply+100 XP
4mos ago
HTML with CSS to PDF never works well. Good luck if you rely on JS views.
The best for me has been as mentioned above the barryvdh/laravel-dompdf. I use Blade views, ~/resources/views/print/pdf/.... Minimal CSS, overridden from the HTML pages, pretend your in the 90s writing HTML email. You cannot do fancy things, as the DOMPDF library does not support all CSS.
I've been doing HTML to PDF for just over 15 years.
Other options are using libreoffice headless. Best is to design your PDF documents, then dynamically fill them.
Best way to test is using your web browsers Print Preview -> Print to PDF. From what I remember, Firefox was the best.
DigitalArtisan wrote a reply+100 XP
4mos ago
You said quoate but Nginx on the host cannot directly access it. This has caused issues where assets like /logo.png work locally in Docker Desktop but fail in production unless accessed via /public/logo.png or through a Laravel route.
Sounds like a configuration problem.
What do you have containerized?
DigitalArtisan wrote a reply+100 XP
4mos ago
DigitalArtisan wrote a reply+100 XP
4mos ago
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
{
DigitalArtisan wrote a reply+100 XP
4mos ago
DigitalArtisan wrote a reply+100 XP
4mos ago
DigitalArtisan wrote a reply+100 XP
4mos ago
DigitalArtisan wrote a reply+100 XP
4mos ago
This is a common gotcha when using Inertia.js on mobile browsers (especially Safari on iOS and some Android OEM browsers).
Your Inertia navigation didn’t create real browser history entries. The native back button sometimes closes the browser tab instead of navigating back through Inertia’s history.
More of an issue with IOS.