Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

james1232's avatar

None CRUD Operation While Using Resources Controllers

I'm currently working on a tasks application and aiming to keep my controllers as small and uniform as possible by primarily using resource controllers for my main models. I really appreciate the simplicity and structure they offer.

However, I'm running into some challenges and was hoping to get some advice.

One of my main concerns is how to handle assigning tasks and roles to users. I'm unsure where this logic should reside. Should I create an AssignmentController, or is there a better approach? Would such a controller only handle task assignments, or could it also manage other relationships like assigning roles to users? Do I need separate controllers for each type of relationship to keep things clean and focused?

Another challenge is handling actions that don't fit into the standard CRUD operations, like publishing a task, marking it as complete, or performing bulk updates. I'm wondering how to implement these non-CRUD actions within the resource controller pattern. Is it acceptable to add custom methods to resource controllers for these actions, or is there a more appropriate pattern to follow?

I'm also considering how to implement pagination, sorting, and filtering within the index methods of resource controllers without bloating them. I want to include these features while adhering to RESTful principles.

Lastly, I'm unsure how to perform bulk actions, like updating or deleting multiple tasks at once, within the resource controller pattern. Should I create custom methods or controllers for bulk operations, or is there a standard approach?

I'm striving to maintain a clean, maintainable, and scalable architecture while sticking to RESTful principles.

0 likes
8 replies
LaryAI's avatar
Level 58

To address your concerns while maintaining a clean and RESTful architecture, let's break down each of your questions:

  1. Handling Assignments (Tasks and Roles):

    • Separate Controllers: It's a good practice to create separate controllers for different types of relationships. For example, you could have a TaskAssignmentController for handling task assignments and a RoleAssignmentController for role assignments. This keeps your logic focused and controllers small.
    • Single Responsibility Principle: Each controller should have a single responsibility. If you find that a controller is handling multiple types of assignments, consider splitting it into more focused controllers.
  2. Non-CRUD Actions (e.g., Publishing, Completing Tasks):

    • Custom Methods in Resource Controllers: It's acceptable to add custom methods to resource controllers for non-CRUD actions. For example, you can add methods like publish or complete to your TaskController.
    • Route Definitions: Use route definitions to map these actions. For example:
      Route::post('tasks/{task}/publish', [TaskController::class, 'publish'])->name('tasks.publish');
      Route::post('tasks/{task}/complete', [TaskController::class, 'complete'])->name('tasks.complete');
      
  3. Pagination, Sorting, and Filtering:

    • Service Classes or Query Scopes: To keep your index methods clean, consider using service classes or query scopes. These can encapsulate the logic for pagination, sorting, and filtering.
    • Example of Query Scopes:
      class Task extends Model
      {
          public function scopeFilter($query, $filters)
          {
              return $query->when($filters['status'] ?? null, function ($query, $status) {
                  $query->where('status', $status);
              });
          }
      }
      
    • Controller Usage:
      public function index(Request $request)
      {
          $tasks = Task::filter($request->only('status'))->paginate(10);
          return view('tasks.index', compact('tasks'));
      }
      
  4. Bulk Actions:

    • Custom Methods for Bulk Operations: You can create custom methods in your controllers for handling bulk operations. For example, a bulkUpdate or bulkDelete method.
    • Route Definitions for Bulk Actions:
      Route::post('tasks/bulk-update', [TaskController::class, 'bulkUpdate'])->name('tasks.bulkUpdate');
      Route::delete('tasks/bulk-delete', [TaskController::class, 'bulkDelete'])->name('tasks.bulkDelete');
      

By following these guidelines, you can maintain a clean, maintainable, and scalable architecture while adhering to RESTful principles. Each controller remains focused on a specific responsibility, and custom actions are clearly defined and organized.

MouteeSabouni's avatar

I would suggest using an intermediary relationship model (e.g., TaskAssignment or RoleAssignment) and a dedicated controller like TaskAssignmentController. This way, the responsibility is focused, and you avoid cluttering the main resource controllers. The same could be done for roles if the logic becomes too much for one controller.

As for handling non-CRUD actions (Publishing and completing tasks), It’s fine to add custom methods for these actions within resource controllers as long as they follow RESTful conventions. You can use custom routes like: POST /tasks/{task}/publish for publishing. PATCH /tasks/{task}/complete for marking tasks as complete. Just make sure these actions are well-documented and follow REST semantics (POST or PATCH for state changes).

Another option is creating a controller specifically for task state changes, like TaskStateController, with methods like publish() and complete(). This helps keep your resource controllers purely focused on CRUD while managing complex states separately.

To prevent bloating your index methods with too much logic, use request filters to extract pagination, sorting, and filtering logic into separate classes. Laravel’s Query Scopes and Eloquent Builder Macros are good patterns for this.

Lastly, you can create custom methods like bulkUpdate or bulkDelete within your resource controllers. For example:

public function bulkUpdate(Request $request)
{
    Task::whereIn('id', $request->ids)->update($request->only(['status', 'assigned_to']));
    return response()->json(['message' => 'Tasks updated successfully']);
}

POST /tasks/bulk-update DELETE /tasks/bulk-delete

However, If you find that bulk operations are common, you could create a TaskBulkController to handle all bulk actions, making your main TaskController purely focused on single-task CRUD.

Sorry if the reply was long, but I tried to cover all the topics and answer all your questions. Please, mark this reply as best if you find it helpful. Thank you.

james1232's avatar

@Moutee if I'm using that many controllers I feel like I should just switch to actions.

1 like
james1232's avatar

@Snapey There are some really interesting ideas in here But I'm not sure how to handle the idea of making the relationship between models it's own model.

Let's take for example a events project where a user can create an event and invite other users to this event.

So if I'm going to create a User, Event, and UserEventOwnership Model, Controller, etc..

class User extends Authenticatable
{
	// default user model
    public function ownedEvents()
    {
        return $this->hasManyThrough(Event::class, UserEventOwnership::class);
    }
}
class Event extends Model
{
    protected $fillable = [
        'name',
        'description'
    ];
    public function owner()
    {
        return $this->belongsToMany(User:class, 'user_event_ownership')->withTimestamps();
    }
}
class UserEventOwnership extends Model
{
    use HasFactory;
    protected $fillable = [
        'user_id',
        'event_id'
    ];


    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function event()
    {
        return $this->belongsTo(Event::class);
    }


}

Then what do I do when I create a event and set the owner to the auth user? Would I just call the UserEventOwnership controller when making the event and Event:create in the UserEventOwnership Controller. Making the store method in the Event controller empty. Or maybe add a check box on the frontend asking if the user would like to be the owner of this event and if true then call the UserEventOwnership model from the Event:store passing in the auth user id and the event id?

like this:

Event Model:
public function store(Request $request)
{
    $validated = $request->validate([
        'name' => 'required|string|max:255',
        'description' => 'required|string',
    ]);

    $event = Event::create($validated);

    if ($request->has('is_owner')) {
        UserEventOwnership::create([
            'user_id' => auth()->id(),
            'event_id' => $event->id,
        ]);
    }

    return redirect()->route('events.index')->with('success', 'Event created successfully!');
}

Or maybe Not even have a checkbox and just assign the creating user as the owner

    $event = Event::create($validated);

        UserEventOwnership::create([
            'user_id' => auth()->id(),
            'event_id' => $event->id,
        ]);

If I do it like this then I guess the UserEventOwnership Controller is useless unless I want to use it for transferring event ownership or adding a user as an owner on an event.

james1232's avatar

I think I might have made a mistake here of creating a UserEventOwnership table instead of just creating a UserEvent table with a is_owner column.

Snapey's avatar

@james1232 if the event can only have one owner then I would have an owner_id column on the event and a belongsTo relationship. It will work easier than scoping a pivot table

Please or to participate in this conversation.