fix: make project template inheritance granular and safe
CI/CD / test_and_check_app (push) Failing after 3m8s
CI/CD / Publish immutable image (push) Skipped
CI/CD / test_and_check_app (pull_request) Failing after 3m8s
CI/CD / Publish immutable image (pull_request) Skipped

This commit is contained in:
Nico Schröder
2026-09-08 10:25:52 +00:00
committed by Twyco
parent fed6a282a4
commit 9895fc36db
50 changed files with 1504 additions and 624 deletions
+46 -24
View File
@@ -2,6 +2,7 @@
namespace App\Actions\Projects;
use App\Models\Project;
use App\Models\ProjectTemplate;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
@@ -10,6 +11,7 @@ class UpdateProjectTemplate
{
public function __construct(
private readonly DuplicateProjectConfiguration $duplicateProjectConfiguration,
private readonly ProjectTemplateOverrideManager $projectTemplateOverrideManager,
) {}
/**
@@ -17,48 +19,68 @@ class UpdateProjectTemplate
*/
public function handle(ProjectTemplate $projectTemplate, array $attributes): ProjectTemplate
{
$projectTemplate->loadMissing('sourceProject', 'linkedProjects.projectTemplate');
$projectTemplate->loadMissing('sourceProject');
$sourceProject = $projectTemplate->sourceProject;
abort_if($sourceProject === null, 422, 'This template cannot be edited because the source project is missing.');
$copyAreas = array_values(Arr::get($attributes, 'copy_areas', []));
$normalizedCopyAreas = $this->duplicateProjectConfiguration->normalizeCopyAreas($copyAreas);
$previousConfiguration = $projectTemplate->configuration_snapshot ?? [];
$previousConfiguration = $this->duplicateProjectConfiguration->normalizeConfiguration(
$projectTemplate->configuration_snapshot ?? [],
);
$nextConfiguration = $this->duplicateProjectConfiguration->export($sourceProject, $normalizedCopyAreas);
$affectedAreas = array_values(array_unique([
...array_keys($previousConfiguration),
...array_keys($nextConfiguration),
]));
$affectedAreas = collect(array_unique([...array_keys($previousConfiguration), ...array_keys($nextConfiguration)]))
->filter(fn (string $area): bool => ($previousConfiguration[$area] ?? null) !== ($nextConfiguration[$area] ?? null))
->values()
->all();
DB::transaction(function () use ($projectTemplate, $attributes, $normalizedCopyAreas, $nextConfiguration, $previousConfiguration, $affectedAreas): void {
ProjectTemplate::query()->whereKey($projectTemplate->id)->lockForUpdate()->firstOrFail();
$projectTemplate->forceFill([
'name' => $attributes['name'],
'copy_areas' => $normalizedCopyAreas,
'configuration_snapshot' => $nextConfiguration,
])->save();
foreach ($projectTemplate->linkedProjects as $linkedProject) {
$currentConfiguration = $this->duplicateProjectConfiguration->export($linkedProject->fresh(), $affectedAreas);
$replacementConfiguration = [];
Project::query()
->where('project_template_id', $projectTemplate->id)
->orderBy('id')
->chunkById(100, function ($linkedProjects) use ($affectedAreas, $nextConfiguration, $previousConfiguration): void {
foreach ($linkedProjects as $linkedProject) {
$linkedProject = Project::query()->whereKey($linkedProject->id)->lockForUpdate()->firstOrFail();
$currentConfiguration = $this->duplicateProjectConfiguration->export($linkedProject, $affectedAreas);
$overrides = $linkedProject->template_overrides ?? [];
$replacementConfiguration = [];
foreach ($nextConfiguration as $area => $configuration) {
$isNewTemplateArea = ! array_key_exists($area, $previousConfiguration);
$matchesPreviousTemplate = array_key_exists($area, $previousConfiguration)
&& array_key_exists($area, $currentConfiguration)
&& $currentConfiguration[$area] === $previousConfiguration[$area];
foreach ($affectedAreas as $area) {
if (in_array($area, $overrides, true)) {
continue;
}
if ($isNewTemplateArea || $matchesPreviousTemplate) {
$replacementConfiguration[$area] = $configuration;
if (! array_key_exists($area, $nextConfiguration)) {
$this->projectTemplateOverrideManager->mark($linkedProject, $area);
continue;
}
if (! array_key_exists($area, $previousConfiguration)
&& array_key_exists($area, $currentConfiguration)
&& $currentConfiguration[$area] !== []) {
$this->projectTemplateOverrideManager->mark($linkedProject, $area);
continue;
}
$replacementConfiguration[$area] = $nextConfiguration[$area];
}
if ($replacementConfiguration !== []) {
$this->duplicateProjectConfiguration->apply($linkedProject, $replacementConfiguration);
}
}
}
if ($replacementConfiguration === []) {
continue;
}
$this->duplicateProjectConfiguration->apply($linkedProject->fresh(), $replacementConfiguration);
}
});
});
return $projectTemplate->fresh(['sourceProject'])->loadCount('linkedProjects');