-
Notifications
You must be signed in to change notification settings - Fork 2
Add Filament Admin resources and feature tests for ReportBuilder module #120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Filament Admin resources and feature tests for ReportBuilder module #120
Conversation
Co-authored-by: nielsdrost7 <[email protected]>
Co-authored-by: nielsdrost7 <[email protected]>
Co-authored-by: nielsdrost7 <[email protected]>
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
🧹 Nitpick comments (10)
Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource/Tables/ReportTemplatesTable.php (2)
66-71: Use a boolean/ternary filter foris_active.Avoid string keys; use
TernaryFilteror boolean options.Apply:
- SelectFilter::make('is_active') - ->label('Active') - ->options([ - '1' => 'Active', - '0' => 'Inactive', - ]), + \Filament\Tables\Filters\TernaryFilter::make('is_active') + ->label('Active') + ->nullable() // if you want All/Yes/No
105-110: Add confirmation to DeleteAction.Match Clone’s confirmation to prevent accidental deletion.
Apply:
- DeleteAction::make('delete') + DeleteAction::make('delete') + ->requiresConfirmation() ->icon(Heroicon::OutlinedTrash)Modules/ReportBuilder/Tests/Feature/BlockCloningTest.php (1)
60-97: Test name doesn't match behavior.The test
it_prevents_editing_system_blocksonly verifies that theis_systemflag is set correctly but doesn't actually test that editing is prevented. Consider renaming to reflect what it tests (e.g.,it_identifies_system_templates) or add assertions that attempt to edit a system template and verify the operation is rejected.To test actual prevention, consider adding:
/* Act - attempt to update system template */ $updatedBlocks = [ [ 'id' => 'block_header_company', 'type' => 'header_company', 'position' => ['x' => 1, 'y' => 1, 'width' => 6, 'height' => 4], 'config' => ['show_vat_id' => false], 'label' => 'Modified Header', 'isCloneable' => true, 'dataSource' => 'company', 'isCloned' => false, 'clonedFrom' => null, ], ]; $this->expectException(\Exception::class); $this->service->updateTemplate($template, $updatedBlocks);Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource/Pages/EditReportTemplate.php (1)
15-37: Reconsider full override of parent save method.Completely overriding the parent
savemethod duplicates framework logic and prevents the page from benefiting from future Filament updates. If you need custom behavior, prefer using Filament's lifecycle hooks (beforeSave,afterSave,mutateFormDataBeforeSave) or callparent::save()and customize specific steps.Consider using hooks instead:
protected function mutateFormDataBeforeSave(array $data): array { // Your custom mutation logic return $data; } protected function afterSave(): void { // Custom post-save logic }Or if you must override, delegate to parent:
public function save(bool $shouldRedirect = true, bool $shouldSendSavedNotification = true): void { // Your pre-save logic here parent::save($shouldRedirect, $shouldSendSavedNotification); // Your post-save logic here }Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource/Pages/ListReportTemplates.php (1)
20-22: Remove no-op data mutation.The
mutateDataUsingcallback returns the data unchanged, making it a no-op. Either remove this callback or add meaningful data transformation logic.return [ CreateAction::make() - ->mutateDataUsing(function (array $data) { - return $data; - }) ->action(function (array $data) {Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource/Pages/CreateReportTemplate.php (1)
15-42: Reconsider full override of parent create method.Similar to the concern in
EditReportTemplate.php, completely overriding the parentcreatemethod duplicates framework logic and prevents the page from benefiting from future Filament updates. Filament provides lifecycle hooks for customization without needing to override the entire method.Consider using Filament's hooks instead:
protected function mutateFormDataBeforeCreate(array $data): array { // Your custom mutation logic return $data; } protected function afterCreate(): void { // Custom post-create logic }If you need to keep the override, at least call
parent::create($another)and wrap your custom logic around it where possible.Modules/ReportBuilder/Tests/Feature/ReportRenderingTest.php (2)
45-48: Consider extracting company and user setup into a helper method.The company, user, and session setup pattern is duplicated across all three test methods. This duplication makes tests harder to maintain.
Consider extracting this pattern into a protected helper method:
+ protected function createCompanyContext(): Company + { + $company = Company::factory()->create(); + $user = User::factory()->create(); + $user->companies()->attach($company); + session(['current_company_id' => $company->id]); + + return $company; + }Then use it in each test:
public function it_renders_template_to_html_with_correct_block_order(): void { /* Arrange */ - $company = Company::factory()->create(); - $user = User::factory()->create(); - $user->companies()->attach($company); - session(['current_company_id' => $company->id]); + $company = $this->createCompanyContext();Also applies to: 103-106, 148-151
50-73: Consider a block factory or builder for test data.Block structure definitions are verbose and contain many required fields. A factory method or builder pattern would reduce duplication and improve readability.
Example factory method:
protected function createHeaderBlock(array $overrides = []): array { return array_merge([ 'id' => 'block_header_company', 'type' => 'header_company', 'position' => ['x' => 0, 'y' => 0, 'width' => 6, 'height' => 4], 'config' => [], 'label' => 'Company Header', 'isCloneable' => true, 'dataSource' => 'company', 'isCloned' => false, 'clonedFrom' => null, ], $overrides); }Also applies to: 108-120
Modules/ReportBuilder/Tests/Feature/CreateReportTemplateTest.php (1)
54-57: Extract repeated test setup into helper methods.Similar to
ReportRenderingTest.php, this test class repeats company, user, and session setup across all test methods. Consider extracting into a protected helper method to reduce duplication.Also applies to: 100-103, 162-165, 193-197
Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource/Pages/DesignReportTemplate.php (1)
140-150: Consider validating block config structure.The method merges config without validation. While this might be acceptable if validation occurs elsewhere (e.g., in the service layer or during rendering), consider adding basic validation to prevent invalid config from being stored.
Example validation approach:
public function updateBlockConfig(string $blockId, array $config): void { if (!isset($this->blocks[$blockId])) { return; } + // Validate config structure based on block type + $blockType = $this->blocks[$blockId]['type']; + // Add type-specific validation here + $this->blocks[$blockId]['config'] = array_merge( $this->blocks[$blockId]['config'] ?? [], $config ); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (16)
Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource.php(1 hunks)Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource/Pages/CreateReportTemplate.php(1 hunks)Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource/Pages/DesignReportTemplate.php(1 hunks)Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource/Pages/EditReportTemplate.php(1 hunks)Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource/Pages/ListReportTemplates.php(1 hunks)Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource/Schemas/ReportTemplateForm.php(1 hunks)Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource/Tables/ReportTemplatesTable.php(1 hunks)Modules/ReportBuilder/Providers/ReportBuilderServiceProvider.php(1 hunks)Modules/ReportBuilder/Tests/Feature/BlockCloningTest.php(1 hunks)Modules/ReportBuilder/Tests/Feature/CreateReportTemplateTest.php(1 hunks)Modules/ReportBuilder/Tests/Feature/GridSnapperTest.php(1 hunks)Modules/ReportBuilder/Tests/Feature/ReportRenderingTest.php(1 hunks)Modules/ReportBuilder/Tests/Feature/UpdateReportTemplateTest.php(1 hunks)Modules/ReportBuilder/composer.json(1 hunks)Modules/ReportBuilder/module.json(1 hunks)modules_statuses.json(1 hunks)
🧰 Additional context used
🪛 PHPMD (2.15.0)
Modules/ReportBuilder/Tests/Feature/CreateReportTemplateTest.php
120-120: Avoid unused local variables such as '$template'. (undefined)
(UnusedLocalVariable)
🔇 Additional comments (12)
modules_statuses.json (1)
9-10: Module enabled entry looks good.JSON is valid; ReportBuilder is correctly marked enabled.
Modules/ReportBuilder/module.json (1)
1-11: Manifest is consistent and correct.Provider path and alias align with the service provider and module naming.
Modules/ReportBuilder/composer.json (1)
1-29: Remove the nested Modules/ReportBuilder/composer.json; it's inert and creates confusion.The root composer.json already defines PSR-4 autoloading for
"Modules\\"→"Modules/"(line 56). This directly routes all module namespaces and makes the nested manifest unused. Composer will never consultModules/ReportBuilder/composer.jsonduring autoloading in this setup—only the root config applies.Keep the codebase simpler by deleting this file.
Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource/Schemas/ReportTemplateForm.php (1)
15-53: LGTM!The form schema is well-structured with appropriate field types, validations, and safeguards. The
is_systemfield is correctly disabled and excluded from form submission viadehydrated(false), preventing unauthorized modification of system template flags.Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource.php (1)
18-67: LGTM!The resource is well-structured and follows Filament best practices. The delegation pattern for form and table configuration promotes maintainability, and the page routing is clear and properly organized.
Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource/Pages/EditReportTemplate.php (1)
50-60: The review comment is incorrect and based on a misunderstanding of Filament's checkbox behavior.Filament's Checkbox component integrates with Livewire via wire:model, which maintains boolean state (false when unchecked, true when checked). Unlike standard HTML forms where unchecked checkboxes are omitted, Livewire keeps the component property as false when unchecked.
Since the form schema includes
Checkbox::make('is_active')with no conditional rendering,$data['is_active']will always be present with a boolean value. The?? truefallback provides defensive handling but won't trigger in normal Filament operation—the field is never omitted.The current implementation correctly preserves false values when the user unchecks the checkbox and defaults to true only if the key is missing (an edge case that shouldn't occur).
Likely an incorrect or invalid review comment.
Modules/ReportBuilder/Tests/Feature/CreateReportTemplateTest.php (1)
120-126: Static analysis false positive: Variable is used for side effects.The static analysis tool flagged
$templateas unused, but this is a false positive. The variable assignment ensurescreateTemplate()executes and produces side effects (database insert and filesystem persistence) that are verified by the subsequent assertions. The variable could optionally be prefixed with an underscore to signal intentional non-use, but the current code is acceptable.Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource/Pages/DesignReportTemplate.php (5)
28-44: LGTM: Clean initialization pattern.The mount and loadBlocks methods correctly initialize the component state. Resetting
$this->blocksto an empty array before loading (line 39) prevents stale data issues.
47-72: LGTM: Proper validation and grid snapping.The method correctly validates block existence, position data, and uses the GridSnapperService to ensure positions align with the grid. The deferred persistence (requiring explicit save() call) is appropriate for a design interface.
75-94: Question: Why are newly added blocks not cloneable?Line 88 sets
isCloneable(false)for all newly added blocks. This seems like an arbitrary restriction. Users might want to clone custom blocks they've just added. Consider whether this should be configurable based on block type or default totrue.Is there a specific reason newly added blocks should not be cloneable? If not, consider:
$block = new BlockDTO(); $block->setId($blockId) ->setType($blockType) ->setPosition($position) ->setConfig([]) ->setLabel(ucfirst(str_replace('_', ' ', $blockType))) - ->setIsCloneable(false) + ->setIsCloneable(true) ->setDataSource('custom') ->setIsCloned(false) ->setClonedFrom(null);
152-159: LGTM: Proper service delegation and user feedback.The save method correctly delegates persistence to the service layer, dispatches a client-side event for feedback, and redirects appropriately.
130-137: Incorrect assumption about block-levelis_systemprotection.The original suggestion to check for system blocks at the block level is incorrect:
is_systemis a property on theReportTemplatemodel only, not on individual blocks. System template protection is enforced at the template level via Filament actions (e.g.,EditReportTemplatehides delete for system templates).The valid concern about deletion is blocks that have been cloned from. If block A was cloned to create block B (where
clonedFromreferences A), deleting A leaves B with an orphanedclonedFromreference. Since these blocks are in-memory within the Livewire component, this causes metadata inconsistency rather than runtime failure, but you may want to clarify the intended behavior: should blocks with clones be protected, or should cloning references be cleared when the original is deleted?Likely an incorrect or invalid review comment.
| $company = Company::find(session('current_company_id')); | ||
| if (!$company) { | ||
| $company = auth()->user()->companies()->first(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Extract duplicated company resolution logic.
This company resolution logic is duplicated in ListReportTemplates.php (lines 24-27). Extract it to a shared trait or helper method to maintain consistency and reduce duplication.
See the suggestion in ListReportTemplates.php for the extraction pattern.
🤖 Prompt for AI Agents
In
Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource/Pages/CreateReportTemplate.php
around lines 46 to 49, the company-resolution block is duplicated (also present
in ListReportTemplates.php lines 24-27); extract this logic into a shared helper
so both pages use the same implementation. Create a small trait (e.g.,
ResolvesCurrentCompany) or a static helper method in a suitable namespace under
Modules/ReportBuilder (implementing: check session('current_company_id') and
fallback to auth()->user()->companies()->first()), add proper return typing,
include unit/usage docblock, then replace the inline code in both
CreateReportTemplate.php and ListReportTemplates.php with a call to the new
trait/helper and import/use it in both classes. Ensure behavior and tests remain
unchanged.
| public function cloneBlock(string $blockId): void | ||
| { | ||
| if (!isset($this->blocks[$blockId])) { | ||
| return; | ||
| } | ||
|
|
||
| $originalBlock = $this->blocks[$blockId]; | ||
|
|
||
| if ($originalBlock['isCloned'] === false && $originalBlock['isCloneable'] === true) { | ||
| $newBlockId = 'block_' . $originalBlock['type'] . '_' . Str::random(8); | ||
|
|
||
| $position = new GridPositionDTO(); | ||
| $position->setX($originalBlock['position']['x'] + 1) | ||
| ->setY($originalBlock['position']['y'] + 1) | ||
| ->setWidth($originalBlock['position']['width']) | ||
| ->setHeight($originalBlock['position']['height']); | ||
|
|
||
| $clonedBlock = new BlockDTO(); | ||
| $clonedBlock->setId($newBlockId) | ||
| ->setType($originalBlock['type']) | ||
| ->setPosition($position) | ||
| ->setConfig($originalBlock['config']) | ||
| ->setLabel($originalBlock['label'] . ' (Clone)') | ||
| ->setIsCloneable(false) | ||
| ->setDataSource($originalBlock['dataSource']) | ||
| ->setIsCloned(true) | ||
| ->setClonedFrom($blockId); | ||
|
|
||
| $this->blocks[$newBlockId] = BlockTransformer::toArray($clonedBlock); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
LGTM: Proper clone logic with minor position concern.
The clone logic correctly validates cloneability (line 105) and sets appropriate metadata. The position offset of (+1, +1) on lines 109-110 is simple but could potentially place clones off-grid or cause overlap. Consider whether the offset should be validated or snapped immediately.
Verify that the +1 offset doesn't cause issues:
🏁 Script executed:
#!/bin/bash
# Check if cloned block positions are validated/snapped elsewhere
rg -nP -C3 '\bcloneBlock\b' --type=phpLength of output: 887
---
🏁 Script executed:
```shell
#!/bin/bash
# Search for position validation, snapping, or grid constraint logic
rg -nP -C2 '(snap|Snap|validate|Validate|constraint|Constraint|grid.*position|position.*grid)' --type=php | head -100
Length of output: 10040
🏁 Script executed:
#!/bin/bash
# Search for where blocks are saved/persisted after cloning
rg -nP -C3 'blocks\[' --type=php | grep -A3 -B3 'cloned\|clone' | head -80Length of output: 1989
🏁 Script executed:
#!/bin/bash
# Check if BlockDTO or GridPositionDTO has validation
fd -e php | xargs rg -l 'class (BlockDTO|GridPositionDTO)' --type=phpLength of output: 193
---
🏁 Script executed:
```shell
#!/bin/bash
# Check what happens after cloneBlock - is there downstream validation?
rg -nP -C5 'cloneBlock' --type=php | grep -v test
Length of output: 1394
🏁 Script executed:
#!/bin/bash
# Check if positions are always validated/snapped before persisting blocks
rg -nP 'saveBlocks|persistBlocks|updateBlocks|save.*blocks' --type=phpLength of output: 1405
🏁 Script executed:
#!/bin/bash
# Check the moveBlock method to see the validation pattern applied there
sed -n '45,72p' Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource/Pages/DesignReportTemplate.phpLength of output: 894
Apply grid validation and snapping to cloned block position, consistent with updateBlockPosition.
The cloneBlock method bypasses the validation and snapping applied in updateBlockPosition (lines 45-72). When a block is cloned with the +1 offset, the position is stored without checking grid boundaries. If the original block is near the grid edge (e.g., x=gridSize-1), the cloned block's offset will exceed valid grid bounds.
The fix: validate and snap the cloned position before storing:
$position = new GridPositionDTO();
$position->setX($originalBlock['position']['x'] + 1)
->setY($originalBlock['position']['y'] + 1)
->setWidth($originalBlock['position']['width'])
->setHeight($originalBlock['position']['height']);
$gridSnapper = app(GridSnapperService::class);
if (!$gridSnapper->validate($position)) {
return;
}
$position = $gridSnapper->snap($position);Then use $position->getX() etc. when setting the cloned block's position (lines 114-117).
🤖 Prompt for AI Agents
In
Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource/Pages/DesignReportTemplate.php
around lines 97 to 127, the cloned block position is created by offsetting the
original position but skips the grid validation and snapping used in
updateBlockPosition, which can allow positions outside grid bounds; fix by
resolving a GridSnapperService from the container, validate the new
GridPositionDTO and return early if invalid, then snap the position before
setting it on the cloned BlockDTO and use the snapped position getters (x, y,
width, height) when assigning the cloned block's position; ensure you abort the
clone when validation fails to match updateBlockPosition behavior.
| ->action(function (array $data) { | ||
| $company = Company::find(session('current_company_id')); | ||
| if (!$company) { | ||
| $company = auth()->user()->companies()->first(); | ||
| } | ||
|
|
||
| app(ReportTemplateService::class)->createTemplate( | ||
| $company, | ||
| $data['name'], | ||
| $data['template_type'], | ||
| [] | ||
| ); | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Duplicated company resolution and creation logic.
The company resolution logic (lines 24-27) and template creation flow are duplicated from CreateReportTemplate.php (lines 46-56). This duplication can lead to inconsistencies if the logic needs to change. Consider one of these approaches:
- Preferred: Remove this custom
CreateActionand rely solely on theCreateReportTemplatepage for creation workflow. - Extract company resolution to a shared trait or helper method if both approaches are needed.
If you need both creation paths, extract the shared logic:
// In a trait or helper
protected function resolveCurrentCompany(): Company
{
$company = Company::find(session('current_company_id'));
if (!$company) {
$company = auth()->user()->companies()->first();
}
return $company;
}Then use it in both places:
->action(function (array $data) {
$company = $this->resolveCurrentCompany();
app(ReportTemplateService::class)->createTemplate(
$company,
$data['name'],
$data['template_type'],
[]
);
})🤖 Prompt for AI Agents
In
Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource/Pages/ListReportTemplates.php
around lines 23 to 35, the company resolution and template creation logic is
duplicated from CreateReportTemplate.php; remove the custom inline CreateAction
and instead rely on the existing CreateReportTemplate page for creation, or if
you must keep both flows, extract the company resolution into a shared protected
method or trait (e.g., resolveCurrentCompany()) and call that from both
locations, then call ReportTemplateService::createTemplate with the resolved
Company and incoming data; ensure you remove the duplicated lines 24–27 and
replace them with a single call to the shared helper or navigation to the
CreateReportTemplate page.
| ->icon(Heroicon::OutlinedEye), | ||
| EditAction::make() | ||
| ->icon(Heroicon::OutlinedPencil) | ||
| ->action(function (ReportTemplate $record, array $data) { | ||
| $blocks = $data['blocks'] ?? []; | ||
| app(ReportTemplateService::class)->updateTemplate($record, $blocks); | ||
| }) | ||
| ->modalWidth('full') | ||
| ->visible(fn (ReportTemplate $record) => !$record->is_system), | ||
| Action::make('design') | ||
| ->label('Design') | ||
| ->icon(Heroicon::OutlinedPaintBrush) | ||
| ->url(fn (ReportTemplate $record) => route('filament.admin.resources.report-templates.design', ['record' => $record->id])) | ||
| ->visible(fn (ReportTemplate $record) => !$record->is_system), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Icon API check and route indirection.
- Verify
Filament\Support\Icons\Heroicon::*constants for your Filament version; if unavailable, use strings like'heroicon-o-eye','heroicon-o-pencil', etc. - Avoid hard‑coded route names; delegate to the Resource for URLs.
Suggested change for the Design action:
- Action::make('design')
+ Action::make('design')
->label('Design')
- ->icon(Heroicon::OutlinedPaintBrush)
- ->url(fn (ReportTemplate $record) => route('filament.admin.resources.report-templates.design', ['record' => $record->id]))
+ ->icon(Heroicon::OutlinedPaintBrush) // or 'heroicon-o-paint-brush'
+ ->url(fn (ReportTemplate $record) =>
+ \Modules\ReportBuilder\Filament\Admin\Resources\ReportTemplateResource::getUrl('design', ['record' => $record]))
->visible(fn (ReportTemplate $record) => !$record->is_system),Quick checks:
🏁 Script executed:
#!/bin/bash
# Do these exist?
rg -nP 'class\s+ReportTemplateForm\b' -C2
rg -nP 'class\s+DesignReportTemplate\b' -C3
rg -nP 'Heroicon::' --type=phpLength of output: 7649
I need to verify the route registration and how the design page is configured in ReportTemplateResource.
Now let me verify the ReportTemplateResource page registration:The web search results are not specific to the codebase. Let me inspect the ReportTemplateResource directly:
---
🏁 Script executed:
```shell
#!/bin/bash
# Search for ReportTemplateResource and its getPages() method
rg -nP 'class ReportTemplateResource' -A 50 | head -100
rg -nP 'getPages' Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource.php -A 20
Length of output: 5459
Use Resource URL generation for the design route.
The design page is registered in ReportTemplateResource::getPages() (line 64), so use ReportTemplateResource::getUrl('design', ['record' => $record]) instead of the hard-coded route name for better maintainability.
Note: The Heroicon::OutlinedPaintBrush constant is properly supported and requires no changes or fallback strings.
Suggested change:
Action::make('design')
->label('Design')
->icon(Heroicon::OutlinedPaintBrush)
- ->url(fn (ReportTemplate $record) => route('filament.admin.resources.report-templates.design', ['record' => $record->id]))
+ ->url(fn (ReportTemplate $record) => ReportTemplateResource::getUrl('design', ['record' => $record]))
->visible(fn (ReportTemplate $record) => !$record->is_system),🤖 Prompt for AI Agents
In
Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource/Tables/ReportTemplatesTable.php
around lines 76 to 89, the Action->url currently builds the design page link via
a hard-coded route name; replace that with Resource URL generation by calling
ReportTemplateResource::getUrl('design', ['record' => $record]) so the link uses
the resource's registered page route and remains maintainable when routes
change.
| EditAction::make() | ||
| ->icon(Heroicon::OutlinedPencil) | ||
| ->action(function (ReportTemplate $record, array $data) { | ||
| $blocks = $data['blocks'] ?? []; | ||
| app(ReportTemplateService::class)->updateTemplate($record, $blocks); | ||
| }) | ||
| ->modalWidth('full') | ||
| ->visible(fn (ReportTemplate $record) => !$record->is_system), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EditAction likely not receiving form data; define a form or route to Edit page.
Without ->form(...), $data will be empty. Either use a modal form, or navigate to the Edit page.
Option A: modal edit using your schema:
- EditAction::make()
+ EditAction::make()
->icon(Heroicon::OutlinedPencil)
- ->action(function (ReportTemplate $record, array $data) {
- $blocks = $data['blocks'] ?? [];
- app(ReportTemplateService::class)->updateTemplate($record, $blocks);
- })
+ ->form(\Modules\ReportBuilder\Filament\Admin\Resources\ReportTemplateResource\Schemas\ReportTemplateForm::schema())
+ ->action(fn (ReportTemplate $record, array $data)
+ => app(ReportTemplateService::class)->updateTemplate($record, $data['blocks'] ?? []))
->modalWidth('full')
->visible(fn (ReportTemplate $record) => !$record->is_system),Option B: link to Edit page instead:
- EditAction::make()
- ->icon(Heroicon::OutlinedPencil)
- ->action(function (ReportTemplate $record, array $data) { /* ... */ })
- ->modalWidth('full')
+ EditAction::make()
+ ->icon(Heroicon::OutlinedPencil)
+ ->url(fn (ReportTemplate $record) =>
+ \Modules\ReportBuilder\Filament\Admin\Resources\ReportTemplateResource::getUrl('edit', ['record' => $record]))
->visible(fn (ReportTemplate $record) => !$record->is_system),🤖 Prompt for AI Agents
In
Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource/Tables/ReportTemplatesTable.php
around lines 77 to 84, the EditAction is using an action closure that expects
form data ($data) but no ->form(...) is defined so $data will be empty; either
convert this to a modal edit by adding ->form(...) with fields that map to
'blocks' (and use a submit handler that calls
ReportTemplateService::updateTemplate with the validated data) or change the
action to navigate to the resource Edit page (use ->url(...) or Filament's
built-in edit navigation) so the standard edit form supplies data — implement
one of these two options and remove the unsupported inline action that assumes
$data exists.
| Action::make('clone') | ||
| ->label('Clone') | ||
| ->icon(Heroicon::OutlinedDocumentDuplicate) | ||
| ->requiresConfirmation() | ||
| ->action(function (ReportTemplate $record) { | ||
| $service = app(ReportTemplateService::class); | ||
| $blocks = $service->loadBlocks($record); | ||
| $service->createTemplate( | ||
| $record->company, | ||
| $record->name . ' (Copy)', | ||
| $record->template_type, | ||
| array_map(fn ($block) => (array) $block, $blocks) | ||
| ); | ||
| }) | ||
| ->visible(fn (ReportTemplate $record) => $record->isCloneable()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid (array) cast when cloning blocks; use arrayable conversion.
Casting objects with (array) can yield unexpected keys. Prefer toArray() (if Arrayable) or deep conversion.
Apply:
- $service->createTemplate(
+ $service->createTemplate(
$record->company,
$record->name . ' (Copy)',
$record->template_type,
- array_map(fn ($block) => (array) $block, $blocks)
+ array_map(function ($block) {
+ return is_object($block) && method_exists($block, 'toArray')
+ ? $block->toArray()
+ : (array) $block;
+ }, $blocks)
);🤖 Prompt for AI Agents
In
Modules/ReportBuilder/Filament/Admin/Resources/ReportTemplateResource/Tables/ReportTemplatesTable.php
around lines 90 to 104, replace the unsafe (array) cast when mapping blocks into
arrays with an Arrayable-aware deep conversion: for each $block, if it
implements Illuminate\Contracts\Support\Arrayable call ->toArray(), otherwise if
it is an object perform a JSON-encode/decode roundtrip to produce a plain array
(or fall back to (array) for scalars), ensuring nested structures convert
correctly; implement this logic inside the array_map callback before passing
blocks to createTemplate.
| $componentNamespace = $this->module_namespace($this->name, $this->app_path(config('modules.paths.generator.component-class.path'))); | ||
| Blade::componentNamespace($componentNamespace, $this->nameLower); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Undefined method $this->app_path(); use module namespace/path directly.
$this->app_path() is not available on ServiceProvider and will fatally error. Compute the component namespace without it.
Apply:
- $componentNamespace = $this->module_namespace($this->name, $this->app_path(config('modules.paths.generator.component-class.path')));
- Blade::componentNamespace($componentNamespace, $this->nameLower);
+ // Resolve to "Modules\ReportBuilder\View\Components" (or configured subpath) safely
+ $componentPath = str_replace('\\', '/', config('modules.paths.generator.component-class.path', 'View/Components'));
+ Blade::componentNamespace(
+ $this->module_namespace($this->name, $componentPath),
+ $this->nameLower
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| $componentNamespace = $this->module_namespace($this->name, $this->app_path(config('modules.paths.generator.component-class.path'))); | |
| Blade::componentNamespace($componentNamespace, $this->nameLower); | |
| } | |
| // Resolve to "Modules\ReportBuilder\View\Components" (or configured subpath) safely | |
| $componentPath = str_replace('\\', '/', config('modules.paths.generator.component-class.path', 'View/Components')); | |
| Blade::componentNamespace( | |
| $this->module_namespace($this->name, $componentPath), | |
| $this->nameLower | |
| ); | |
| } |
🤖 Prompt for AI Agents
In Modules/ReportBuilder/Providers/ReportBuilderServiceProvider.php around lines
52 to 54, the code calls an undefined $this->app_path() method which will cause
a fatal error; remove the app_path() call and compute the component namespace
directly by passing the module name and the configured generator path to
module_namespace, e.g. call $this->module_namespace($this->name,
config('modules.paths.generator.component-class.path')) (or the equivalent
config value used in your project) and then pass that result to
Blade::componentNamespace($componentNamespace, $this->nameLower).
| protected function registerConfig(): void | ||
| { | ||
| $relativeConfigPath = config('modules.paths.generator.config.path'); | ||
| $configPath = module_path($this->name, $relativeConfigPath); | ||
|
|
||
| if (is_dir($configPath)) { | ||
| $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($configPath)); | ||
|
|
||
| foreach ($iterator as $file) { | ||
| if ($file->isFile() && $file->getExtension() === 'php') { | ||
| $relativePath = str_replace($configPath . DIRECTORY_SEPARATOR, '', $file->getPathname()); | ||
| $configKey = $this->nameLower . '.' . str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $relativePath); | ||
| $key = ($relativePath === 'config.php') ? $this->nameLower : $configKey; | ||
|
|
||
| $this->publishes([$file->getPathname() => config_path($relativePath)], 'config'); | ||
| $this->mergeConfigFrom($file->getPathname(), $key); | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Config publishing target and iterator robustness.
- Publishing to
config_path($relativePath)can createconfig/config.php(collision/confusion). Prefer namespaced publish paths. - Skip dot entries and only iterate PHP files.
- Provide a safe fallback when
modules.paths.generator.config.pathis unset.
Apply:
protected function registerConfig(): void
{
- $relativeConfigPath = config('modules.paths.generator.config.path');
+ $relativeConfigPath = config('modules.paths.generator.config.path', 'Config');
$configPath = module_path($this->name, $relativeConfigPath);
if (is_dir($configPath)) {
- $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($configPath));
+ $flags = \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::CURRENT_AS_FILEINFO;
+ $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($configPath, $flags));
foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === 'php') {
$relativePath = str_replace($configPath . DIRECTORY_SEPARATOR, '', $file->getPathname());
$configKey = $this->nameLower . '.' . str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $relativePath);
$key = ($relativePath === 'config.php') ? $this->nameLower : $configKey;
- $this->publishes([$file->getPathname() => config_path($relativePath)], 'config');
+ // Publish base config as reportbuilder.php; nest others under reportbuilder/
+ $publishTo = $relativePath === 'config.php'
+ ? config_path($this->nameLower . '.php')
+ : config_path($this->nameLower . DIRECTORY_SEPARATOR . $relativePath);
+ $this->publishes([$file->getPathname() => $publishTo], [$this->nameLower . '-config', 'config']);
$this->mergeConfigFrom($file->getPathname(), $key);
}
}
}
}Optional: also allow publishing translations with a module-specific tag.
🤖 Prompt for AI Agents
In Modules/ReportBuilder/Providers/ReportBuilderServiceProvider.php around lines
61 to 80, the config publishing path and iterator need hardening: ensure you use
a module-namespaced publish target (e.g. config_path($this->nameLower . '/' .
$relativePath) or config_path("{$this->nameLower}.{$fileName}.php") instead of
config_path($relativePath) to avoid colliding with root config files), guard
against an unset modules.paths.generator.config.path by falling back to a sane
default (e.g. 'Config' or return early if not set), skip dot entries when
iterating (filter out '.' and '..' and non-files) and only process files with
the php extension, and optionally add a second publishes call for translations
using a module-specific tag (e.g. $this->publishes([...],
'config:reportbuilder') and a similar tag for translations).
| public function it_snaps_position_to_grid(): void | ||
| { | ||
| /* Arrange */ | ||
| $position = new GridPositionDTO(); | ||
| $position->setX(0)->setY(0)->setWidth(6)->setHeight(4); | ||
|
|
||
| /* Act */ | ||
| $snapped = $this->gridSnapper->snap($position); | ||
|
|
||
| /* Assert */ | ||
| $this->assertEquals(0, $snapped->getX()); | ||
| $this->assertEquals(0, $snapped->getY()); | ||
| $this->assertEquals(6, $snapped->getWidth()); | ||
| $this->assertEquals(4, $snapped->getHeight()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test doesn't verify actual snapping behavior.
The test uses already-aligned grid coordinates (0,0,6,4) and expects them back unchanged, which doesn't verify that the snap method correctly snaps non-aligned positions to the grid. To properly test snapping behavior, use misaligned input coordinates.
Apply this diff to test actual snapping:
public function it_snaps_position_to_grid(): void
{
/* Arrange */
$position = new GridPositionDTO();
- $position->setX(0)->setY(0)->setWidth(6)->setHeight(4);
+ $position->setX(0.7)->setY(0.3)->setWidth(5.8)->setHeight(3.9);
/* Act */
$snapped = $this->gridSnapper->snap($position);
/* Assert */
$this->assertEquals(0, $snapped->getX());
$this->assertEquals(0, $snapped->getY());
- $this->assertEquals(6, $snapped->getWidth());
- $this->assertEquals(4, $snapped->getHeight());
+ $this->assertEquals(6, $snapped->getWidth(), 'Width should snap to nearest grid unit');
+ $this->assertEquals(4, $snapped->getHeight(), 'Height should snap to nearest grid unit');
}🤖 Prompt for AI Agents
In Modules/ReportBuilder/Tests/Feature/GridSnapperTest.php around lines 30 to
44, the test only uses already-aligned coordinates so it doesn't validate
snapping behavior; change the Arrange input to use misaligned coordinates (e.g.,
non-multiples/offsets) and update the expected assertions to the coordinates
resulting from snapping (set X/Y/width/height to values that require snapping,
call $this->gridSnapper->snap($position), and assert the returned
X/Y/width/height equal the snapped values).
| #[Test] | ||
| #[Group('crud')] | ||
| public function it_snaps_blocks_to_grid_on_update(): void | ||
| { | ||
| /* Arrange */ | ||
| $company = Company::factory()->create(); | ||
| $user = User::factory()->create(); | ||
| $user->companies()->attach($company); | ||
| session(['current_company_id' => $company->id]); | ||
|
|
||
| $template = $this->service->createTemplate( | ||
| $company, | ||
| 'Test Template', | ||
| 'invoice', | ||
| [] | ||
| ); | ||
|
|
||
| $blocksWithValidPosition = [ | ||
| [ | ||
| 'id' => 'block_header_company', | ||
| 'type' => 'header_company', | ||
| 'position' => ['x' => 0, 'y' => 0, 'width' => 6, 'height' => 4], | ||
| 'config' => [], | ||
| 'label' => 'Company Header', | ||
| 'isCloneable' => true, | ||
| 'dataSource' => 'company', | ||
| 'isCloned' => false, | ||
| 'clonedFrom' => null, | ||
| ], | ||
| ]; | ||
|
|
||
| /* Act */ | ||
| $this->service->updateTemplate($template, $blocksWithValidPosition); | ||
|
|
||
| /* Assert */ | ||
| $fileContents = Storage::disk('report_templates')->get( | ||
| "{$company->id}/test-template.json" | ||
| ); | ||
| $savedBlocks = json_decode($fileContents, true); | ||
|
|
||
| $this->assertEquals(0, $savedBlocks[0]['position']['x']); | ||
| $this->assertEquals(0, $savedBlocks[0]['position']['y']); | ||
| $this->assertEquals(6, $savedBlocks[0]['position']['width']); | ||
| $this->assertEquals(4, $savedBlocks[0]['position']['height']); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test doesn't verify grid snapping behavior.
The test it_snaps_blocks_to_grid_on_update uses already grid-aligned coordinates (0,0,6,4), so it doesn't actually test whether the snapping logic works. To properly verify snapping, use misaligned input coordinates and assert they're corrected.
Apply this diff to test actual snapping:
$blocksWithValidPosition = [
[
'id' => 'block_header_company',
'type' => 'header_company',
- 'position' => ['x' => 0, 'y' => 0, 'width' => 6, 'height' => 4],
+ 'position' => ['x' => 0.3, 'y' => 0.7, 'width' => 5.9, 'height' => 3.8],
'config' => [],
'label' => 'Company Header',
'isCloneable' => true,
'dataSource' => 'company',
'isCloned' => false,
'clonedFrom' => null,
],
];
/* Act */
$this->service->updateTemplate($template, $blocksWithValidPosition);
/* Assert */
$fileContents = Storage::disk('report_templates')->get(
"{$company->id}/test-template.json"
);
$savedBlocks = json_decode($fileContents, true);
- $this->assertEquals(0, $savedBlocks[0]['position']['x']);
- $this->assertEquals(0, $savedBlocks[0]['position']['y']);
- $this->assertEquals(6, $savedBlocks[0]['position']['width']);
- $this->assertEquals(4, $savedBlocks[0]['position']['height']);
+ $this->assertEquals(0, $savedBlocks[0]['position']['x'], 'X should snap to 0');
+ $this->assertEquals(1, $savedBlocks[0]['position']['y'], 'Y should snap to nearest grid unit (1)');
+ $this->assertEquals(6, $savedBlocks[0]['position']['width'], 'Width should snap to 6');
+ $this->assertEquals(4, $savedBlocks[0]['position']['height'], 'Height should snap to 4');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| #[Test] | |
| #[Group('crud')] | |
| public function it_snaps_blocks_to_grid_on_update(): void | |
| { | |
| /* Arrange */ | |
| $company = Company::factory()->create(); | |
| $user = User::factory()->create(); | |
| $user->companies()->attach($company); | |
| session(['current_company_id' => $company->id]); | |
| $template = $this->service->createTemplate( | |
| $company, | |
| 'Test Template', | |
| 'invoice', | |
| [] | |
| ); | |
| $blocksWithValidPosition = [ | |
| [ | |
| 'id' => 'block_header_company', | |
| 'type' => 'header_company', | |
| 'position' => ['x' => 0, 'y' => 0, 'width' => 6, 'height' => 4], | |
| 'config' => [], | |
| 'label' => 'Company Header', | |
| 'isCloneable' => true, | |
| 'dataSource' => 'company', | |
| 'isCloned' => false, | |
| 'clonedFrom' => null, | |
| ], | |
| ]; | |
| /* Act */ | |
| $this->service->updateTemplate($template, $blocksWithValidPosition); | |
| /* Assert */ | |
| $fileContents = Storage::disk('report_templates')->get( | |
| "{$company->id}/test-template.json" | |
| ); | |
| $savedBlocks = json_decode($fileContents, true); | |
| $this->assertEquals(0, $savedBlocks[0]['position']['x']); | |
| $this->assertEquals(0, $savedBlocks[0]['position']['y']); | |
| $this->assertEquals(6, $savedBlocks[0]['position']['width']); | |
| $this->assertEquals(4, $savedBlocks[0]['position']['height']); | |
| } | |
| #[Test] | |
| #[Group('crud')] | |
| public function it_snaps_blocks_to_grid_on_update(): void | |
| { | |
| /* Arrange */ | |
| $company = Company::factory()->create(); | |
| $user = User::factory()->create(); | |
| $user->companies()->attach($company); | |
| session(['current_company_id' => $company->id]); | |
| $template = $this->service->createTemplate( | |
| $company, | |
| 'Test Template', | |
| 'invoice', | |
| [] | |
| ); | |
| $blocksWithValidPosition = [ | |
| [ | |
| 'id' => 'block_header_company', | |
| 'type' => 'header_company', | |
| 'position' => ['x' => 0.3, 'y' => 0.7, 'width' => 5.9, 'height' => 3.8], | |
| 'config' => [], | |
| 'label' => 'Company Header', | |
| 'isCloneable' => true, | |
| 'dataSource' => 'company', | |
| 'isCloned' => false, | |
| 'clonedFrom' => null, | |
| ], | |
| ]; | |
| /* Act */ | |
| $this->service->updateTemplate($template, $blocksWithValidPosition); | |
| /* Assert */ | |
| $fileContents = Storage::disk('report_templates')->get( | |
| "{$company->id}/test-template.json" | |
| ); | |
| $savedBlocks = json_decode($fileContents, true); | |
| $this->assertEquals(0, $savedBlocks[0]['position']['x'], 'X should snap to 0'); | |
| $this->assertEquals(1, $savedBlocks[0]['position']['y'], 'Y should snap to nearest grid unit (1)'); | |
| $this->assertEquals(6, $savedBlocks[0]['position']['width'], 'Width should snap to 6'); | |
| $this->assertEquals(4, $savedBlocks[0]['position']['height'], 'Height should snap to 4'); | |
| } |
🤖 Prompt for AI Agents
In Modules/ReportBuilder/Tests/Feature/UpdateReportTemplateTest.php around lines
106–150 the test uses already grid-aligned coordinates so it never verifies
snapping; change the arranged block position to misaligned coordinates (e.g.,
'position' => ['x' => 1, 'y' => 1, 'width' => 5, 'height' => 3]) and update the
assertions to assert that the saved file contains the coordinates snapped to the
grid per the service's snapping rules (replace the example expected values with
the actual snapped values your service produces, e.g., assert saved
x/y/width/height equal 0/0/6/4 if those are the correct snapped results).
…t quality (#121) * Initial plan * Implement code review feedback: improve filters, confirmations, and test quality Co-authored-by: nielsdrost7 <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: nielsdrost7 <[email protected]>
2ea0243
into
copilot/add-report-templates-disk
|
Important Review skippedBot user detected. To trigger a single review, invoke the You can disable this status message by setting the Comment |
Implements ReportTemplateResource with CRUD operations, a Livewire-based block designer page, and comprehensive feature tests for the report template builder.
Changes
Filament Resources
DesignReportTemplate Livewire Component
Custom page for drag-and-drop block layout with:
ReportTemplate $record,array $blocks,string $selectedBlockIddrag-block,add-block,clone-block,delete-block,edit-configFeature Tests
Five test classes (15 tests total) covering:
All tests use
Storage::fake('report_templates')and extendAbstractAdminPanelTestCase.Module Infrastructure
reportbuilder::Notes
BelongsToCompanytrait on ReportTemplate modelWarning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
https://api.github.com/repos/AnourValar/eloquent-serialize/zipball/0934a98866e02b73e38696961a9d7984b834c9d9/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/CarbonPHP/carbon/zipball/8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/f265cf5e38577d42311f1a90d619bcd3740bea23/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/brick/math/zipball/113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/danharrin/date-format-converter/zipball/7c31171bc981e48726729a5f3a05a2d2b63f0b1e/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/danharrin/livewire-rate-limiting/zipball/14dde653a9ae8f38af07a0ba4921dc046235e1a0/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/doctrine/dbal/zipball/231959669bb2173194c95636eae7f1b41b2a8b19/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/dragonmantank/cron-expression/zipball/8c784d071debd117328803d86b2097615b457500/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/driesvints/blade-icons/zipball/7b743f27476acb2ed04cb518213d78abe096e814/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/driftingly/rector-laravel/zipball/625dc02cee08d47ecf0ac86de2f02a55026cf34e/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/laravel/framework/zipball/868c1f2d3dba4df6d21e3a8d818479f094cfd942/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/laravel/prompts/zipball/86a8b692e8661d0fb308cec64f3d176821323077/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/nette/utils/zipball/c930ca4e3cf4f17dcfb03037703679d2396d2ede/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/nunomaduro/termwind/zipball/dfa08f390e509967a15c22493dc0bac5733d9123/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/php-debugbar/php-debugbar/zipball/3146d04671f51f69ffec2a4207ac3bdcf13a9f35/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/phpstan/phpstan/zipball/41600c8379eb5aee63e9413fe9e97273e25d57e4/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/rectorphp/rector/zipball/729aabc0ec66e700ef164e26454a1357f222a2f3/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/console/zipball/cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/error-handler/zipball/0b31a944fcd8759ae294da4d2808cbc53aebd0c3/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/http-foundation/zipball/7475561ec27020196c49bb7c4f178d33d7d3dc00/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/http-kernel/zipball/72c304de37e1a1cec6d5d12b81187ebd4850a17b/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/mailer/zipball/a32f3f45f1990db8c4341d5122a7d3a381c7e575/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/mime/zipball/e0a0f859148daf1edf6c60b398eb40bfc96697d1/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/process/zipball/32241012d521e2e8a9d713adb0812bb773b907f1/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/routing/zipball/7614b8ca5fa89b9cd233e21b627bfc5774f586e4/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/string/zipball/17a426cce5fd1f0901fefa9b2a490d0038fd3c9c/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/translation/zipball/e0837b4cbcef63c754d89a4806575cada743a38d/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/symfony/var-dumper/zipball/34d8d4c4b9597347306d1ec8eb4e1319b1e6986f/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/thephpleague/flysystem-local/zipball/6691915f77c7fb69adfb87dcd550052dc184ee10/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/thephpleague/flysystem/zipball/2203e3151755d874bb2943649dae1eb8533ac93e/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)https://api.github.com/repos/wikimedia/composer-merge-plugin/zipball/a03d426c8e9fb2c9c569d9deeb31a083292788bc/usr/bin/php8.3 -n -c /tmp/bsepvi /usr/bin/composer install --no-interaction(http block)If you need me to access, download, or install something from one of these locations, you can either:
Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.
Summary by CodeRabbit