Skip to content

Feat: Move Event action into standalone actions#51

Open
Dunkstormen wants to merge 5 commits intov2from
feat/extract-model-actions
Open

Feat: Move Event action into standalone actions#51
Dunkstormen wants to merge 5 commits intov2from
feat/extract-model-actions

Conversation

@Dunkstormen
Copy link
Contributor

@Dunkstormen Dunkstormen commented Mar 10, 2026

Summary by Sourcery

Extract event creation, update, and deletion logic from EventService into dedicated action classes and wire controllers to use them while keeping occurrence-related utilities in EventService.

New Features:

  • Introduce CreateEvent, UpdateEvent, and DeleteEvent action classes to encapsulate event lifecycle operations.

Enhancements:

  • Simplify EventService by removing CRUD responsibilities and retaining only event querying and occurrence utilities.
  • Update EventController to delegate event lifecycle operations to the new action classes and enforce authorization on updates.

@sourcery-ai
Copy link

sourcery-ai bot commented Mar 10, 2026

Reviewer's Guide

Refactors event creation, update, and deletion logic out of EventService into three dedicated action classes (CreateEvent, UpdateEvent, DeleteEvent) and wires EventController to use these actions while keeping read-only/event-utility logic in EventService.

Sequence diagram for creating an event with CreateEvent action

sequenceDiagram
    actor User
    participant EventController
    participant CreateEvent
    participant BannerUploadService
    participant DB
    participant Event
    participant Log

    User->>EventController: POST /events (StoreEventRequest)
    EventController->>CreateEvent: __invoke(validatedData, authenticatedUser)
    CreateEvent->>CreateEvent: validateRecurrenceRule(recurrence_rule?)
    CreateEvent->>DB: beginTransaction()
    alt banner is UploadedFile
        CreateEvent->>BannerUploadService: upload(banner)
        BannerUploadService-->>CreateEvent: bannerPath
    else no banner
        CreateEvent-->>CreateEvent: bannerPath = null
    end
    CreateEvent->>Event: create(data + banner_path + created_by)
    Event-->>CreateEvent: event
    CreateEvent->>Log: info(event created message)
    CreateEvent->>DB: commit()
    CreateEvent-->>EventController: event
    EventController-->>User: redirect to events.show(event)
Loading

Sequence diagram for updating an event with UpdateEvent action

sequenceDiagram
    actor User
    participant EventController
    participant UpdateEvent
    participant BannerUploadService
    participant DB
    participant Event
    participant Log

    User->>EventController: PUT /events/{event} (UpdateEventRequest)
    EventController->>EventController: authorize(update, event)
    EventController->>UpdateEvent: __invoke(event, validatedData, authenticatedUser, bannerFile)
    UpdateEvent->>UpdateEvent: validateRecurrenceRule(recurrence_rule?)
    UpdateEvent->>DB: beginTransaction()
    alt banner is UploadedFile
        UpdateEvent->>BannerUploadService: upload(banner)
        BannerUploadService-->>UpdateEvent: bannerPath
        UpdateEvent->>Event: set banner_path
    else no banner
        UpdateEvent-->>Event: keep existing banner_path
    end
    UpdateEvent->>Event: update(data)
    UpdateEvent->>Log: info(event updated message)
    UpdateEvent->>DB: commit()
    UpdateEvent-->>EventController: event
    EventController-->>User: redirect to events.show(event)
Loading

Updated class diagram for Event actions and EventService

classDiagram
    class EventController {
        create(Request request)
        store(StoreEventRequest request, CreateEvent createEvent)
        edit(Request request, Event event, EventService eventService)
        update(UpdateEventRequest request, Event event, UpdateEvent updateEvent)
        destroy(Event event, DeleteEvent deleteEvent)
    }

    class EventService {
        <<service>>
        +__construct(BannerUploadService bannerUploadService)
        +getEventSummary(Event event) array
        +getEventDetails(Event event) array
        +getManagementData(Event event) array
        +generateUpcomingInstances(Event event, int limit, Carbon startDate) Collection
        +toggleOccurrence(Event event, string date, bool cancel) void
        +getLastEndedOccurrence(Event event) array
    }

    class CreateEvent {
        <<action>>
        +__construct(BannerUploadService bannerUploadService)
        +__invoke(array data, User user) Event
        -validateRecurrenceRule(string rule) void
    }

    class UpdateEvent {
        <<action>>
        +__construct(BannerUploadService bannerUploadService)
        +__invoke(Event event, array data, User user, UploadedFile banner) Event
        -validateRecurrenceRule(string rule) void
    }

    class DeleteEvent {
        <<action>>
        +__construct(BannerUploadService bannerUploadService)
        +__invoke(Event event, User user) void
    }

    class BannerUploadService {
        <<service>>
        +upload(UploadedFile file) string
        +delete(string path) void
    }

    class Event {
        <<model>>
        +int id
        +string title
        +string banner_path
        +int created_by
        +string recurrence_rule
        +Carbon start_datetime
        +array cancelled_occurrences
        +create(array attributes) Event
        +update(array attributes) void
        +delete() void
    }

    class User {
        <<model>>
        +int id
        +string vatsim_cid
    }

    EventController --> EventService : uses for read operations
    EventController --> CreateEvent : uses for create
    EventController --> UpdateEvent : uses for update
    EventController --> DeleteEvent : uses for delete

    CreateEvent --> BannerUploadService : uploads banner
    UpdateEvent --> BannerUploadService : uploads banner
    DeleteEvent --> BannerUploadService : deletes banner

    CreateEvent --> Event : creates
    UpdateEvent --> Event : updates
    DeleteEvent --> Event : deletes

    CreateEvent --> User : creator
    UpdateEvent --> User : updater
    DeleteEvent --> User : deleter
Loading

File-Level Changes

Change Details Files
Extract event creation logic into a dedicated CreateEvent action and update controller to use it.
  • Introduce final CreateEvent invokable class that validates recurrence rules, handles optional banner upload, wraps persistence in a DB transaction, and logs creation with user context.
  • Move recurrence rule validation into a private validateRecurrenceRule method using Recurr\Rule and throw ValidationException on errors.
  • Adjust EventController::store to depend on CreateEvent instead of EventService::createEvent, passing validated data and the authenticated user; banner is now expected in the data array rather than as a separate argument.
app/Actions/CreateEvent.php
app/Http/Controllers/EventController.php
app/Services/EventService.php
Extract event update logic into a dedicated UpdateEvent action and update controller to use it, including explicit authorization.
  • Introduce final UpdateEvent invokable class that validates recurrence rules, conditionally updates the event banner, updates the event model inside a DB transaction, and logs the update with user context.
  • Simplify banner handling by reading and unsetting 'banner' from the data array and, if it is an UploadedFile, uploading it and assigning banner_path directly on the model.
  • Update EventController::update to call $this->authorize('update', $event) and then invoke UpdateEvent with the event, validated data, authenticated user, and uploaded banner, returning the updated event for the redirect.
app/Actions/UpdateEvent.php
app/Http/Controllers/EventController.php
app/Services/EventService.php
Extract event deletion logic into a dedicated DeleteEvent action and update controller to use it, keeping banner cleanup and logging.
  • Introduce final DeleteEvent invokable class that wraps deletion in a DB transaction, deletes an existing banner via BannerUploadService, deletes the event, and logs the deletion with user context.
  • Update EventController::destroy to depend on DeleteEvent instead of EventService::deleteEvent, passing the event and authenticated user after an explicit authorization check.
app/Actions/DeleteEvent.php
app/Http/Controllers/EventController.php
app/Services/EventService.php
Trim EventService down to read/utility responsibilities and perform small formatting cleanups.
  • Remove createEvent, updateEvent, and deleteEvent methods from EventService, leaving constructor injection of BannerUploadService and read-oriented methods such as getEventSummary, getManagementData, generateUpcomingInstances, toggleOccurrence, and getLastEndedOccurrence.
  • Normalize whitespace and alignment in EventService methods (e.g., around generateUpcomingInstances and toggleOccurrence) without changing behavior.
app/Services/EventService.php

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 32efae4a-884a-40cf-b643-f65f427991d2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/extract-model-actions

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • In EventController@store, the banner file is no longer passed to the action, but CreateEvent expects banner inside the $data array, so banner uploads on create will silently stop working unless you explicitly pass the uploaded file into the action or adjust how $data['banner'] is populated.
  • The UpdateEvent action signature __invoke(Event $event, array $data, User $user) does not match the controller call $updateEvent($event, $request->validated(), auth()->user(), $request->file('banner')), which will cause an argument count/type mismatch; either update the action signature to accept the banner file or move the banner handling back into the controller data.
  • When updating an event banner in UpdateEvent, the previous implementation deleted the old banner file before uploading a new one, but the new action only uploads and overwrites banner_path without cleanup, which can leave orphaned files; consider preserving the delete behavior.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `EventController@store`, the banner file is no longer passed to the action, but `CreateEvent` expects `banner` inside the `$data` array, so banner uploads on create will silently stop working unless you explicitly pass the uploaded file into the action or adjust how `$data['banner']` is populated.
- The `UpdateEvent` action signature `__invoke(Event $event, array $data, User $user)` does not match the controller call `$updateEvent($event, $request->validated(), auth()->user(), $request->file('banner'))`, which will cause an argument count/type mismatch; either update the action signature to accept the banner file or move the banner handling back into the controller data.
- When updating an event banner in `UpdateEvent`, the previous implementation deleted the old banner file before uploading a new one, but the new action only uploads and overwrites `banner_path` without cleanup, which can leave orphaned files; consider preserving the delete behavior.

## Individual Comments

### Comment 1
<location path="app/Http/Controllers/EventController.php" line_range="144" />
<code_context>
-        );
+        $this->authorize('update', $event);
+
+        $event = $updateEvent($event, $request->validated(), auth()->user(), $request->file('banner'));

         return redirect()->route('events.show', $event)
</code_context>
<issue_to_address>
**issue (bug_risk):** The UpdateEvent action is invoked with a mismatched signature, which will cause a runtime error.

`UpdateEvent::__invoke` is declared as `(Event $event, array $data, User $user): Event`, but it’s being called with four arguments (including the banner file). In PHP this will throw an argument count error. Either extend the action signature to accept the banner file or stop passing it and handle the banner within the existing parameters.
</issue_to_address>

### Comment 2
<location path="app/Actions/UpdateEvent.php" line_range="37-38" />
<code_context>
+
+        return DB::transaction(function () use ($event, $data, $user, $banner) {
+
+            $event->banner_path = $banner instanceof UploadedFile
+                ? $this->bannerUploadService->upload($banner)
+                : $event->banner_path;
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Updating a banner does not delete the previous file, potentially leaving orphaned uploads.

Previously, `EventService::updateEvent` deleted the existing banner before uploading a new one. The new logic only overwrites `banner_path`, leaving the old file on disk. Please restore the cleanup step so that, when a new banner is uploaded and an old one exists, the old file is deleted before saving the new `banner_path`.
</issue_to_address>

### Comment 3
<location path="app/Actions/UpdateEvent.php" line_range="51" />
<code_context>
+        });
+    }
+
+    protected function validateRecurrenceRule(string $rule): void
+    {
+        try {
</code_context>
<issue_to_address>
**suggestion:** Recurrence rule validation logic is duplicated between CreateEvent and UpdateEvent.

Since both `CreateEvent` and `UpdateEvent` define the same `validateRecurrenceRule` method, please extract this into a shared helper/trait or validator so recurrence validation is maintained in a single place.

Suggested implementation:

```
        });
    }
}

```

To fully implement the refactor and avoid duplication:

1. **Create a shared trait** (e.g. `app/Actions/Concerns/ValidatesRecurrenceRule.php`):
   - Namespace: `App\Actions\Concerns;`
   - Define the `protected function validateRecurrenceRule(string $rule): void` method there, with the existing `Rule`/`ValidationException` logic moved into it.

2. **Update this `UpdateEvent` action class**:
   - Add `use App\Actions\Concerns\ValidatesRecurrenceRule;` at the top of the file with the other imports.
   - Inside the `UpdateEvent` class body, add `use ValidatesRecurrenceRule;`.

3. **Update the `CreateEvent` action class** (likely `app/Actions/CreateEvent.php`):
   - Remove its local `validateRecurrenceRule` implementation, just like above.
   - Import and `use` the same `ValidatesRecurrenceRule` trait inside the `CreateEvent` class.

These steps will centralize recurrence rule validation while keeping both actions using the same logic.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

);
$this->authorize('update', $event);

$event = $updateEvent($event, $request->validated(), auth()->user(), $request->file('banner'));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The UpdateEvent action is invoked with a mismatched signature, which will cause a runtime error.

UpdateEvent::__invoke is declared as (Event $event, array $data, User $user): Event, but it’s being called with four arguments (including the banner file). In PHP this will throw an argument count error. Either extend the action signature to accept the banner file or stop passing it and handle the banner within the existing parameters.

Comment on lines +37 to +38
$event->banner_path = $banner instanceof UploadedFile
? $this->bannerUploadService->upload($banner)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Updating a banner does not delete the previous file, potentially leaving orphaned uploads.

Previously, EventService::updateEvent deleted the existing banner before uploading a new one. The new logic only overwrites banner_path, leaving the old file on disk. Please restore the cleanup step so that, when a new banner is uploaded and an old one exists, the old file is deleted before saving the new banner_path.

});
}

protected function validateRecurrenceRule(string $rule): void
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Recurrence rule validation logic is duplicated between CreateEvent and UpdateEvent.

Since both CreateEvent and UpdateEvent define the same validateRecurrenceRule method, please extract this into a shared helper/trait or validator so recurrence validation is maintained in a single place.

Suggested implementation:

        });
    }
}

To fully implement the refactor and avoid duplication:

  1. Create a shared trait (e.g. app/Actions/Concerns/ValidatesRecurrenceRule.php):

    • Namespace: App\Actions\Concerns;
    • Define the protected function validateRecurrenceRule(string $rule): void method there, with the existing Rule/ValidationException logic moved into it.
  2. Update this UpdateEvent action class:

    • Add use App\Actions\Concerns\ValidatesRecurrenceRule; at the top of the file with the other imports.
    • Inside the UpdateEvent class body, add use ValidatesRecurrenceRule;.
  3. Update the CreateEvent action class (likely app/Actions/CreateEvent.php):

    • Remove its local validateRecurrenceRule implementation, just like above.
    • Import and use the same ValidatesRecurrenceRule trait inside the CreateEvent class.

These steps will centralize recurrence rule validation while keeping both actions using the same logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant