diff --git a/.gitignore b/.gitignore index 247a7ee5f..876832dc1 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,4 @@ docker-compose.override.yml _docker_production_test .devcontainer .agents -skills-lock.json \ No newline at end of file +skills-lock.json diff --git a/Dockerfile b/Dockerfile index 5da677694..80a4f57c0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,6 +45,31 @@ RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=locked \ # Install dev.command.sh COPY --chmod=755 --chown=www-data:www-data docker/app/dev.command.sh /usr/bin/dev.command.sh +# ----------------------------------------------------- +# APP - TEST +# ----------------------------------------------------- +FROM app_root AS app_test + +COPY --chown=www-data:www-data . . +COPY --from=node_builder --chown=www-data:www-data /var/www/html/public/build /var/www/html/public/build + +RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,id=apt-lib,target=/var/lib/apt,sharing=locked \ + apt-get update && apt-get upgrade -y && apt-get install -y \ + tmux \ + && pecl install xdebug-3.5.0 \ + && docker-php-ext-enable xdebug + +RUN mkdir -p /var/www/html/storage/framework/cache \ + && mkdir -p /var/www/html/storage/framework/sessions \ + && mkdir -p /var/www/html/storage/framework/views \ + && mkdir -p /var/www/html/storage/logs \ + && mkdir -p /var/www/html/storage/app/public \ + && chown -R www-data:www-data /var/www/html/storage \ + && composer install --no-cache --no-progress --no-interaction --verbose \ + && composer clear-cache + +ENV XDEBUG_MODE=coverage # ----------------------------------------------------- # APP - PROD # ----------------------------------------------------- diff --git a/_changelog/next-upgrade.md b/_changelog/next-upgrade.md index a8e6cfc18..3180d41c9 100644 --- a/_changelog/next-upgrade.md +++ b/_changelog/next-upgrade.md @@ -12,6 +12,8 @@ [//]: # (Describe what the user needs to do.) +- Run the migrations to add the assistent model + ## Notes [//]: # (Any additional warnings or tips for administrators performing the upgrade.) diff --git a/_changelog/next.md b/_changelog/next.md index 37d902ab9..4f856ba62 100644 --- a/_changelog/next.md +++ b/_changelog/next.md @@ -4,6 +4,8 @@ [//]: # (- The main new features and changes in this version.) +- The assistent model is introduced + ### Quality of Life [//]: # (- Improvements and enhancements that improve the user experience.) diff --git a/app/Console/Commands/Ai/CheckModelStatusCommand.php b/app/Console/Commands/Ai/CheckModelStatusCommand.php index 298eb4474..6994cbaf5 100644 --- a/app/Console/Commands/Ai/CheckModelStatusCommand.php +++ b/app/Console/Commands/Ai/CheckModelStatusCommand.php @@ -32,7 +32,7 @@ public function handle(): void $models = $this->aiService->getAvailableModels()->models; foreach ($models as $model) { $this->output->write("Checking model: {$model->getId()}"); - $status = $model->getClient()->getStatus(); + $status = $model->getClient()->getStatus($model); $this->output->writeln(" is " . $status->value); $this->modelStatusDb->setModelStatus($model, $status); } diff --git a/app/Events/AssistantCreated.php b/app/Events/AssistantCreated.php new file mode 100644 index 000000000..78b6aeb91 --- /dev/null +++ b/app/Events/AssistantCreated.php @@ -0,0 +1,12 @@ +authorizeResource(Assistant::class, 'assistant'); + } + + public function created(Assistant $assistant, AssistantRequest $request, AssistantQuery $query): void + { + Event::dispatch(new AssistantCreated($assistant)); + } + + public function updated(Assistant $assistant, AssistantRequest $request, AssistantQuery $query): void + { + $changedKeys = array_values(array_filter( + array_keys($assistant->getChanges()), + fn (string $key) => $key !== 'updated_at', + )); + + $validated = $request->validated(); + if (isset($validated['tags'])) { + $changedKeys[] = 'tags'; + } + if (isset($validated['ai_tools'])) { + $changedKeys[] = 'ai_tools'; + } + if (isset($validated['user_prompts'])) { + $changedKeys[] = 'user_prompts'; + } + + if ($changedKeys !== []) { + Event::dispatch(new AssistantUpdated( + $assistant, + $validated['version_text'] ?? null, + $changedKeys, + )); + } + } + + public function remix(AssistantSchema $schema, AssistantQuery $query, Assistant $assistant): Responsable + { + $this->authorize('remix', $assistant); + + $remixed = $this->assistantService->remix($assistant, request()->user()); + + return DataResponse::make($remixed) + ->withQueryParameters($query) + ->didCreate(); + } + + public function feedback( + FeedbackAssistantRequest $request, + AssistantSchema $schema, + AssistantQuery $query, + Assistant $assistant, + ): Responsable { + $this->authorize('view', $assistant); + + $this->assistantService->feedback( + $assistant, + $request->user(), + $request->input('data.attributes.text'), + ); + + return DataResponse::make($assistant) + ->withQueryParameters($query); + } + + public function release(ReleaseAssistantRequest $request, AssistantSchema $schema, AssistantQuery $query, Assistant $assistant): Responsable + { + $this->authorize('release', $assistant); + + $releaseStage = ReleaseStage::from($request->input('data.attributes.release_stage')); + + $assistant = $this->assistantService->release($assistant, $releaseStage); + + return DataResponse::make($assistant) + ->withQueryParameters($query); + } + + public function favorite( + FavoriteAssistantRequest $request, + AssistantSchema $schema, + AssistantQuery $query, + Assistant $assistant, + ): Responsable { + $this->authorize('favorite', $assistant); + + $this->assistantService->setFavorite( + $assistant, + $request->user(), + $request->boolean('data.attributes.is_favorite'), + ); + + return DataResponse::make($assistant->fresh()) + ->withQueryParameters($query); + } + + public function chatTest( + ChatTestAssistantRequest $request, + AssistantSchema $schema, + AssistantQuery $query, + Assistant $assistant, + ): StreamedResponse { + $this->authorize('view', $assistant); + + $attrs = $request->input('data.attributes'); + $runner = app(AssistantChatRunnerInterface::class); + $messages = $this->buildMessages($attrs['messages'], $assistant); + + return response()->stream(function () use ($runner, $messages, $assistant, $attrs) { + $this->writeEvent('stream_start', ['model' => $assistant->model]); + + $fullContent = ''; + + try { + $generator = $runner->stream( + systemPrompt: $assistant->system_prompt ?? '', + messages: $messages, + model: $assistant->model, + tools: $attrs['tools'] ?? [], + params: $attrs['params'] ?? [], + ); + + foreach ($generator as $chunk) { + $this->writeEvent($chunk['type'], $chunk['content']); + + if ($chunk['type'] === 'text_delta') { + $fullContent .= $chunk['content']; + } + } + + $this->writeEvent('message', [ + 'type' => 'messages', + 'id' => (string) Str::uuid(), + 'attributes' => [ + 'content' => $fullContent, + 'model' => $assistant->model, + 'status' => 'completed', + ], + ]); + + $this->writeEvent('stream_end', ['reason' => 'stop']); + } catch (\Throwable $e) { + $this->writeEvent('stream_failed', ['message' => $e->getMessage()]); + } + }, 200, [ + 'Content-Type' => 'text/event-stream', + 'Cache-Control' => 'no-cache', + 'X-Accel-Buffering' => 'no', + 'Connection' => 'keep-alive', + ]); + } + + private function writeEvent(string $event, mixed $data): void + { + echo "event: {$event}\ndata: ".json_encode($data)."\n\n"; + if (ob_get_level() > 0) { + ob_flush(); + } + flush(); + } + + private function buildMessages(array $messages, Assistant $assistant): array + { + $payload = []; + + if (! empty($assistant->system_prompt)) { + $payload[] = [ + 'role' => 'system', + 'content' => ['text' => $assistant->system_prompt], + ]; + } + + foreach ($messages as $msg) { + $payload[] = [ + 'role' => $msg['role'], + 'content' => $msg['content'] ?? ['text' => ''], + ]; + } + + return $payload; + } +} diff --git a/app/Http/Controllers/AssistantLanguageController.php b/app/Http/Controllers/AssistantLanguageController.php new file mode 100644 index 000000000..56642fff9 --- /dev/null +++ b/app/Http/Controllers/AssistantLanguageController.php @@ -0,0 +1,13 @@ +authorizeResource(Review::class, 'assistant_review'); + } + + public function update(ReviewRequest $request, ReviewSchema $schema, ReviewQuery $query, Review $review): Responsable + { + $this->authorize('update', $review); + + $review = $this->reviewService->update($review, $request->validated()); + + return DataResponse::make($review) + ->withQueryParameters($query); + } + + public function destroy(Review $review): Response + { + $this->authorize('delete', $review); + + return response()->noContent(); + } +} diff --git a/app/Http/Controllers/TagController.php b/app/Http/Controllers/TagController.php new file mode 100644 index 000000000..3ccd6ed02 --- /dev/null +++ b/app/Http/Controllers/TagController.php @@ -0,0 +1,15 @@ +selfLink()) { + $links->push($self); + } + + $base = $this->selfUrl(); + $user = $request?->user(); + + if ($base && $user) { + $gate = Gate::forUser($user); + + foreach ($this->resolveActionNames() as $action) { + $response = $gate->inspect($action, $this->resource); + $links->push(new Link( + $action, + $base . '/actions/' . $action, + ['message' => $response->allowed() ? 'ALLOWED' : 'DENIED'], + )); + } + } + + return $links; + } + + protected function resolveActionNames(): iterable + { + $resourceType = $this->schema::type(); + + foreach (app('router')->getRoutes() as $route) { + $uri = $route->uri(); + $name = $route->getName(); + + if ( + is_string($name) + && is_string($uri) + && str_contains($name, ".{$resourceType}.") + && str_contains($uri, '/actions/') + && str_contains($uri, '{') + ) { + $method = $route->getActionMethod(); + + if (is_string($method) && $method !== 'Closure') { + yield $method; + } + } + } + } +} diff --git a/app/JsonApi/V1/AiModelStatuses/AiModelStatusSchema.php b/app/JsonApi/V1/AiModelStatuses/AiModelStatusSchema.php new file mode 100644 index 000000000..32594e30e --- /dev/null +++ b/app/JsonApi/V1/AiModelStatuses/AiModelStatusSchema.php @@ -0,0 +1,48 @@ +readOnly(), + DateTime::make('updated_at')->readOnly(), + ]; + } + + public function authorizable(): bool + { + return false; + } + + public function filters(): array + { + return []; + } + + public function pagination(): ?\LaravelJsonApi\Eloquent\Pagination\PagePagination + { + return null; + } +} diff --git a/app/JsonApi/V1/AiModels/AiModelSchema.php b/app/JsonApi/V1/AiModels/AiModelSchema.php new file mode 100644 index 000000000..76fee3f2b --- /dev/null +++ b/app/JsonApi/V1/AiModels/AiModelSchema.php @@ -0,0 +1,65 @@ +readOnly(), + DateTime::make('updated_at')->readOnly(), + + BelongsTo::make('provider')->type('ai-providers')->readOnly(), + BelongsToMany::make('assignedTools', 'assignedTools')->type('ai-tools')->readOnly(), + HasOne::make('status')->type('ai-model-statuses')->readOnly(), + ]; + } + + public function authorizable(): bool + { + return false; + } + + public function filters(): array + { + return []; + } + + public function pagination(): ?PagePagination + { + return PagePagination::make(); + } +} diff --git a/app/JsonApi/V1/AiProviders/AiProviderSchema.php b/app/JsonApi/V1/AiProviders/AiProviderSchema.php new file mode 100644 index 000000000..622d0b7f8 --- /dev/null +++ b/app/JsonApi/V1/AiProviders/AiProviderSchema.php @@ -0,0 +1,61 @@ +readOnly(), + DateTime::make('updated_at')->readOnly(), + + HasMany::make('models')->type('ai-models')->readOnly(), + ]; + } + + public function authorizable(): bool + { + return false; + } + + public function filters(): array + { + return [ + ToolCapabilityFilter::make(), + ]; + } + + public function pagination(): ?PagePagination + { + return PagePagination::make(); + } +} diff --git a/app/JsonApi/V1/AiProviders/ToolCapabilityFilter.php b/app/JsonApi/V1/AiProviders/ToolCapabilityFilter.php new file mode 100644 index 000000000..bcbefd8b4 --- /dev/null +++ b/app/JsonApi/V1/AiProviders/ToolCapabilityFilter.php @@ -0,0 +1,31 @@ +filterByToolCapability($query, $value); + } +} diff --git a/app/JsonApi/V1/AiTools/AiToolSchema.php b/app/JsonApi/V1/AiTools/AiToolSchema.php new file mode 100644 index 000000000..a433642c6 --- /dev/null +++ b/app/JsonApi/V1/AiTools/AiToolSchema.php @@ -0,0 +1,63 @@ +readOnly(), + DateTime::make('updated_at')->readOnly(), + + BelongsTo::make('server')->type('mcp-servers')->readOnly(), + BelongsToMany::make('models')->type('ai-models')->readOnly(), + ]; + } + + public function authorizable(): bool + { + return false; + } + + public function filters(): array + { + return []; + } + + public function pagination(): ?PagePagination + { + return PagePagination::make(); + } +} diff --git a/app/JsonApi/V1/Assistants/AssistantCollectionQuery.php b/app/JsonApi/V1/Assistants/AssistantCollectionQuery.php new file mode 100644 index 000000000..d5604bc96 --- /dev/null +++ b/app/JsonApi/V1/Assistants/AssistantCollectionQuery.php @@ -0,0 +1,22 @@ + [JsonApiRule::fieldSets()], + 'filter' => [JsonApiRule::filter()], + 'include' => [JsonApiRule::includePaths()], + 'page' => [JsonApiRule::page()], + 'sort' => [JsonApiRule::notSupported()], + ]; + } +} diff --git a/app/JsonApi/V1/Assistants/AssistantFavoriteFilter.php b/app/JsonApi/V1/Assistants/AssistantFavoriteFilter.php new file mode 100644 index 000000000..6e11cb6df --- /dev/null +++ b/app/JsonApi/V1/Assistants/AssistantFavoriteFilter.php @@ -0,0 +1,36 @@ +filterByIsFavorite( + $query, + Auth::user(), + filter_var($value, FILTER_VALIDATE_BOOLEAN), + ); + } +} diff --git a/app/JsonApi/V1/Assistants/AssistantNameFilter.php b/app/JsonApi/V1/Assistants/AssistantNameFilter.php new file mode 100644 index 000000000..26449a9d9 --- /dev/null +++ b/app/JsonApi/V1/Assistants/AssistantNameFilter.php @@ -0,0 +1,31 @@ +filterByName($query, $value); + } +} diff --git a/app/JsonApi/V1/Assistants/AssistantQuery.php b/app/JsonApi/V1/Assistants/AssistantQuery.php new file mode 100644 index 000000000..3eee31a65 --- /dev/null +++ b/app/JsonApi/V1/Assistants/AssistantQuery.php @@ -0,0 +1,19 @@ + [JsonApiRule::fieldSets()], + 'include' => [JsonApiRule::includePaths()], + ]; + } +} diff --git a/app/JsonApi/V1/Assistants/AssistantRequest.php b/app/JsonApi/V1/Assistants/AssistantRequest.php new file mode 100644 index 000000000..c7bfe3ea9 --- /dev/null +++ b/app/JsonApi/V1/Assistants/AssistantRequest.php @@ -0,0 +1,78 @@ + ['nullable', 'string', 'max:255'], + 'handle' => [ + 'nullable', + 'string', + 'max:255', + 'unique:assistants,handle', + 'unique:ai_models,label', + ], + 'system_prompt' => ['nullable', 'string'], + 'greeting' => ['nullable', 'string'], + 'description' => ['nullable', 'string'], + 'detail_description' => ['nullable', 'string'], + 'allow_remix' => ['nullable', 'boolean'], + 'allow_model_select' => ['nullable', 'boolean'], + 'release_stage' => ['nullable', Rule::enum(ReleaseStage::class)], + 'formality' => ['nullable', 'string'], + 'category' => ['nullable', JsonApiRule::toOne()], + 'language' => ['nullable', JsonApiRule::toOne()], + 'model' => ['nullable', 'string'], + 'model_length' => ['nullable', 'integer', 'min:1'], + 'model_temp' => ['nullable', 'numeric', 'min:0', 'max:1'], + 'model_top_p' => ['nullable', 'numeric', 'min:0', 'max:1'], + 'user_prompts' => [JsonApiRule::toMany()], + 'ai_tools' => [JsonApiRule::toMany()], + 'tags' => [JsonApiRule::toMany()], + ]; + + if ($this->isUpdating()) { + $assistant = $this->model(); + + $rules['name'] = ['sometimes', 'string', 'max:255']; + $rules['handle'] = [ + 'sometimes', + 'nullable', + 'string', + 'max:255', + 'unique:assistants,handle,' . $assistant?->id, + 'unique:ai_models,label', + ]; + $rules['system_prompt'] = ['sometimes', 'nullable', 'string']; + $rules['greeting'] = ['sometimes', 'nullable', 'string']; + $rules['description'] = ['sometimes', 'nullable', 'string']; + $rules['detail_description'] = ['sometimes', 'nullable', 'string']; + $rules['allow_remix'] = ['sometimes', 'boolean']; + $rules['allow_model_select'] = ['sometimes', 'boolean']; + $rules['category'] = ['sometimes', JsonApiRule::toOne()]; + $rules['language'] = ['sometimes', JsonApiRule::toOne()]; + $rules['release_stage'] = ['sometimes', Rule::enum(ReleaseStage::class)]; + $rules['formality'] = ['sometimes', 'string']; + $rules['model'] = ['sometimes', 'string']; + $rules['model_length'] = ['sometimes', 'integer', 'min:1']; + $rules['model_temp'] = ['sometimes', 'numeric', 'min:0', 'max:1']; + $rules['model_top_p'] = ['sometimes', 'numeric', 'min:0', 'max:1']; + $rules['user_prompts'] = ['sometimes', JsonApiRule::toMany()]; + $rules['ai_tools'] = ['sometimes', JsonApiRule::toMany()]; + $rules['tags'] = ['sometimes', JsonApiRule::toMany()]; + $rules['version_text'] = ['sometimes', 'nullable', 'string']; + } + + return $rules; + } +} diff --git a/app/JsonApi/V1/Assistants/AssistantResource.php b/app/JsonApi/V1/Assistants/AssistantResource.php new file mode 100644 index 000000000..d9ed4715b --- /dev/null +++ b/app/JsonApi/V1/Assistants/AssistantResource.php @@ -0,0 +1,19 @@ +actionLinks($request); + } +} diff --git a/app/JsonApi/V1/Assistants/AssistantSchema.php b/app/JsonApi/V1/Assistants/AssistantSchema.php new file mode 100644 index 000000000..da088d7ea --- /dev/null +++ b/app/JsonApi/V1/Assistants/AssistantSchema.php @@ -0,0 +1,94 @@ +sortable()->readOnly(), + DateTime::make('updated_at')->sortable()->readOnly(), + Str::make('version_text')->hidden(), + Boolean::make('is_favorite')->readOnly(), + + BelongsTo::make('language')->type('assistant-languages'), + BelongsTo::make('category')->type('assistant-categories'), + HasMany::make('user_prompts', 'user_prompts'), + BelongsToMany::make('ai_tools', 'ai_tools'), + BelongsToMany::make('tags', 'tags'), + BelongsTo::make('creator', 'creator')->type('users')->readOnly(), + BelongsTo::make('remix_creator', 'remix_creator')->type('users')->readOnly(), + BelongsTo::make('remixed_assistant', 'remixed_assistant')->type('assistants')->readOnly(), + HasMany::make('versions', 'versions')->readOnly(), + BelongsTo::make('organization')->readOnly(), + HasOne::make('review', 'review')->type('assistant-reviews')->readOnly(), + HasMany::make('feedback', 'feedback')->readOnly(), + ]; + } + + public function filters(): array + { + return [ + WhereHas::make($this, 'category'), + AssistantNameFilter::make(), + AssistantFavoriteFilter::make(), + Where::make('release_stage') + ]; + } + + public function pagination(): ?PagePagination + { + return PagePagination::make(); + } + + public function indexQuery(?Request $request, Builder $query): Builder + { + $user = $request?->user(); + + if ($user === null) { + return $query; + } + + return app(AssistantRepository::class) + ->filterVisibleForUser($query, $user) + ->withCount(['favoritedByUsers as is_favorite' => fn($q) => $q->where('user_id', $user->id)]); + } +} diff --git a/app/JsonApi/V1/Assistants/ChatTestAssistantRequest.php b/app/JsonApi/V1/Assistants/ChatTestAssistantRequest.php new file mode 100644 index 000000000..bdda4f591 --- /dev/null +++ b/app/JsonApi/V1/Assistants/ChatTestAssistantRequest.php @@ -0,0 +1,27 @@ + ['required', 'in:assistants'], + 'data.id' => ['required'], + 'data.attributes.messages' => ['required', 'array'], + 'data.attributes.messages.*.role' => ['required', 'string'], + 'data.attributes.messages.*.content' => ['required', 'array'], + 'data.attributes.messages.*.content.text' => ['nullable', 'string'], + 'data.attributes.tools' => ['nullable', 'array'], + 'data.attributes.params' => ['nullable', 'array'], + ]; + } +} diff --git a/app/JsonApi/V1/Assistants/FavoriteAssistantRequest.php b/app/JsonApi/V1/Assistants/FavoriteAssistantRequest.php new file mode 100644 index 000000000..4ec9e7ebd --- /dev/null +++ b/app/JsonApi/V1/Assistants/FavoriteAssistantRequest.php @@ -0,0 +1,20 @@ + ['required', 'boolean'], + ]; + } +} diff --git a/app/JsonApi/V1/Assistants/FeedbackAssistantRequest.php b/app/JsonApi/V1/Assistants/FeedbackAssistantRequest.php new file mode 100644 index 000000000..707f0cb2b --- /dev/null +++ b/app/JsonApi/V1/Assistants/FeedbackAssistantRequest.php @@ -0,0 +1,20 @@ + ['required', 'string'], + ]; + } +} diff --git a/app/JsonApi/V1/Assistants/ReleaseAssistantRequest.php b/app/JsonApi/V1/Assistants/ReleaseAssistantRequest.php new file mode 100644 index 000000000..ad00b588e --- /dev/null +++ b/app/JsonApi/V1/Assistants/ReleaseAssistantRequest.php @@ -0,0 +1,22 @@ + ['required', 'string', Rule::enum(ReleaseStage::class)], + ]; + } +} diff --git a/app/JsonApi/V1/Categories/CategoryCollectionQuery.php b/app/JsonApi/V1/Categories/CategoryCollectionQuery.php new file mode 100644 index 000000000..43581302e --- /dev/null +++ b/app/JsonApi/V1/Categories/CategoryCollectionQuery.php @@ -0,0 +1,21 @@ + [JsonApiRule::fieldSets()], + 'include' => [JsonApiRule::includePaths()], + 'page' => [JsonApiRule::page()], + 'sort' => [JsonApiRule::sort()], + ]; + } +} diff --git a/app/JsonApi/V1/Categories/CategoryQuery.php b/app/JsonApi/V1/Categories/CategoryQuery.php new file mode 100644 index 000000000..37afa8558 --- /dev/null +++ b/app/JsonApi/V1/Categories/CategoryQuery.php @@ -0,0 +1,19 @@ + [JsonApiRule::fieldSets()], + 'include' => [JsonApiRule::includePaths()], + ]; + } +} diff --git a/app/JsonApi/V1/Categories/CategoryRequest.php b/app/JsonApi/V1/Categories/CategoryRequest.php new file mode 100644 index 000000000..01eb459af --- /dev/null +++ b/app/JsonApi/V1/Categories/CategoryRequest.php @@ -0,0 +1,22 @@ + ['required', 'string', Rule::unique('text')] + ]; + } +} diff --git a/app/JsonApi/V1/Categories/CategorySchema.php b/app/JsonApi/V1/Categories/CategorySchema.php new file mode 100644 index 000000000..ea8da2ae9 --- /dev/null +++ b/app/JsonApi/V1/Categories/CategorySchema.php @@ -0,0 +1,60 @@ +sortable(), + DateTime::make('created_at')->sortable()->readOnly(), + DateTime::make('updated_at')->sortable()->readOnly(), + + HasMany::make('assistants')->readOnly(), + ]; + } + + public function authorizable(): bool + { + return false; + } + + public function filters(): array + { + return [ + WhereIn::make('text')->delimiter(','), + ]; + } + + public function indexQuery(?Request $request, Builder $query): Builder + { + return $query->orderBy('text'); + } + + public function pagination(): ?PagePagination + { + return PagePagination::make(); + } +} diff --git a/app/JsonApi/V1/Languages/LanguageCollectionQuery.php b/app/JsonApi/V1/Languages/LanguageCollectionQuery.php new file mode 100644 index 000000000..102cc1d0e --- /dev/null +++ b/app/JsonApi/V1/Languages/LanguageCollectionQuery.php @@ -0,0 +1,21 @@ + [JsonApiRule::fieldSets()], + 'include' => [JsonApiRule::includePaths()], + 'page' => [JsonApiRule::page()], + 'sort' => [JsonApiRule::sort()], + ]; + } +} diff --git a/app/JsonApi/V1/Languages/LanguageQuery.php b/app/JsonApi/V1/Languages/LanguageQuery.php new file mode 100644 index 000000000..24035e2f4 --- /dev/null +++ b/app/JsonApi/V1/Languages/LanguageQuery.php @@ -0,0 +1,19 @@ + [JsonApiRule::fieldSets()], + 'include' => [JsonApiRule::includePaths()], + ]; + } +} diff --git a/app/JsonApi/V1/Languages/LanguageRequest.php b/app/JsonApi/V1/Languages/LanguageRequest.php new file mode 100644 index 000000000..cdaae4c36 --- /dev/null +++ b/app/JsonApi/V1/Languages/LanguageRequest.php @@ -0,0 +1,23 @@ + ['required', 'string', Rule::unique('text')] + ]; + } + +} diff --git a/app/JsonApi/V1/Languages/LanguageSchema.php b/app/JsonApi/V1/Languages/LanguageSchema.php new file mode 100644 index 000000000..0d6f4799d --- /dev/null +++ b/app/JsonApi/V1/Languages/LanguageSchema.php @@ -0,0 +1,57 @@ +sortable(), + DateTime::make('created_at')->sortable()->readOnly(), + DateTime::make('updated_at')->sortable()->readOnly(), + + HasMany::make('assistants')->readOnly(), + ]; + } + + public function authorizable(): bool + { + return false; + } + + public function filters(): array + { + return []; + } + + public function indexQuery(?Request $request, Builder $query): Builder + { + return $query->orderBy('text'); + } + + public function pagination(): ?PagePagination + { + return PagePagination::make(); + } +} diff --git a/app/JsonApi/V1/McpServers/McpServerSchema.php b/app/JsonApi/V1/McpServers/McpServerSchema.php new file mode 100644 index 000000000..1921b87d2 --- /dev/null +++ b/app/JsonApi/V1/McpServers/McpServerSchema.php @@ -0,0 +1,59 @@ +readOnly(), + DateTime::make('updated_at')->readOnly(), + + HasMany::make('tools')->type('ai-tools')->readOnly(), + ]; + } + + public function authorizable(): bool + { + return false; + } + + public function filters(): array + { + return []; + } + + public function pagination(): ?PagePagination + { + return PagePagination::make(); + } +} diff --git a/app/JsonApi/V1/Organizations/OrganizationSchema.php b/app/JsonApi/V1/Organizations/OrganizationSchema.php new file mode 100644 index 000000000..7e6883c35 --- /dev/null +++ b/app/JsonApi/V1/Organizations/OrganizationSchema.php @@ -0,0 +1,33 @@ +readOnly(), + DateTime::make('updated_at')->readOnly(), + ]; + } + + public function filters(): array + { + return []; + } +} diff --git a/app/JsonApi/V1/Reviews/ReviewCollectionQuery.php b/app/JsonApi/V1/Reviews/ReviewCollectionQuery.php new file mode 100644 index 000000000..145f89054 --- /dev/null +++ b/app/JsonApi/V1/Reviews/ReviewCollectionQuery.php @@ -0,0 +1,20 @@ + [JsonApiRule::fieldSets()], + 'include' => [JsonApiRule::includePaths()], + 'page' => [JsonApiRule::page()], + ]; + } +} diff --git a/app/JsonApi/V1/Reviews/ReviewQuery.php b/app/JsonApi/V1/Reviews/ReviewQuery.php new file mode 100644 index 000000000..d8d2a7ba6 --- /dev/null +++ b/app/JsonApi/V1/Reviews/ReviewQuery.php @@ -0,0 +1,19 @@ + [JsonApiRule::fieldSets()], + 'include' => [JsonApiRule::includePaths()], + ]; + } +} diff --git a/app/JsonApi/V1/Reviews/ReviewRequest.php b/app/JsonApi/V1/Reviews/ReviewRequest.php new file mode 100644 index 000000000..5075871d8 --- /dev/null +++ b/app/JsonApi/V1/Reviews/ReviewRequest.php @@ -0,0 +1,20 @@ + ['required', Rule::enum(ReviewStatus::class)], + 'reason' => ['required_if:status,denied', 'nullable', 'string'], + ]; + } +} diff --git a/app/JsonApi/V1/Reviews/ReviewSchema.php b/app/JsonApi/V1/Reviews/ReviewSchema.php new file mode 100644 index 000000000..d08478007 --- /dev/null +++ b/app/JsonApi/V1/Reviews/ReviewSchema.php @@ -0,0 +1,46 @@ +sortable()->readOnly(), + DateTime::make('updated_at')->sortable()->readOnly(), + + BelongsTo::make('assistant')->readOnly(), + ]; + } + + public function filters(): array + { + return []; + } + + public function pagination(): ?PagePagination + { + return PagePagination::make(); + } +} diff --git a/app/JsonApi/V1/Server.php b/app/JsonApi/V1/Server.php new file mode 100644 index 000000000..0c1ed2f8f --- /dev/null +++ b/app/JsonApi/V1/Server.php @@ -0,0 +1,51 @@ + ['required', 'string', Rule::unique('tags')], + ]; + } +} diff --git a/app/JsonApi/V1/Tags/TagSchema.php b/app/JsonApi/V1/Tags/TagSchema.php new file mode 100644 index 000000000..5874ef5fb --- /dev/null +++ b/app/JsonApi/V1/Tags/TagSchema.php @@ -0,0 +1,41 @@ +readOnly(), + DateTime::make('updated_at')->readOnly(), + + HasMany::make('assistants')->type('assistants')->readOnly(), + ]; + } + + public function filters(): array + { + return []; + } + + public function authorizable(): bool + { + return false; + } +} diff --git a/app/JsonApi/V1/UserPrompts/UserPromptSchema.php b/app/JsonApi/V1/UserPrompts/UserPromptSchema.php new file mode 100644 index 000000000..42d8e9356 --- /dev/null +++ b/app/JsonApi/V1/UserPrompts/UserPromptSchema.php @@ -0,0 +1,33 @@ +readOnly(), + DateTime::make('updated_at')->readOnly(), + ]; + } + + public function filters(): array + { + return []; + } +} diff --git a/app/JsonApi/V1/Users/UserSchema.php b/app/JsonApi/V1/Users/UserSchema.php new file mode 100644 index 000000000..fc3d9f0ff --- /dev/null +++ b/app/JsonApi/V1/Users/UserSchema.php @@ -0,0 +1,30 @@ +readOnly(), + DateTime::make('updated_at')->readOnly(), + ]; + } + + public function filters(): array + { + return []; + } +} diff --git a/app/Listeners/AssistantCreateInitialVersion.php b/app/Listeners/AssistantCreateInitialVersion.php new file mode 100644 index 000000000..5194faf93 --- /dev/null +++ b/app/Listeners/AssistantCreateInitialVersion.php @@ -0,0 +1,20 @@ +assistant->versions()->exists()) { + return; + } + + $event->assistant->versions()->create([ + 'text' => 'Initial version', + 'version' => 1.0, + ]); + } +} diff --git a/app/Listeners/AssistantReleaseStatus.php b/app/Listeners/AssistantReleaseStatus.php new file mode 100644 index 000000000..d01c3e396 --- /dev/null +++ b/app/Listeners/AssistantReleaseStatus.php @@ -0,0 +1,27 @@ +newStage === ReleaseStage::PRIVATE) { + return; + } + + $this->reviewRepository->updateOrCreateForAssistant( + $event->assistant->id, + ['status' => ReviewStatus::PENDING->value], + ); + } +} diff --git a/app/Listeners/AssistantUpdatedVersion.php b/app/Listeners/AssistantUpdatedVersion.php new file mode 100644 index 000000000..27df914eb --- /dev/null +++ b/app/Listeners/AssistantUpdatedVersion.php @@ -0,0 +1,24 @@ +assistant->release_stage === ReleaseStage::DRAFT->value) { + return; + } + + $lastVersion = $event->assistant->versions()->max('version') ?? 0.0; + + $event->assistant->versions()->create([ + 'text' => $event->versionText ?? 'Updated', + 'version' => $lastVersion + 1.0, + 'changed_keys' => $event->changedKeys, + ]); + } +} diff --git a/app/Listeners/ResetReviewOnUpdate.php b/app/Listeners/ResetReviewOnUpdate.php new file mode 100644 index 000000000..2107ff36d --- /dev/null +++ b/app/Listeners/ResetReviewOnUpdate.php @@ -0,0 +1,26 @@ +assistant->release_stage, [ReleaseStage::PRIVATE->value, ReleaseStage::DRAFT->value], true)) { + return; + } + + $this->reviewRepository->resetReviewForAssistant( + $event->assistant->id, + ); + } +} diff --git a/app/Models/Assistants/Assistant.php b/app/Models/Assistants/Assistant.php new file mode 100644 index 000000000..0387d232d --- /dev/null +++ b/app/Models/Assistants/Assistant.php @@ -0,0 +1,176 @@ + '', + 'system_prompt' => '', + 'greeting' => '', + 'description' => '', + 'detail_description' => '', + 'allow_remix' => false, + 'allow_model_select' => false, + 'release_stage' => ReleaseStage::DRAFT, + 'formality' => 'neutral', + 'model' => '', + 'model_length' => 0, + 'model_temp' => 0.0, + 'model_top_p' => 0.0, + ]; + + protected static function booted(): void + { + static::creating(function (Assistant $assistant) { + if ($assistant->creator_id !== null) { + return; + } + + $user = Auth::user(); + if ($user === null) { + return; + } + + $assistant->creator_id = $user->id; + $assistant->remixed_creator_id ??= null; + $assistant->organization_id = app(OrganizationRepository::class)->getForUser($user)?->id; + }); + } + + protected $fillable = [ + 'name', + 'handle', + 'system_prompt', + 'greeting', + 'description', + 'detail_description', + 'allow_remix', + 'allow_model_select', + 'language_id', + 'category_id', + 'release_stage', + 'formality', + 'model', + 'model_length', + 'model_temp', + 'model_top_p', + 'creator_id', + 'remixed_creator_id', + 'remixed_assistant_id', + 'organization_id', + ]; + + protected function casts(): array + { + return [ + 'allow_remix' => 'boolean', + 'allow_model_select' => 'boolean', + ]; + } + + public function category(): BelongsTo + { + return $this->belongsTo(Category::class); + } + + public function organization(): BelongsTo + { + return $this->belongsTo(Organization::class); + } + + public function language(): BelongsTo + { + return $this->belongsTo(Language::class); + } + + public function user_prompts(): HasMany + { + return $this->hasMany(UserPrompt::class); + } + + public function versions(): HasMany + { + return $this->hasMany(Version::class); + } + + public function tags(): BelongsToMany + { + return $this->belongsToMany(Tag::class); + } + + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'creator_id'); + } + + public function remix_creator(): BelongsTo + { + return $this->belongsTo(User::class, 'remixed_creator_id'); + } + + public function remixed_assistant(): BelongsTo + { + return $this->belongsTo(Assistant::class, 'remixed_assistant_id'); + } + + public function copies(): HasMany + { + return $this->hasMany(Assistant::class, 'remixed_assistant_id'); + } + + public function ai_tools(): BelongsToMany + { + return $this->belongsToMany(AiTool::class, 'assistant_tools'); + } + + public function feedback(): HasMany + { + return $this->hasMany(Feedback::class); + } + + public function attachments() + { + return $this->morphMany(Attachment::class, 'attachable'); + } + + public function review(): HasOne + { + return $this->hasOne(Review::class); + } + + public function favoritedByUsers(): BelongsToMany + { + return $this->belongsToMany(User::class, 'assistant_favorite_users') + ->withTimestamps(); + } + + public function getIsFavoriteAttribute(): bool + { + if (array_key_exists('is_favorite', $this->attributes)) { + return (bool) $this->attributes['is_favorite']; + } + + return $this->favoritedByUsers() + ->where('user_id', Auth::id()) + ->exists(); + } +} diff --git a/app/Models/Assistants/Category.php b/app/Models/Assistants/Category.php new file mode 100644 index 000000000..0e0e57b70 --- /dev/null +++ b/app/Models/Assistants/Category.php @@ -0,0 +1,23 @@ +hasMany(Assistant::class); + } +} diff --git a/app/Models/Assistants/Feedback.php b/app/Models/Assistants/Feedback.php new file mode 100644 index 000000000..567ff52e4 --- /dev/null +++ b/app/Models/Assistants/Feedback.php @@ -0,0 +1,31 @@ +belongsTo(Assistant::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/Assistants/Language.php b/app/Models/Assistants/Language.php new file mode 100644 index 000000000..03eacd8a3 --- /dev/null +++ b/app/Models/Assistants/Language.php @@ -0,0 +1,23 @@ +hasMany(Assistant::class); + } +} diff --git a/app/Models/Assistants/Review.php b/app/Models/Assistants/Review.php new file mode 100644 index 000000000..93bbc2e83 --- /dev/null +++ b/app/Models/Assistants/Review.php @@ -0,0 +1,25 @@ +belongsTo(Assistant::class); + } +} diff --git a/app/Models/Assistants/Tag.php b/app/Models/Assistants/Tag.php new file mode 100644 index 000000000..d174b24e6 --- /dev/null +++ b/app/Models/Assistants/Tag.php @@ -0,0 +1,19 @@ +belongsToMany(Assistant::class); + } +} diff --git a/app/Models/Assistants/UserPrompt.php b/app/Models/Assistants/UserPrompt.php new file mode 100644 index 000000000..fefabd146 --- /dev/null +++ b/app/Models/Assistants/UserPrompt.php @@ -0,0 +1,14 @@ + 'decimal:1', + 'changed_keys' => 'array', + ]; + + public function assistant(): BelongsTo + { + return $this->belongsTo(Assistant::class); + } +} diff --git a/app/Models/Organization.php b/app/Models/Organization.php new file mode 100644 index 000000000..48b9ab8c0 --- /dev/null +++ b/app/Models/Organization.php @@ -0,0 +1,40 @@ +belongsToMany(User::class, 'organization_user') + ->withPivot('role') + ->withTimestamps(); + } + + public function adminUsers(): BelongsToMany + { + return $this->belongsToMany(User::class, 'organization_user') + ->withPivot('role') + ->withTimestamps() + ->wherePivot('role', 'admin'); + } + + public function assistants(): HasMany + { + return $this->hasMany(Assistant::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index c412b4a6b..9d3c33cb8 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -4,6 +4,8 @@ use App\Models\Announcements\Announcement; use App\Models\Announcements\AnnouncementUser; +use App\Models\Assistants\Assistant; +use App\Models\Organization; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; @@ -32,6 +34,13 @@ public function members() return $this->hasMany(Member::class)->where('isRemoved', false); } + public function organizations() + { + return $this->belongsToMany(Organization::class, 'organization_user') + ->withPivot('role') + ->withTimestamps(); + } + public function rooms() { return $this->belongsToMany(Room::class, 'members', 'user_id', 'room_id') @@ -44,6 +53,12 @@ public function conversations() return $this->hasMany(AiConv::class); } + public function favoriteAssistants() + { + return $this->belongsToMany(Assistant::class, 'assistant_favorite_users') + ->withTimestamps(); + } + public function invitations() { return $this->hasMany(Invitation::class, 'username', 'username'); diff --git a/app/Policies/AssistantPolicy.php b/app/Policies/AssistantPolicy.php new file mode 100644 index 000000000..fa50b0dd1 --- /dev/null +++ b/app/Policies/AssistantPolicy.php @@ -0,0 +1,59 @@ +repository->isVisibleTo($assistant, $user); + } + + public function create(User $user): bool + { + return true; + } + + public function update(User $user, Assistant $assistant): bool + { + return $user->id === $assistant->creator_id; + } + + public function delete(User $user, Assistant $assistant): bool + { + return $user->id === $assistant->creator_id; + } + + public function remix(User $user, Assistant $assistant): bool + { + return (bool) $assistant->allow_remix; + } + + public function release(User $user, Assistant $assistant): bool + { + return $user->id === $assistant->creator_id; + } + + public function feedback(User $user, Assistant $assistant): bool + { + return $this->view($user, $assistant); + } + + public function favorite(User $user, Assistant $assistant): bool + { + return $this->view($user, $assistant); + } +} diff --git a/app/Policies/ReviewPolicy.php b/app/Policies/ReviewPolicy.php new file mode 100644 index 000000000..76140d724 --- /dev/null +++ b/app/Policies/ReviewPolicy.php @@ -0,0 +1,38 @@ +isOrgAdmin($user); + } + + public function update(User $user, Review $review): bool + { + return $this->isOrgAdminOf($user, $review->assistant->organization_id); + } + + private function isOrgAdmin(User $user): bool + { + return $user->organizations() + ->wherePivot('role', 'admin') + ->exists(); + } + + private function isOrgAdminOf(User $user, ?int $organizationId): bool + { + if ($organizationId === null) { + return false; + } + + return $user->organizations() + ->wherePivot('role', 'admin') + ->where('organizations.id', $organizationId) + ->exists(); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 1d5c5c934..89777e8eb 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -11,11 +11,13 @@ use App\Http\Middleware\RegistrationAccess; use App\Http\Middleware\SessionExpiryChecker; use App\Http\Middleware\TokenCreationCheck; +use App\Services\Assistant\Chat\AssistantChatRunnerInterface; +use App\Services\Assistant\Chat\SimpleAssistantChatRunner; use App\Services\Storage\AvatarStorageService; use App\Services\Storage\FileStorageService; use App\Services\Storage\StorageServiceFactory; use App\Services\System\ScheduleWithDynamicIntervalFactory; -use Illuminate\Console\Scheduling\Event; +use Illuminate\Console\Scheduling\Event as ScheduleEvent; use Illuminate\Contracts\Foundation\Application; use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Support\Facades\Route; @@ -26,7 +28,6 @@ use League\Flysystem\WebDAV\WebDAVAdapter; use Sabre\DAV\Client; - class AppServiceProvider extends ServiceProvider { /** @@ -36,6 +37,7 @@ public function register(): void { $this->registerMiddlewareAliases(); $this->registerStorageServices(); + $this->registerAssistantServices(); } /** @@ -51,12 +53,12 @@ protected function registerStorageServices(): void { $this->app->singleton( AvatarStorageService::class, - fn(Application $app) => $app->make(StorageServiceFactory::class)->getAvatarStorage() + fn (Application $app) => $app->make(StorageServiceFactory::class)->getAvatarStorage() ); $this->app->singleton( FileStorageService::class, - fn(Application $app) => $app->make(StorageServiceFactory::class)->getFileStorage() + fn (Application $app) => $app->make(StorageServiceFactory::class)->getFileStorage() ); } @@ -90,18 +92,18 @@ private function bootSchedulerMacros(): void * Acts in the same way as the standard "command" method, but allows for dynamic scheduling intervals and arguments, which can be defined in the database or configuration. * This macro uses the {@see ScheduleWithDynamicIntervalFactory} to create the scheduled job, which handles the parsing and validation of the interval and arguments, and logs any errors that occur during scheduling. * - * @param string $command The command to be scheduled. - * @param array|null $parameters Optional parameters for the command. - * @param mixed $interval The scheduling interval, which can be a string representing a scheduling method or the special "never" value. - * @param mixed|null $intervalArgs Optional arguments for the scheduling method, which can be a JSON string, a single numeric value, or a simple string. - * @return Event|null Returns the scheduled Event if successful, or null if there was an error in scheduling due to invalid interval or arguments. + * @param string $command The command to be scheduled. + * @param array|null $parameters Optional parameters for the command. + * @param mixed $interval The scheduling interval, which can be a string representing a scheduling method or the special "never" value. + * @param mixed|null $intervalArgs Optional arguments for the scheduling method, which can be a JSON string, a single numeric value, or a simple string. + * @return ScheduleEvent|null Returns the scheduled Event if successful, or null if there was an error in scheduling due to invalid interval or arguments. */ function ( - string $command, - array|null $parameters = null, - mixed $interval = ScheduleWithDynamicIntervalFactory::NEVER_INTERVAL, - mixed $intervalArgs = null - ) use ($app): Event|null { + string $command, + ?array $parameters = null, + mixed $interval = ScheduleWithDynamicIntervalFactory::NEVER_INTERVAL, + mixed $intervalArgs = null + ) use ($app): ScheduleEvent|null { return $app->make(ScheduleWithDynamicIntervalFactory::class)->makeJob( command: $command, parameters: $parameters, @@ -124,4 +126,9 @@ private function registerMiddlewareAliases(): void Route::aliasMiddleware('signature_check', MandatorySignatureCheck::class); Route::aliasMiddleware('deprecated', DeprecatedEndpointMiddleware::class); } + + protected function registerAssistantServices(): void + { + $this->app->singleton(AssistantChatRunnerInterface::class, SimpleAssistantChatRunner::class); + } } diff --git a/app/Services/AI/Repositories/AiProviderRepository.php b/app/Services/AI/Repositories/AiProviderRepository.php new file mode 100644 index 000000000..5d8b000a5 --- /dev/null +++ b/app/Services/AI/Repositories/AiProviderRepository.php @@ -0,0 +1,17 @@ +whereHas('models.assignedTools', function ($q) use ($capability) { + $q->where('capability', $capability); + }); + } +} diff --git a/app/Services/Assistant/AssistantReviewService.php b/app/Services/Assistant/AssistantReviewService.php new file mode 100644 index 000000000..077a371d0 --- /dev/null +++ b/app/Services/Assistant/AssistantReviewService.php @@ -0,0 +1,39 @@ +reviewRepository->update($review, $data); + + if (ReviewStatus::from($data['status']) === ReviewStatus::DENIED) { + $assistant = $review->assistant; + $oldStage = ReleaseStage::from($assistant->release_stage); + + $this->assistantRepository->setReleaseStage($assistant, ReleaseStage::PRIVATE); + + Event::dispatch(new AssistantTriggerReleaseStatus($assistant, $oldStage, ReleaseStage::PRIVATE)); + } + + return $this->reviewRepository->find($review); + } +} diff --git a/app/Services/Assistant/AssistantService.php b/app/Services/Assistant/AssistantService.php new file mode 100644 index 000000000..c5fb49eab --- /dev/null +++ b/app/Services/Assistant/AssistantService.php @@ -0,0 +1,92 @@ +db->transaction(function () use ($source, $creator) { + $source->load(['user_prompts', 'ai_tools', 'tags', 'attachments', 'versions']); + + $organizationId = $this->organizationRepository->getForUser($creator)?->id; + + $clone = $this->repository->clone($source, $creator->id, $organizationId); + + $clone->user_prompts()->createMany( + $source->user_prompts->map(fn ($prompt) => ['text' => $prompt->text])->toArray() + ); + + $this->repository->syncTags($clone, $source->tags->pluck('id')->toArray()); + + $sourceCreator = $source->creator; + if ($this->organizationRepository->usersShareOrganization($creator, $sourceCreator)) { + $this->repository->syncTools($clone, $source->ai_tools->pluck('id')->toArray()); + } + + $latestVersion = $source->versions->sortByDesc('version')->first(); + if ($latestVersion) { + $clone->versions()->create([ + 'text' => $latestVersion->text, + 'version' => $latestVersion->version, + 'changed_keys' => $latestVersion->changed_keys, + ]); + } + + foreach ($source->attachments as $attachment) { + $clone->attachments()->create( + $attachment->only(['uuid', 'name', 'category', 'type', 'mime', 'user_id']) + ); + } + + Event::dispatch(new AssistantCreated($clone)); + + return $this->repository->loadRelations($clone, ['user_prompts', 'ai_tools', 'tags', 'attachments', 'versions']); + }); + } + + public function feedback(Assistant $assistant, User $user, string $text): void + { + $this->feedbackRepository->create($assistant, $user, $text); + } + + public function release(Assistant $assistant, ReleaseStage $newStage): Assistant + { + $oldStage = ReleaseStage::from($assistant->release_stage); + + $changed = $this->repository->setReleaseStage($assistant, $newStage); + + if ($changed) { + Event::dispatch(new AssistantTriggerReleaseStatus($assistant, $oldStage, $newStage)); + } + + return $assistant; + } + + public function setFavorite(Assistant $assistant, User $user, bool $isFavorite): void + { + $this->repository->setFavorite($assistant, $user, $isFavorite); + } +} diff --git a/app/Services/Assistant/Chat/AssistantChatRunnerInterface.php b/app/Services/Assistant/Chat/AssistantChatRunnerInterface.php new file mode 100644 index 000000000..df1a819af --- /dev/null +++ b/app/Services/Assistant/Chat/AssistantChatRunnerInterface.php @@ -0,0 +1,21 @@ + + */ + public function stream( + string $systemPrompt, + array $messages, + string $model, + array $tools = [], + array $params = [], + ): Generator; +} diff --git a/app/Services/Assistant/Chat/SimpleAssistantChatRunner.php b/app/Services/Assistant/Chat/SimpleAssistantChatRunner.php new file mode 100644 index 000000000..47f1fca4f --- /dev/null +++ b/app/Services/Assistant/Chat/SimpleAssistantChatRunner.php @@ -0,0 +1,131 @@ +buildPayload($systemPrompt, $messages, $model, $tools, $params); + + $chunks = []; + + $onData = function (AiResponse $response) use (&$chunks): void { + if ($response->error !== null) { + return; + } + + if ($response->type === 'tool_call' && ! empty($response->content['tool_call_id'] ?? null)) { + $chunks[] = [ + 'type' => 'tool_call', + 'content' => [ + 'tool_id' => $response->content['tool_call_id'], + 'tool_name' => $response->content['tool_name'] ?? '', + 'arguments' => $response->content['arguments'] ?? [], + ], + ]; + + return; + } + + if ($response->type === 'tool_result') { + $chunks[] = [ + 'type' => 'tool_result', + 'content' => [ + 'tool_id' => $response->content['tool_call_id'] ?? '', + 'tool_name' => $response->content['tool_name'] ?? '', + 'result' => $response->content['result'] ?? null, + ], + ]; + + return; + } + + if ($response->type === 'status') { + $chunks[] = [ + 'type' => 'status', + 'content' => $response->status ?? $response->content, + ]; + + return; + } + + if (! empty($response->content['text'] ?? null)) { + $chunks[] = [ + 'type' => 'text_delta', + 'content' => $response->content['text'], + ]; + } + }; + + $this->aiService->sendStreamRequest($payload, $onData); + + if ($chunks === []) { + $response = $this->aiService->sendRequest($payload); + + if (! empty($response->content['text'] ?? null)) { + $chunks[] = [ + 'type' => 'text_delta', + 'content' => $response->content['text'], + ]; + } + } + + foreach ($chunks as $chunk) { + yield $chunk; + } + } + + private function buildPayload( + string $systemPrompt, + array $messages, + string $model, + array $tools, + array $params, + ): array { + $payload = [ + 'model' => $model, + 'stream' => true, + 'messages' => [], + ]; + + if ($systemPrompt !== '') { + $payload['messages'][] = [ + 'role' => 'system', + 'content' => ['text' => $systemPrompt], + ]; + } + + foreach ($messages as $message) { + $payload['messages'][] = [ + 'role' => $message['role'], + 'content' => $message['content'] ?? ['text' => ''], + ]; + } + + if ($tools !== []) { + $payload['tools'] = $tools; + } + + if ($params !== []) { + $payload['params'] = $params; + } + + return $payload; + } +} diff --git a/app/Services/Assistant/Repositories/AssistantRepository.php b/app/Services/Assistant/Repositories/AssistantRepository.php new file mode 100644 index 000000000..a16767653 --- /dev/null +++ b/app/Services/Assistant/Repositories/AssistantRepository.php @@ -0,0 +1,107 @@ +whereHas('category', function ($q) use ($text) { + $q->where('text', $text); + }); + } + + public function filterByName(Builder $query, string $name): Builder + { + // Database independent case insensitive filter + return $query->whereRaw('LOWER(name) LIKE ?', ['%' . strtolower($name) . '%']); + } + + public function filterVisibleForUser(Builder $query, User $user): Builder + { + return $query->where(function ($q) use ($user) { + $q->where('release_stage', '!=', 'private') + ->orWhere('creator_id', $user->id); + }); + } + + public function isVisibleTo(Assistant $assistant, User $user): bool + { + return $assistant->release_stage !== 'private' || $assistant->creator_id === $user->id; + } + + public function clone(Assistant $source, int $creatorId, ?int $organizationId = null): Assistant + { + return Assistant::create([ + 'name' => $source->name, + 'description' => $source->description, + 'system_prompt' => $source->system_prompt, + 'greeting' => $source->greeting, + 'allow_remix' => $source->allow_remix, + 'allow_model_select' => $source->allow_model_select, + 'model_length' => $source->model_length, + 'model_temp' => $source->model_temp, + 'model_top_p' => $source->model_top_p, + 'model' => $source->model, + 'formality' => $source->formality, + 'detail_description' => $source->detail_description, + 'language_id' => $source->language_id, + 'category_id' => $source->category_id, + 'creator_id' => $creatorId, + 'remixed_creator_id' => $source->creator_id, + 'remixed_assistant_id' => $source->id, + 'release_stage' => 'private', + 'organization_id' => $organizationId, + ]); + } + + public function syncTools(Assistant $assistant, array $toolIds): array + { + return $assistant->ai_tools()->sync($toolIds); + } + + public function syncTags(Assistant $assistant, array $tagIds): array + { + return $assistant->tags()->sync($tagIds); + } + + public function setReleaseStage(Assistant $assistant, ReleaseStage $stage): bool + { + if ($assistant->release_stage === $stage->value) { + return false; + } + + $assistant->release_stage = $stage->value; + $assistant->save(); + + return true; + } + + public function loadRelations(Assistant $assistant, array $relations): Assistant + { + return $assistant->load($relations); + } + + public function filterByIsFavorite(Builder $query, User $user, bool $isFavorite): Builder + { + $method = $isFavorite ? 'whereHas' : 'whereDoesntHave'; + + return $query->$method('favoritedByUsers', fn($q) => $q->where('user_id', $user->id)); + } + + public function setFavorite(Assistant $assistant, User $user, bool $isFavorite): void + { + if ($isFavorite) { + $user->favoriteAssistants()->syncWithoutDetaching([$assistant->id]); + } else { + $user->favoriteAssistants()->detach($assistant->id); + } + } +} diff --git a/app/Services/Assistant/Repositories/FeedbackRepository.php b/app/Services/Assistant/Repositories/FeedbackRepository.php new file mode 100644 index 000000000..1a88d47c3 --- /dev/null +++ b/app/Services/Assistant/Repositories/FeedbackRepository.php @@ -0,0 +1,20 @@ +feedback()->create([ + 'text' => $text, + 'user_id' => $user->id, + ]); + } +} diff --git a/app/Services/Assistant/Repositories/OrganizationRepository.php b/app/Services/Assistant/Repositories/OrganizationRepository.php new file mode 100644 index 000000000..a80e72fe0 --- /dev/null +++ b/app/Services/Assistant/Repositories/OrganizationRepository.php @@ -0,0 +1,27 @@ +organizations()->first(); + } + + public function usersShareOrganization(User $userA, User $userB): bool + { + if ($userA->id === $userB->id) { + return true; + } + + return $userA->organizations() + ->whereHas('users', fn ($q) => $q->where('users.id', $userB->id)) + ->exists(); + } +} diff --git a/app/Services/Assistant/Repositories/ReviewRepository.php b/app/Services/Assistant/Repositories/ReviewRepository.php new file mode 100644 index 000000000..c5462cdf5 --- /dev/null +++ b/app/Services/Assistant/Repositories/ReviewRepository.php @@ -0,0 +1,46 @@ +load('assistant'); + } + + public function updateOrCreateForAssistant(int $assistantId, array $data): Review + { + return Review::updateOrCreate( + ['assistant_id' => $assistantId], + $data, + ); + } + + public function update(Review $review, array $data): Review + { + $review->fill($data); + $review->save(); + + return $review; + } + + public function resetReviewForAssistant(int $assistantId): void + { + $review = Review::where('assistant_id', $assistantId)->first(); + + if ($review === null) { + return; + } + + $review->status = ReviewStatus::PENDING->value; + $review->reason = null; + $review->save(); + } +} diff --git a/app/Services/Assistant/Values/ReleaseStage.php b/app/Services/Assistant/Values/ReleaseStage.php new file mode 100644 index 000000000..82ce67f84 --- /dev/null +++ b/app/Services/Assistant/Values/ReleaseStage.php @@ -0,0 +1,11 @@ + ({ + commands: async (program) => { + program + .command('ci:phpunit') + .description('runs the phpunit test suite for laravel as service') + .allowExcessArguments(true) + .allowUnknownOption(true) + .action(async (_, command) => { + await context.docker.executeComposeCommand( + ['-f', 'docker-compose.ci.yml','run', '--build', '--rm', 'test', ...command.args], + { cwd: context.paths.projectDir, interactive: true } + ); + }); + } +}); diff --git a/bootstrap/app.php b/bootstrap/app.php index f8f4c972d..3c57cfb45 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -7,10 +7,10 @@ return Application::configure(basePath: dirname(__DIR__)) ->withRouting( - web: __DIR__.'/../routes/web.php', - api: __DIR__.'/../routes/api.php', - commands: __DIR__.'/../routes/console.php', - channels: __DIR__.'/../routes/channels.php', + web: __DIR__ . '/../routes/web.php', + api: __DIR__ . '/../routes/api.php', + commands: __DIR__ . '/../routes/console.php', + channels: __DIR__ . '/../routes/channels.php', health: '/up', ) @@ -19,6 +19,12 @@ }) ->withExceptions(function (Exceptions $exceptions) { + $exceptions->dontReport( + \LaravelJsonApi\Core\Exceptions\JsonApiException::class, + ); + $exceptions->render( + \LaravelJsonApi\Exceptions\ExceptionParser::renderer(), + ); $exceptions->shouldRenderJsonWhen(function (Request $request) { return $request->expectsJson() || $request->is('api/*'); }); diff --git a/composer.json b/composer.json index 524b6154a..ed1787ada 100644 --- a/composer.json +++ b/composer.json @@ -12,28 +12,30 @@ ], "license": "MIT", "require": { - "php": "^8.2", + "php": "^8.3", "ext-pdo": "*", "blade-ui-kit/blade-icons": "^1.8.0", "hawk-hhg/hawki-crypto": "^0.5.3", "jumbojett/openid-connect-php": "^1.0.2", - "laravel/framework": "^12.35.1", + "laravel-json-api/laravel": "^5.2", + "laravel/framework": "^13.0", "laravel/reverb": "^1.6.0", "laravel/sanctum": "^4.2.0", - "laravel/tinker": "^2.10.1", + "laravel/tinker": "^3.0", "league/flysystem-aws-s3-v3": "^3.30.1", "league/flysystem-sftp-v3": "^3.30.0", "league/flysystem-webdav": "^3.30.0", "neunerlei/dbg": "^3.3", - "spatie/laravel-backup": "^9.3.4" + "phpunit/phpunit": "^13", + "spatie/laravel-backup": "^10.0" }, "require-dev": { "fakerphp/faker": "^1.24.1", + "laravel-json-api/testing": "^3.2", "laravel/pint": "^1.25.1", "laravel/sail": "^1.46.0", "mockery/mockery": "^1.6.12", "nunomaduro/collision": "^8.8.2", - "phpunit/phpunit": "^11.0.1", "spatie/laravel-ignition": "^2.9.1" }, "autoload": { diff --git a/composer.lock b/composer.lock index 78c00e176..4d0385350 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9d4003e242f7a11d2baf5baa82d48dde", + "content-hash": "05fa2a5ff60fbfe1412a105051f66964", "packages": [ { "name": "0.0.0/composer-include-files", @@ -116,16 +116,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.373.0", + "version": "3.380.2", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "fb74a2dca7ae2363e929c5cea33a4a4db0d22690" + "reference": "af23f62b555be3ab337de571b45ae28558b6daf6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/fb74a2dca7ae2363e929c5cea33a4a4db0d22690", - "reference": "fb74a2dca7ae2363e929c5cea33a4a4db0d22690", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/af23f62b555be3ab337de571b45ae28558b6daf6", + "reference": "af23f62b555be3ab337de571b45ae28558b6daf6", "shasum": "" }, "require": { @@ -146,12 +146,12 @@ "aws/aws-php-sns-message-validator": "~1.0", "behat/behat": "~3.0", "composer/composer": "^2.7.8", - "dms/phpunit-arraysubset-asserts": "^0.4.0", + "dms/phpunit-arraysubset-asserts": "^v0.5.0", "doctrine/cache": "~1.4", "ext-dom": "*", "ext-openssl": "*", "ext-sockets": "*", - "phpunit/phpunit": "^9.6", + "phpunit/phpunit": "^10.0", "psr/cache": "^2.0 || ^3.0", "psr/simple-cache": "^2.0 || ^3.0", "sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0", @@ -207,22 +207,22 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.373.0" + "source": "https://github.com/aws/aws-sdk-php/tree/3.380.2" }, - "time": "2026-03-11T18:33:36+00:00" + "time": "2026-05-06T18:28:56+00:00" }, { "name": "blade-ui-kit/blade-icons", - "version": "1.9.0", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/driesvints/blade-icons.git", - "reference": "caa92fde675d7a651c38bf73ca582ddada56f318" + "reference": "74189a80bbaa4966aebaee54fec3a3c2ef0a5f3a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/driesvints/blade-icons/zipball/caa92fde675d7a651c38bf73ca582ddada56f318", - "reference": "caa92fde675d7a651c38bf73ca582ddada56f318", + "url": "https://api.github.com/repos/driesvints/blade-icons/zipball/74189a80bbaa4966aebaee54fec3a3c2ef0a5f3a", + "reference": "74189a80bbaa4966aebaee54fec3a3c2ef0a5f3a", "shasum": "" }, "require": { @@ -290,7 +290,7 @@ "type": "paypal" } ], - "time": "2026-02-23T10:42:23+00:00" + "time": "2026-04-23T19:03:45+00:00" }, { "name": "brick/math", @@ -1663,203 +1663,40 @@ "time": "2025-11-08T12:59:43+00:00" }, { - "name": "laravel/framework", - "version": "v12.54.1", + "name": "laravel-json-api/core", + "version": "v5.3.0", "source": { "type": "git", - "url": "https://github.com/laravel/framework.git", - "reference": "325497463e7599cd14224c422c6e5dd2fe832868" + "url": "https://github.com/laravel-json-api/core.git", + "reference": "1829a84d4bdc61c6b130e209c41881d0e17b613e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/325497463e7599cd14224c422c6e5dd2fe832868", - "reference": "325497463e7599cd14224c422c6e5dd2fe832868", + "url": "https://api.github.com/repos/laravel-json-api/core/zipball/1829a84d4bdc61c6b130e209c41881d0e17b613e", + "reference": "1829a84d4bdc61c6b130e209c41881d0e17b613e", "shasum": "" }, "require": { - "brick/math": "^0.11|^0.12|^0.13|^0.14", - "composer-runtime-api": "^2.2", - "doctrine/inflector": "^2.0.5", - "dragonmantank/cron-expression": "^3.4", - "egulias/email-validator": "^3.2.1|^4.0", - "ext-ctype": "*", - "ext-filter": "*", - "ext-hash": "*", - "ext-mbstring": "*", - "ext-openssl": "*", - "ext-session": "*", - "ext-tokenizer": "*", - "fruitcake/php-cors": "^1.3", - "guzzlehttp/guzzle": "^7.8.2", - "guzzlehttp/uri-template": "^1.0", - "laravel/prompts": "^0.3.0", - "laravel/serializable-closure": "^1.3|^2.0", - "league/commonmark": "^2.8.1", - "league/flysystem": "^3.25.1", - "league/flysystem-local": "^3.25.1", - "league/uri": "^7.5.1", - "monolog/monolog": "^3.0", - "nesbot/carbon": "^3.8.4", - "nunomaduro/termwind": "^2.0", - "php": "^8.2", - "psr/container": "^1.1.1|^2.0.1", - "psr/log": "^1.0|^2.0|^3.0", - "psr/simple-cache": "^1.0|^2.0|^3.0", - "ramsey/uuid": "^4.7", - "symfony/console": "^7.2.0", - "symfony/error-handler": "^7.2.0", - "symfony/finder": "^7.2.0", - "symfony/http-foundation": "^7.2.0", - "symfony/http-kernel": "^7.2.0", - "symfony/mailer": "^7.2.0", - "symfony/mime": "^7.2.0", - "symfony/polyfill-php83": "^1.33", - "symfony/polyfill-php84": "^1.33", - "symfony/polyfill-php85": "^1.33", - "symfony/process": "^7.2.0", - "symfony/routing": "^7.2.0", - "symfony/uid": "^7.2.0", - "symfony/var-dumper": "^7.2.0", - "tijsverkoyen/css-to-inline-styles": "^2.2.5", - "vlucas/phpdotenv": "^5.6.1", - "voku/portable-ascii": "^2.0.2" - }, - "conflict": { - "tightenco/collect": "<5.5.33" - }, - "provide": { - "psr/container-implementation": "1.1|2.0", - "psr/log-implementation": "1.0|2.0|3.0", - "psr/simple-cache-implementation": "1.0|2.0|3.0" - }, - "replace": { - "illuminate/auth": "self.version", - "illuminate/broadcasting": "self.version", - "illuminate/bus": "self.version", - "illuminate/cache": "self.version", - "illuminate/collections": "self.version", - "illuminate/concurrency": "self.version", - "illuminate/conditionable": "self.version", - "illuminate/config": "self.version", - "illuminate/console": "self.version", - "illuminate/container": "self.version", - "illuminate/contracts": "self.version", - "illuminate/cookie": "self.version", - "illuminate/database": "self.version", - "illuminate/encryption": "self.version", - "illuminate/events": "self.version", - "illuminate/filesystem": "self.version", - "illuminate/hashing": "self.version", - "illuminate/http": "self.version", - "illuminate/json-schema": "self.version", - "illuminate/log": "self.version", - "illuminate/macroable": "self.version", - "illuminate/mail": "self.version", - "illuminate/notifications": "self.version", - "illuminate/pagination": "self.version", - "illuminate/pipeline": "self.version", - "illuminate/process": "self.version", - "illuminate/queue": "self.version", - "illuminate/redis": "self.version", - "illuminate/reflection": "self.version", - "illuminate/routing": "self.version", - "illuminate/session": "self.version", - "illuminate/support": "self.version", - "illuminate/testing": "self.version", - "illuminate/translation": "self.version", - "illuminate/validation": "self.version", - "illuminate/view": "self.version", - "spatie/once": "*" + "ext-json": "*", + "illuminate/auth": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/http": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2" }, "require-dev": { - "ably/ably-php": "^1.0", - "aws/aws-sdk-php": "^3.322.9", - "ext-gmp": "*", - "fakerphp/faker": "^1.24", - "guzzlehttp/promises": "^2.0.3", - "guzzlehttp/psr7": "^2.4", - "laravel/pint": "^1.18", - "league/flysystem-aws-s3-v3": "^3.25.1", - "league/flysystem-ftp": "^3.25.1", - "league/flysystem-path-prefixing": "^3.25.1", - "league/flysystem-read-only": "^3.25.1", - "league/flysystem-sftp-v3": "^3.25.1", - "mockery/mockery": "^1.6.10", - "opis/json-schema": "^2.4.1", - "orchestra/testbench-core": "^10.9.0", - "pda/pheanstalk": "^5.0.6|^7.0.0", - "php-http/discovery": "^1.15", - "phpstan/phpstan": "^2.0", - "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", - "predis/predis": "^2.3|^3.0", - "resend/resend-php": "^0.10.0|^1.0", - "symfony/cache": "^7.2.0", - "symfony/http-client": "^7.2.0", - "symfony/psr-http-message-bridge": "^7.2.0", - "symfony/translation": "^7.2.0" - }, - "suggest": { - "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", - "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", - "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", - "ext-apcu": "Required to use the APC cache driver.", - "ext-fileinfo": "Required to use the Filesystem class.", - "ext-ftp": "Required to use the Flysystem FTP driver.", - "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", - "ext-memcached": "Required to use the memcache cache driver.", - "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", - "ext-pdo": "Required to use all database features.", - "ext-posix": "Required to use all features of the queue worker.", - "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", - "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", - "filp/whoops": "Required for friendly error pages in development (^2.14.3).", - "laravel/tinker": "Required to use the tinker console command (^2.0).", - "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", - "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", - "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", - "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", - "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", - "mockery/mockery": "Required to use mocking (^1.6).", - "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", - "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", - "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", - "predis/predis": "Required to use the predis connector (^2.3|^3.0).", - "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", - "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", - "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", - "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", - "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", - "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", - "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.2).", - "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.2).", - "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)." + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "12.x-dev" + "dev-develop": "5.x-dev" } }, "autoload": { - "files": [ - "src/Illuminate/Collections/functions.php", - "src/Illuminate/Collections/helpers.php", - "src/Illuminate/Events/functions.php", - "src/Illuminate/Filesystem/functions.php", - "src/Illuminate/Foundation/helpers.php", - "src/Illuminate/Log/functions.php", - "src/Illuminate/Reflection/helpers.php", - "src/Illuminate/Support/functions.php", - "src/Illuminate/Support/helpers.php" - ], "psr-4": { - "Illuminate\\": "src/Illuminate/", - "Illuminate\\Support\\": [ - "src/Illuminate/Macroable/", - "src/Illuminate/Collections/", - "src/Illuminate/Conditionable/", - "src/Illuminate/Reflection/" - ] + "LaravelJsonApi\\Core\\": "src/Core", + "LaravelJsonApi\\Contracts\\": "src/Contracts" } }, "notification-url": "https://packagist.org/downloads/", @@ -1868,131 +1705,131 @@ ], "authors": [ { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" + "name": "Cloud Creativity Ltd", + "email": "info@cloudcreativity.co.uk" + }, + { + "name": "Christopher Gammie", + "email": "chris@cloudcreativity.co.uk" } ], - "description": "The Laravel Framework.", - "homepage": "https://laravel.com", + "description": "Contracts and support classes for Laravel JSON:API packages.", + "homepage": "https://github.com/laravel-json-api/core", "keywords": [ - "framework", + "JSON-API", + "jsonapi", + "jsonapi.org", "laravel" ], "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" + "issues": "https://github.com/laravel-json-api/core/issues", + "source": "https://github.com/laravel-json-api/core/tree/v5.3.0" }, - "time": "2026-03-10T20:25:56+00:00" + "time": "2026-03-28T17:53:51+00:00" }, { - "name": "laravel/prompts", - "version": "v0.3.14", + "name": "laravel-json-api/eloquent", + "version": "v4.7.0", "source": { "type": "git", - "url": "https://github.com/laravel/prompts.git", - "reference": "9f0e371244eedfe2ebeaa72c79c54bb5df6e0176" + "url": "https://github.com/laravel-json-api/eloquent.git", + "reference": "ddc31a352d00612dc1972c5ec5a84f4697cde573" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/9f0e371244eedfe2ebeaa72c79c54bb5df6e0176", - "reference": "9f0e371244eedfe2ebeaa72c79c54bb5df6e0176", + "url": "https://api.github.com/repos/laravel-json-api/eloquent/zipball/ddc31a352d00612dc1972c5ec5a84f4697cde573", + "reference": "ddc31a352d00612dc1972c5ec5a84f4697cde573", "shasum": "" }, "require": { - "composer-runtime-api": "^2.2", - "ext-mbstring": "*", - "php": "^8.1", - "symfony/console": "^6.2|^7.0|^8.0" - }, - "conflict": { - "illuminate/console": ">=10.17.0 <10.25.0", - "laravel/framework": ">=10.17.0 <10.25.0" + "ext-json": "*", + "illuminate/database": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "laravel-json-api/core": "^4.3.2|^5.3", + "php": "^8.2" }, "require-dev": { - "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", - "mockery/mockery": "^1.5", - "pestphp/pest": "^2.3|^3.4|^4.0", - "phpstan/phpstan": "^1.12.28", - "phpstan/phpstan-mockery": "^1.1.3" - }, - "suggest": { - "ext-pcntl": "Required for the spinner to be animated." + "orchestra/testbench": "^9.0|^10.0|^11.0", + "phpunit/phpunit": "^10.5|^11.5.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "0.3.x-dev" + "dev-develop": "4.x-dev" } }, "autoload": { - "files": [ - "src/helpers.php" - ], "psr-4": { - "Laravel\\Prompts\\": "src/" + "LaravelJsonApi\\Eloquent\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Add beautiful and user-friendly forms to your command-line applications.", + "authors": [ + { + "name": "Cloud Creativity Ltd", + "email": "info@cloudcreativity.co.uk" + }, + { + "name": "Christopher Gammie", + "email": "chris@cloudcreativity.co.uk" + } + ], + "description": "Serialize Eloquent models as JSON:API resources.", + "homepage": "https://github.com/laravel-json-api/eloquent", + "keywords": [ + "JSON-API", + "jsonapi", + "jsonapi.org", + "laravel" + ], "support": { - "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.14" + "issues": "https://github.com/laravel-json-api/eloquent/issues", + "source": "https://github.com/laravel-json-api/eloquent/tree/v4.7.0" }, - "time": "2026-03-01T09:02:38+00:00" + "time": "2026-03-28T18:12:20+00:00" }, { - "name": "laravel/reverb", - "version": "v1.8.0", + "name": "laravel-json-api/encoder-neomerx", + "version": "v4.3.0", "source": { "type": "git", - "url": "https://github.com/laravel/reverb.git", - "reference": "53753b72035f1b13899fa57d2ad4dfe9480c8d61" + "url": "https://github.com/laravel-json-api/encoder-neomerx.git", + "reference": "1fde071e2f498c4294599728d69b8e11b24020aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/reverb/zipball/53753b72035f1b13899fa57d2ad4dfe9480c8d61", - "reference": "53753b72035f1b13899fa57d2ad4dfe9480c8d61", + "url": "https://api.github.com/repos/laravel-json-api/encoder-neomerx/zipball/1fde071e2f498c4294599728d69b8e11b24020aa", + "reference": "1fde071e2f498c4294599728d69b8e11b24020aa", "shasum": "" }, "require": { - "clue/redis-react": "^2.6", - "guzzlehttp/psr7": "^2.6", - "illuminate/console": "^10.47|^11.0|^12.0|^13.0", - "illuminate/contracts": "^10.47|^11.0|^12.0|^13.0", - "illuminate/http": "^10.47|^11.0|^12.0|^13.0", - "illuminate/support": "^10.47|^11.0|^12.0|^13.0", - "laravel/prompts": "^0.1.15|^0.2.0|^0.3.0", - "php": "^8.2", - "pusher/pusher-php-server": "^7.2", - "ratchet/rfc6455": "^0.4", - "react/promise-timer": "^1.10", - "react/socket": "^1.14", - "symfony/console": "^6.0|^7.0|^8.0", - "symfony/http-foundation": "^6.3|^7.0|^8.0" + "ext-json": "*", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "laravel-json-api/core": "^5.3", + "laravel-json-api/neomerx-json-api": "^5.0.3", + "php": "^8.2" }, "require-dev": { - "orchestra/testbench": "^8.36|^9.15|^10.8|^11.0", - "pestphp/pest": "^2.0|^3.0|^4.0", - "phpstan/phpstan": "^1.10", - "ratchet/pawl": "^0.4.1", - "react/async": "^4.2", - "react/http": "^1.9" + "phpunit/phpunit": "^10.5|^11.0" }, "type": "library", "extra": { "laravel": { "providers": [ - "Laravel\\Reverb\\ApplicationManagerServiceProvider", - "Laravel\\Reverb\\ReverbServiceProvider" + "LaravelJsonApi\\Encoder\\Neomerx\\ServiceProvider" ] + }, + "branch-alias": { + "dev-develop": "4.x-dev" } }, "autoload": { "psr-4": { - "Laravel\\Reverb\\": "src/" + "LaravelJsonApi\\Encoder\\Neomerx\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2001,66 +1838,64 @@ ], "authors": [ { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" + "name": "Cloud Creativity Ltd", + "email": "info@cloudcreativity.co.uk" }, { - "name": "Joe Dixon", - "email": "joe@laravel.com" + "name": "Christopher Gammie", + "email": "chris@cloudcreativity.co.uk" } ], - "description": "Laravel Reverb provides a real-time WebSocket communication backend for Laravel applications.", + "description": "Encode JSON:API resources using the neomerx/json-api package.", + "homepage": "https://github.com/laravel-json-api/encoder-neomerx", "keywords": [ - "WebSockets", - "laravel", - "real-time", - "websocket" + "JSON-API", + "jsonapi", + "jsonapi.org", + "laravel" ], "support": { - "issues": "https://github.com/laravel/reverb/issues", - "source": "https://github.com/laravel/reverb/tree/v1.8.0" + "issues": "https://github.com/laravel-json-api/encoder-neomerx/issues", + "source": "https://github.com/laravel-json-api/encoder-neomerx/tree/v4.3.0" }, - "time": "2026-02-21T14:37:48+00:00" + "time": "2026-03-28T18:14:34+00:00" }, { - "name": "laravel/sanctum", - "version": "v4.3.1", + "name": "laravel-json-api/exceptions", + "version": "v3.3.0", "source": { "type": "git", - "url": "https://github.com/laravel/sanctum.git", - "reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76" + "url": "https://github.com/laravel-json-api/exceptions.git", + "reference": "fa4a10cc4ed11634cfc2885a38c47b5f9f2eb335" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sanctum/zipball/e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76", - "reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76", + "url": "https://api.github.com/repos/laravel-json-api/exceptions/zipball/fa4a10cc4ed11634cfc2885a38c47b5f9f2eb335", + "reference": "fa4a10cc4ed11634cfc2885a38c47b5f9f2eb335", "shasum": "" }, "require": { "ext-json": "*", - "illuminate/console": "^11.0|^12.0|^13.0", "illuminate/contracts": "^11.0|^12.0|^13.0", - "illuminate/database": "^11.0|^12.0|^13.0", - "illuminate/support": "^11.0|^12.0|^13.0", - "php": "^8.2", - "symfony/console": "^7.0|^8.0" + "illuminate/pipeline": "^11.0|^12.0|^13.0", + "laravel-json-api/core": "^5.3", + "laravel-json-api/validation": "^4.4", + "php": "^8.2" }, "require-dev": { - "mockery/mockery": "^1.6", - "orchestra/testbench": "^9.15|^10.8|^11.0", - "phpstan/phpstan": "^1.10" + "laravel-json-api/testing": "^3.2", + "orchestra/testbench": "^9.0|^10.0|^11.0", + "phpunit/phpunit": "^10.5|^11.5.3" }, "type": "library", "extra": { - "laravel": { - "providers": [ - "Laravel\\Sanctum\\SanctumServiceProvider" - ] + "branch-alias": { + "dev-develop": "3.x-dev" } }, "autoload": { "psr-4": { - "Laravel\\Sanctum\\": "src/" + "LaravelJsonApi\\Exceptions\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2069,55 +1904,76 @@ ], "authors": [ { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" + "name": "Cloud Creativity Ltd", + "email": "info@cloudcreativity.co.uk" + }, + { + "name": "Christopher Gammie", + "email": "chris@cloudcreativity.co.uk" } ], - "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "description": "JSON:API exception parsing for Laravel applications.", + "homepage": "https://github.com/laravel-json-api/exceptions", "keywords": [ - "auth", - "laravel", - "sanctum" + "JSON-API", + "jsonapi", + "jsonapi.org", + "laravel" ], "support": { - "issues": "https://github.com/laravel/sanctum/issues", - "source": "https://github.com/laravel/sanctum" + "issues": "https://github.com/laravel-json-api/exceptions/issues", + "source": "https://github.com/laravel-json-api/exceptions/tree/v3.3.0" }, - "time": "2026-02-07T17:19:31+00:00" + "time": "2026-03-28T18:07:59+00:00" }, { - "name": "laravel/serializable-closure", - "version": "v2.0.10", + "name": "laravel-json-api/laravel", + "version": "v5.2.1", "source": { "type": "git", - "url": "https://github.com/laravel/serializable-closure.git", - "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669" + "url": "https://github.com/laravel-json-api/laravel.git", + "reference": "6d51a3a4eb8b842ac2462cc54d5de8a80703a45e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/870fc81d2f879903dfc5b60bf8a0f94a1609e669", - "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "url": "https://api.github.com/repos/laravel-json-api/laravel/zipball/6d51a3a4eb8b842ac2462cc54d5de8a80703a45e", + "reference": "6d51a3a4eb8b842ac2462cc54d5de8a80703a45e", "shasum": "" }, "require": { - "php": "^8.1" - }, + "ext-json": "*", + "laravel-json-api/core": "^5.3", + "laravel-json-api/eloquent": "^4.7", + "laravel-json-api/encoder-neomerx": "^4.3", + "laravel-json-api/exceptions": "^3.3", + "laravel-json-api/spec": "^3.3", + "laravel-json-api/validation": "^4.4", + "laravel/framework": "^11.0|^12.1|^13.0", + "php": "^8.2" + }, "require-dev": { - "illuminate/support": "^10.0|^11.0|^12.0|^13.0", - "nesbot/carbon": "^2.67|^3.0", - "pestphp/pest": "^2.36|^3.0|^4.0", - "phpstan/phpstan": "^2.0", - "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + "laravel-json-api/testing": "^3.2", + "orchestra/testbench": "^9.0|^10.0|^11.0", + "phpunit/phpunit": "^10.5|^11.0" }, "type": "library", "extra": { + "laravel": { + "aliases": { + "JsonApi": "LaravelJsonApi\\Core\\Facades\\JsonApi", + "JsonApiRoute": "LaravelJsonApi\\Laravel\\Facades\\JsonApiRoute" + }, + "providers": [ + "LaravelJsonApi\\Laravel\\ServiceProvider" + ] + }, "branch-alias": { - "dev-master": "2.x-dev" + "dev-develop": "5.x-dev" } }, "autoload": { "psr-4": { - "Laravel\\SerializableClosure\\": "src/" + "LaravelJsonApi\\Laravel\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2126,322 +1982,1022 @@ ], "authors": [ { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" + "name": "Cloud Creativity Ltd", + "email": "info@cloudcreativity.co.uk" }, { - "name": "Nuno Maduro", - "email": "nuno@laravel.com" + "name": "Christopher Gammie", + "email": "chris@cloudcreativity.co.uk" } ], - "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "description": "JSON:API for Laravel applications.", + "homepage": "https://github.com/laravel-json-api/laravel", "keywords": [ - "closure", - "laravel", - "serializable" + "JSON-API", + "jsonapi", + "jsonapi.org", + "laravel" ], "support": { - "issues": "https://github.com/laravel/serializable-closure/issues", - "source": "https://github.com/laravel/serializable-closure" + "issues": "https://github.com/laravel-json-api/laravel/issues", + "source": "https://github.com/laravel-json-api/laravel/tree/v5.2.1" }, - "time": "2026-02-20T19:59:49+00:00" + "time": "2026-04-14T12:29:58+00:00" }, { - "name": "laravel/tinker", - "version": "v2.11.1", + "name": "laravel-json-api/neomerx-json-api", + "version": "v5.0.3", "source": { "type": "git", - "url": "https://github.com/laravel/tinker.git", - "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741" + "url": "https://github.com/laravel-json-api/neomerx-json-api.git", + "reference": "836342be5eb4bcf6c3734c7f8e82c9ceec3be550" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/c9f80cc835649b5c1842898fb043f8cc098dd741", - "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741", + "url": "https://api.github.com/repos/laravel-json-api/neomerx-json-api/zipball/836342be5eb4bcf6c3734c7f8e82c9ceec3be550", + "reference": "836342be5eb4bcf6c3734c7f8e82c9ceec3be550", "shasum": "" }, "require": { - "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", - "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", - "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", - "php": "^7.2.5|^8.0", - "psy/psysh": "^0.11.1|^0.12.0", - "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0|^8.0" + "ext-json": "*", + "php": "^7.4|^8.0" }, "require-dev": { - "mockery/mockery": "~1.3.3|^1.4.2", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0" - }, - "suggest": { - "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)." + "friendsofphp/php-cs-fixer": "^3.17", + "mockery/mockery": "^1.4.4", + "phpmd/phpmd": "^2.11.1", + "phpunit/phpunit": "^9.5.10", + "scrutinizer/ocular": "^1.8", + "squizlabs/php_codesniffer": "^3.6.2" }, "type": "library", "extra": { - "laravel": { - "providers": [ - "Laravel\\Tinker\\TinkerServiceProvider" - ] + "branch-alias": { + "dev-develop": "5.x-dev" } }, "autoload": { + "files": [ + "src/I18n/format.php" + ], "psr-4": { - "Laravel\\Tinker\\": "src/" + "Neomerx\\JsonApi\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "Apache-2.0" ], "authors": [ { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" + "name": "neomerx", + "email": "info@neomerx.com" + }, + { + "name": "Christopher Gammie", + "email": "contact@gammie.co.uk" } ], - "description": "Powerful REPL for the Laravel framework.", + "description": "Framework agnostic JSON API (jsonapi.org) implementation", + "homepage": "https://github.com/neomerx/json-api", "keywords": [ - "REPL", - "Tinker", - "laravel", - "psysh" + "JSON-API", + "api", + "json", + "jsonapi", + "jsonapi.org", + "neomerx" ], "support": { - "issues": "https://github.com/laravel/tinker/issues", - "source": "https://github.com/laravel/tinker/tree/v2.11.1" + "issues": "https://github.com/neomerx/json-api/issues", + "source": "https://github.com/laravel-json-api/neomerx-json-api/tree/v5.0.3" }, - "time": "2026-02-06T14:12:35+00:00" + "time": "2024-11-29T17:49:31+00:00" }, { - "name": "league/commonmark", - "version": "2.8.1", + "name": "laravel-json-api/spec", + "version": "v3.3.0", "source": { "type": "git", - "url": "https://github.com/thephpleague/commonmark.git", - "reference": "84b1ca48347efdbe775426f108622a42735a6579" + "url": "https://github.com/laravel-json-api/spec.git", + "reference": "381bf1f280a97e28187dc96a8c55cd51973a1f32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/84b1ca48347efdbe775426f108622a42735a6579", - "reference": "84b1ca48347efdbe775426f108622a42735a6579", + "url": "https://api.github.com/repos/laravel-json-api/spec/zipball/381bf1f280a97e28187dc96a8c55cd51973a1f32", + "reference": "381bf1f280a97e28187dc96a8c55cd51973a1f32", "shasum": "" }, "require": { - "ext-mbstring": "*", - "league/config": "^1.1.1", - "php": "^7.4 || ^8.0", - "psr/event-dispatcher": "^1.0", - "symfony/deprecation-contracts": "^2.1 || ^3.0", - "symfony/polyfill-php80": "^1.16" - }, - "require-dev": { - "cebe/markdown": "^1.0", - "commonmark/cmark": "0.31.1", - "commonmark/commonmark.js": "0.31.1", - "composer/package-versions-deprecated": "^1.8", - "embed/embed": "^4.4", - "erusev/parsedown": "^1.0", "ext-json": "*", - "github/gfm": "0.29.0", - "michelf/php-markdown": "^1.4 || ^2.0", - "nyholm/psr7": "^1.5", - "phpstan/phpstan": "^1.8.2", - "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", - "scrutinizer/ocular": "^1.8.1", - "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", - "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", - "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", - "unleashedtech/php-coding-standard": "^3.1.1", - "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/pipeline": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "laravel-json-api/core": "^5.3", + "php": "^8.2" }, - "suggest": { - "symfony/yaml": "v2.3+ required if using the Front Matter extension" + "require-dev": { + "orchestra/testbench": "^9.0|^10.0|^11.0", + "phpunit/phpunit": "^10.5|^11.5.3" }, "type": "library", "extra": { + "laravel": { + "providers": [ + "LaravelJsonApi\\Spec\\ServiceProvider" + ] + }, "branch-alias": { - "dev-main": "2.9-dev" + "dev-develop": "3.x-dev" } }, "autoload": { "psr-4": { - "League\\CommonMark\\": "src" + "LaravelJsonApi\\Spec\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Colin O'Dell", - "email": "colinodell@gmail.com", - "homepage": "https://www.colinodell.com", - "role": "Lead Developer" + "name": "Cloud Creativity Ltd", + "email": "info@cloudcreativity.co.uk" + }, + { + "name": "Christopher Gammie", + "email": "chris@cloudcreativity.co.uk" } ], - "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", - "homepage": "https://commonmark.thephpleague.com", + "description": "Validate JSON documents for compliance with the JSON:API specification.", + "homepage": "https://github.com/laravel-json-api/spec", "keywords": [ - "commonmark", - "flavored", - "gfm", - "github", - "github-flavored", - "markdown", - "md", - "parser" + "JSON-API", + "jsonapi", + "jsonapi.org", + "laravel" ], "support": { - "docs": "https://commonmark.thephpleague.com/", - "forum": "https://github.com/thephpleague/commonmark/discussions", - "issues": "https://github.com/thephpleague/commonmark/issues", - "rss": "https://github.com/thephpleague/commonmark/releases.atom", - "source": "https://github.com/thephpleague/commonmark" + "issues": "https://github.com/laravel-json-api/spec/issues", + "source": "https://github.com/laravel-json-api/spec/tree/v3.3.0" }, - "funding": [ - { - "url": "https://www.colinodell.com/sponsor", - "type": "custom" - }, - { - "url": "https://www.paypal.me/colinpodell/10.00", - "type": "custom" - }, - { - "url": "https://github.com/colinodell", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/league/commonmark", - "type": "tidelift" - } - ], - "time": "2026-03-05T21:37:03+00:00" + "time": "2026-03-28T17:57:07+00:00" }, { - "name": "league/config", - "version": "v1.2.0", + "name": "laravel-json-api/validation", + "version": "v4.4.0", "source": { "type": "git", - "url": "https://github.com/thephpleague/config.git", - "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + "url": "https://github.com/laravel-json-api/validation.git", + "reference": "1221348fe9f42e2312bff735da185ed6c9c50072" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", - "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "url": "https://api.github.com/repos/laravel-json-api/validation/zipball/1221348fe9f42e2312bff735da185ed6c9c50072", + "reference": "1221348fe9f42e2312bff735da185ed6c9c50072", "shasum": "" }, "require": { - "dflydev/dot-access-data": "^3.0.1", - "nette/schema": "^1.2", - "php": "^7.4 || ^8.0" + "ext-json": "*", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "laravel-json-api/core": "^5.3", + "php": "^8.2" }, "require-dev": { - "phpstan/phpstan": "^1.8.2", - "phpunit/phpunit": "^9.5.5", - "scrutinizer/ocular": "^1.8.1", - "unleashedtech/php-coding-standard": "^3.1", - "vimeo/psalm": "^4.7.3" + "orchestra/testbench": "^9.0|^10.0|^11.0", + "phpunit/phpunit": "^10.5|^11.0" }, "type": "library", "extra": { + "laravel": { + "providers": [ + "LaravelJsonApi\\Validation\\ServiceProvider" + ] + }, "branch-alias": { - "dev-main": "1.2-dev" + "dev-develop": "4.x-dev" } }, "autoload": { "psr-4": { - "League\\Config\\": "src" + "LaravelJsonApi\\Validation\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Colin O'Dell", - "email": "colinodell@gmail.com", - "homepage": "https://www.colinodell.com", - "role": "Lead Developer" + "name": "Cloud Creativity Ltd", + "email": "info@cloudcreativity.co.uk" + }, + { + "name": "Christopher Gammie", + "email": "chris@cloudcreativity.co.uk" } ], - "description": "Define configuration arrays with strict schemas and access values with dot notation", - "homepage": "https://config.thephpleague.com", + "description": "Laravel validation for JSON:API resources.", + "homepage": "https://github.com/laravel-json-api/validation", "keywords": [ - "array", - "config", - "configuration", - "dot", - "dot-access", - "nested", - "schema" + "JSON-API", + "jsonapi", + "jsonapi.org", + "laravel" ], "support": { - "docs": "https://config.thephpleague.com/", - "issues": "https://github.com/thephpleague/config/issues", - "rss": "https://github.com/thephpleague/config/releases.atom", - "source": "https://github.com/thephpleague/config" + "issues": "https://github.com/laravel-json-api/validation/issues", + "source": "https://github.com/laravel-json-api/validation/tree/v4.4.0" }, - "funding": [ - { - "url": "https://www.colinodell.com/sponsor", - "type": "custom" - }, - { - "url": "https://www.paypal.me/colinpodell/10.00", - "type": "custom" - }, - { - "url": "https://github.com/colinodell", - "type": "github" - } - ], - "time": "2022-12-11T20:36:23+00:00" + "time": "2026-03-28T18:00:12+00:00" }, { - "name": "league/flysystem", - "version": "3.32.0", + "name": "laravel/framework", + "version": "v13.8.0", "source": { "type": "git", - "url": "https://github.com/thephpleague/flysystem.git", - "reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725" + "url": "https://github.com/laravel/framework.git", + "reference": "e7db333a025a1e93ebca7744953069d7719f4bcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/254b1595b16b22dbddaaef9ed6ca9fdac4956725", - "reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725", + "url": "https://api.github.com/repos/laravel/framework/zipball/e7db333a025a1e93ebca7744953069d7719f4bcf", + "reference": "e7db333a025a1e93ebca7744953069d7719f4bcf", "shasum": "" }, "require": { - "league/flysystem-local": "^3.0.0", - "league/mime-type-detection": "^1.0.0", - "php": "^8.0.2" + "brick/math": "^0.14.2 || ^0.15 || ^0.16 || ^0.17", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.3.0", + "laravel/serializable-closure": "^2.0.10", + "league/commonmark": "^2.8.1", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.3", + "psr/container": "^1.1.1 || ^2.0.1", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.4.0 || ^8.0.0", + "symfony/error-handler": "^7.4.0 || ^8.0.0", + "symfony/finder": "^7.4.0 || ^8.0.0", + "symfony/http-foundation": "^7.4.0 || ^8.0.0", + "symfony/http-kernel": "^7.4.0 || ^8.0.0", + "symfony/mailer": "^7.4.0 || ^8.0.0", + "symfony/mime": "^7.4.0 || ^8.0.0", + "symfony/polyfill-php84": "^1.36", + "symfony/polyfill-php85": "^1.36", + "symfony/polyfill-php86": "^1.36", + "symfony/process": "^7.4.5 || ^8.0.5", + "symfony/routing": "^7.4.0 || ^8.0.0", + "symfony/uid": "^7.4.0 || ^8.0.0", + "symfony/var-dumper": "^7.4.0 || ^8.0.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" }, "conflict": { - "async-aws/core": "<1.19.0", - "async-aws/s3": "<1.14.0", - "aws/aws-sdk-php": "3.209.31 || 3.210.0", - "guzzlehttp/guzzle": "<7.0", - "guzzlehttp/ringphp": "<1.1.1", - "phpseclib/phpseclib": "3.0.15", - "symfony/http-client": "<5.2" + "tightenco/collect": "<5.5.33" }, - "require-dev": { - "async-aws/s3": "^1.5 || ^2.0", - "async-aws/simple-s3": "^1.1 || ^2.0", - "aws/aws-sdk-php": "^3.295.10", - "composer/semver": "^3.0", - "ext-fileinfo": "*", - "ext-ftp": "*", - "ext-mongodb": "^1.3|^2", - "ext-zip": "*", - "friendsofphp/php-cs-fixer": "^3.5", - "google/cloud-storage": "^1.23", - "guzzlehttp/psr7": "^2.6", - "microsoft/azure-storage-blob": "^1.1", + "provide": { + "psr/container-implementation": "1.1 || 2.0", + "psr/log-implementation": "1.0 || 2.0 || 3.0", + "psr/simple-cache-implementation": "1.0 || 2.0 || 3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/json-schema": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "spatie/once": "*" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.322.9", + "ext-gmp": "*", + "fakerphp/faker": "^1.24", + "guzzlehttp/psr7": "^2.4", + "laravel/pint": "^1.18", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^11.0.0", + "pda/pheanstalk": "^7.0.0 || ^8.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^11.5.50 || ^12.5.8 || ^13.0.3", + "predis/predis": "^2.3 || ^3.0", + "rector/rector": "^2.3", + "resend/resend-php": "^1.0", + "symfony/cache": "^7.4.0 || ^8.0.0", + "symfony/http-client": "^7.4.0 || ^8.0.0", + "symfony/psr-http-message-bridge": "^7.4.0 || ^8.0.0", + "symfony/translation": "^7.4.0 || ^8.0.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0 || ^8.0).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0 || ^5.0 || ^6.0).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^7.0 || ^8.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^11.5.50 || ^12.5.8 || ^13.0.3).", + "predis/predis": "Required to use the predis connector (^2.3 || ^3.0).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0 || ^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0 || ^1.0).", + "spatie/fork": "Required to use the 'fork' concurrency driver (^1.2).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.4 || ^8.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.4 || ^8.0).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.4 || ^8.0).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.4 || ^8.0).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.4 || ^8.0).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.4 || ^8.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "13.0.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/functions.php", + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", + "src/Illuminate/Support/functions.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-05-05T21:01:14+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.17", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "6a82ac19a28b916ae0885828795dbd4c59d9a818" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/6a82ac19a28b916ae0885828795dbd4c59d9a818", + "reference": "6a82ac19a28b916ae0885828795dbd4c59d9a818", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0|^8.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.17" + }, + "time": "2026-04-20T16:07:33+00:00" + }, + { + "name": "laravel/reverb", + "version": "v1.10.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/reverb.git", + "reference": "a96310ae8b844d4862b2188a3cd6e79434893a6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/reverb/zipball/a96310ae8b844d4862b2188a3cd6e79434893a6b", + "reference": "a96310ae8b844d4862b2188a3cd6e79434893a6b", + "shasum": "" + }, + "require": { + "clue/redis-react": "^2.6", + "guzzlehttp/psr7": "^2.6", + "illuminate/console": "^10.47|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.47|^11.0|^12.0|^13.0", + "illuminate/http": "^10.47|^11.0|^12.0|^13.0", + "illuminate/support": "^10.47|^11.0|^12.0|^13.0", + "laravel/prompts": "^0.1.15|^0.2.0|^0.3.0", + "php": "^8.2", + "pusher/pusher-php-server": "^7.2", + "ratchet/rfc6455": "^0.4", + "react/promise-timer": "^1.10", + "react/socket": "^1.14", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/http-foundation": "^6.3|^7.0|^8.0" + }, + "require-dev": { + "orchestra/testbench": "^8.36|^9.15|^10.8|^11.0", + "pestphp/pest": "^2.0|^3.0|^4.0", + "phpstan/phpstan": "^1.10", + "ratchet/pawl": "^0.4.1", + "react/async": "^4.2", + "react/http": "^1.9" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Reverb\\ApplicationManagerServiceProvider", + "Laravel\\Reverb\\ReverbServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Reverb\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Joe Dixon", + "email": "joe@laravel.com" + } + ], + "description": "Laravel Reverb provides a real-time WebSocket communication backend for Laravel applications.", + "keywords": [ + "WebSockets", + "laravel", + "real-time", + "websocket" + ], + "support": { + "issues": "https://github.com/laravel/reverb/issues", + "source": "https://github.com/laravel/reverb/tree/v1.10.1" + }, + "time": "2026-04-30T12:07:26+00:00" + }, + { + "name": "laravel/sanctum", + "version": "v4.3.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "2a9bccc18e9907808e0018dd15fa643937886b1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/2a9bccc18e9907808e0018dd15fa643937886b1e", + "reference": "2a9bccc18e9907808e0018dd15fa643937886b1e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/database": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "symfony/console": "^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2026-04-30T11:46:25+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.13", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-04-16T14:03:50+00:00" + }, + { + "name": "laravel/tinker", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "4faba77764bd33411735936acdf30446d058c78b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/4faba77764bd33411735936acdf30446d058c78b", + "reference": "4faba77764bd33411735936acdf30446d058c78b", + "shasum": "" + }, + "require": { + "illuminate/console": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "php": "^8.1", + "psy/psysh": "^0.12.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5|^11.5" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^8.0|^9.0|^10.0|^11.0|^12.0|^13.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v3.0.2" + }, + "time": "2026-03-17T14:54:13+00:00" + }, + { + "name": "league/commonmark", + "version": "2.8.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.9-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2026-03-19T13:16:38+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.33.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "570b8871e0ce693764434b29154c54b434905350" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/570b8871e0ce693764434b29154c54b434905350", + "reference": "570b8871e0ce693764434b29154c54b434905350", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", "mongodb/mongodb": "^1.2|^2", "phpseclib/phpseclib": "^3.0.36", "phpstan/phpstan": "^1.10", @@ -2480,9 +3036,9 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.32.0" + "source": "https://github.com/thephpleague/flysystem/tree/3.33.0" }, - "time": "2026-02-25T17:01:41+00:00" + "time": "2026-03-25T07:59:30+00:00" }, { "name": "league/flysystem-aws-s3-v3", @@ -2590,16 +3146,16 @@ }, { "name": "league/flysystem-sftp-v3", - "version": "3.31.0", + "version": "3.33.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-sftp-v3.git", - "reference": "f01dd8d66e98b20608846963cc790c2b698e8b03" + "reference": "34ff5ef0f841add92e2b902c1005f72135b03646" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-sftp-v3/zipball/f01dd8d66e98b20608846963cc790c2b698e8b03", - "reference": "f01dd8d66e98b20608846963cc790c2b698e8b03", + "url": "https://api.github.com/repos/thephpleague/flysystem-sftp-v3/zipball/34ff5ef0f841add92e2b902c1005f72135b03646", + "reference": "34ff5ef0f841add92e2b902c1005f72135b03646", "shasum": "" }, "require": { @@ -2633,9 +3189,9 @@ "sftp" ], "support": { - "source": "https://github.com/thephpleague/flysystem-sftp-v3/tree/3.31.0" + "source": "https://github.com/thephpleague/flysystem-sftp-v3/tree/3.33.0" }, - "time": "2026-01-23T15:30:45+00:00" + "time": "2026-03-20T13:22:31+00:00" }, { "name": "league/flysystem-webdav", @@ -2743,20 +3299,20 @@ }, { "name": "league/uri", - "version": "7.8.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "4436c6ec8d458e4244448b069cc572d088230b76" + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/4436c6ec8d458e4244448b069cc572d088230b76", - "reference": "4436c6ec8d458e4244448b069cc572d088230b76", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.8", + "league/uri-interfaces": "^7.8.1", "php": "^8.1", "psr/http-factory": "^1" }, @@ -2829,7 +3385,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.8.0" + "source": "https://github.com/thephpleague/uri/tree/7.8.1" }, "funding": [ { @@ -2837,20 +3393,20 @@ "type": "github" } ], - "time": "2026-01-14T17:24:56+00:00" + "time": "2026-03-15T20:22:25+00:00" }, { "name": "league/uri-interfaces", - "version": "7.8.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4" + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/c5c5cd056110fc8afaba29fa6b72a43ced42acd4", - "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", "shasum": "" }, "require": { @@ -2913,7 +3469,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" }, "funding": [ { @@ -2921,7 +3477,7 @@ "type": "github" } ], - "time": "2026-01-15T06:54:53+00:00" + "time": "2026-03-08T20:05:35+00:00" }, { "name": "monolog/monolog", @@ -3092,18 +3648,78 @@ }, "time": "2024-09-04T18:46:31+00:00" }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, { "name": "nesbot/carbon", - "version": "3.11.3", + "version": "3.11.4", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "6a7e652845bb018c668220c2a545aded8594fbbf" + "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/6a7e652845bb018c668220c2a545aded8594fbbf", - "reference": "6a7e652845bb018c668220c2a545aded8594fbbf", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/e890471a3494740f7d9326d72ce6a8c559ffee60", + "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60", "shasum": "" }, "require": { @@ -3195,7 +3811,7 @@ "type": "tidelift" } ], - "time": "2026-03-11T17:23:39+00:00" + "time": "2026-04-07T09:57:54+00:00" }, { "name": "nette/schema", @@ -3766,7 +4382,125 @@ "issues": "https://github.com/paragonie/sodium_compat/issues", "source": "https://github.com/paragonie/sodium_compat/tree/v2.5.0" }, - "time": "2025-12-30T16:12:18+00:00" + "time": "2025-12-30T16:12:18+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" }, { "name": "php-console/php-console", @@ -3900,16 +4634,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.49", + "version": "3.0.52", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9" + "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/6233a1e12584754e6b5daa69fe1289b47775c1b9", - "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2adaefc83df2ec548558307690f376dd7d4f4fce", + "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce", "shasum": "" }, "require": { @@ -3990,7 +4724,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.49" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.52" }, "funding": [ { @@ -4006,292 +4740,503 @@ "type": "tidelift" } ], - "time": "2026-01-27T09:17:28+00:00" + "time": "2026-04-27T07:02:15+00:00" }, { - "name": "psr/clock", - "version": "1.0.0", + "name": "phpunit/php-code-coverage", + "version": "14.1.8", "source": { "type": "git", - "url": "https://github.com/php-fig/clock.git", - "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "031856c28c060e1c1d1fc94d256e3ffbe4230c91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", - "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/031856c28c060e1c1d1fc94d256e3ffbe4230c91", + "reference": "031856c28c060e1c1d1fc94d256e3ffbe4230c91", "shasum": "" }, "require": { - "php": "^7.0 || ^8.0" + "ext-dom": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.4", + "phpunit/php-text-template": "^6.0", + "sebastian/complexity": "^6.0", + "sebastian/environment": "^9.2", + "sebastian/git-state": "^1.0", + "sebastian/lines-of-code": "^5.0", + "sebastian/version": "^7.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^13.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "type": "library", - "autoload": { - "psr-4": { - "Psr\\Clock\\": "src/" + "extra": { + "branch-alias": { + "dev-main": "14.1.x-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Common interface for reading the clock.", - "homepage": "https://github.com/php-fig/clock", + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", "keywords": [ - "clock", - "now", - "psr", - "psr-20", - "time" + "coverage", + "testing", + "xunit" ], "support": { - "issues": "https://github.com/php-fig/clock/issues", - "source": "https://github.com/php-fig/clock/tree/1.0.0" + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/14.1.8" }, - "time": "2022-11-25T14:36:26+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2026-05-09T12:06:52+00:00" }, { - "name": "psr/container", - "version": "2.0.2", + "name": "phpunit/php-file-iterator", + "version": "7.0.0", "source": { "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "6e5aa1fb0a95b1703d83e721299ee18bb4e2de50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6e5aa1fb0a95b1703d83e721299ee18bb4e2de50", + "reference": "6e5aa1fb0a95b1703d83e721299ee18bb4e2de50", "shasum": "" }, "require": { - "php": ">=7.4.0" + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-main": "7.0-dev" } }, "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" + "filesystem", + "iterator" ], "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/7.0.0" }, - "time": "2021-11-05T16:47:00+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:33:26+00:00" }, { - "name": "psr/event-dispatcher", - "version": "1.0.0", + "name": "phpunit/php-invoker", + "version": "7.0.0", "source": { "type": "git", - "url": "https://github.com/php-fig/event-dispatcher.git", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", "shasum": "" }, "require": { - "php": ">=7.2.0" + "php": ">=8.4" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^13.0" + }, + "suggest": { + "ext-pcntl": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-main": "7.0-dev" } }, "autoload": { - "psr-4": { - "Psr\\EventDispatcher\\": "src/" + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-invoker", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:34:47+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/a47af19f93f76aa3368303d752aa5272ca3299f4", + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Standard interfaces for event handling.", + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", "keywords": [ - "events", - "psr", - "psr-14" + "template" ], "support": { - "issues": "https://github.com/php-fig/event-dispatcher/issues", - "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/6.0.0" }, - "time": "2019-01-08T18:20:26+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-text-template", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:36:37+00:00" }, { - "name": "psr/http-client", - "version": "1.0.3", + "name": "phpunit/php-timer", + "version": "9.0.0", "source": { "type": "git", - "url": "https://github.com/php-fig/http-client.git", - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", - "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/a0e12065831f6ab0d83120dc61513eb8d9a966f6", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6", "shasum": "" }, "require": { - "php": "^7.0 || ^8.0", - "psr/http-message": "^1.0 || ^2.0" + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-main": "9.0-dev" } }, "autoload": { - "psr-4": { - "Psr\\Http\\Client\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Common interface for HTTP clients", - "homepage": "https://github.com/php-fig/http-client", + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", "keywords": [ - "http", - "http-client", - "psr", - "psr-18" + "timer" ], "support": { - "source": "https://github.com/php-fig/http-client" + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/9.0.0" }, - "time": "2023-09-23T14:17:50+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-timer", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:37:53+00:00" }, { - "name": "psr/http-factory", - "version": "1.1.0", + "name": "phpunit/phpunit", + "version": "13.1.9", "source": { "type": "git", - "url": "https://github.com/php-fig/http-factory.git", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "506033fd7d6855fea1e2973767d65844412b4574" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", - "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/506033fd7d6855fea1e2973767d65844412b4574", + "reference": "506033fd7d6855fea1e2973767d65844412b4574", "shasum": "" }, "require": { - "php": ">=7.1", - "psr/http-message": "^1.0 || ^2.0" + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.4.1", + "phpunit/php-code-coverage": "^14.1.8", + "phpunit/php-file-iterator": "^7.0.0", + "phpunit/php-invoker": "^7.0.0", + "phpunit/php-text-template": "^6.0.0", + "phpunit/php-timer": "^9.0.0", + "sebastian/cli-parser": "^5.0.0", + "sebastian/comparator": "^8.1.2", + "sebastian/diff": "^8.1.0", + "sebastian/environment": "^9.3.0", + "sebastian/exporter": "^8.0.2", + "sebastian/git-state": "^1.0", + "sebastian/global-state": "^9.0.0", + "sebastian/object-enumerator": "^8.0.0", + "sebastian/recursion-context": "^8.0.0", + "sebastian/type": "^7.0.0", + "sebastian/version": "^7.0.0", + "staabm/side-effects-detector": "^1.0.5" }, + "bin": [ + "phpunit" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-main": "13.1-dev" } }, "autoload": { - "psr-4": { - "Psr\\Http\\Message\\": "src/" - } + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", "keywords": [ - "factory", - "http", - "message", - "psr", - "psr-17", - "psr-7", - "request", - "response" + "phpunit", + "testing", + "xunit" ], "support": { - "source": "https://github.com/php-fig/http-factory" + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/13.1.9" }, - "time": "2024-04-15T12:06:14+00:00" + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-05-13T04:00:53+00:00" }, { - "name": "psr/http-message", - "version": "2.0", + "name": "psr/clock", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/php-fig/http-message.git", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.0 || ^8.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, "autoload": { "psr-4": { - "Psr\\Http\\Message\\": "src/" + "Psr\\Clock\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4304,47 +5249,47 @@ "homepage": "https://www.php-fig.org/" } ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", "keywords": [ - "http", - "http-message", + "clock", + "now", "psr", - "psr-7", - "request", - "response" + "psr-20", + "time" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/2.0" + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" }, - "time": "2023-04-04T09:54:51+00:00" + "time": "2022-11-25T14:36:26+00:00" }, { - "name": "psr/log", - "version": "3.0.2", + "name": "psr/container", + "version": "2.0.2", "source": { "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", - "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "shasum": "" }, "require": { - "php": ">=8.0.0" + "php": ">=7.4.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Log\\": "src" + "Psr\\Container\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4357,44 +5302,47 @@ "homepage": "https://www.php-fig.org/" } ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", "keywords": [ - "log", - "psr", - "psr-3" + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.2" + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" }, - "time": "2024-09-11T13:17:53+00:00" + "time": "2021-11-05T16:47:00+00:00" }, { - "name": "psr/simple-cache", - "version": "3.0.0", + "name": "psr/event-dispatcher", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/php-fig/simple-cache.git", - "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", - "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", "shasum": "" }, "require": { - "php": ">=8.0.0" + "php": ">=7.2.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { "psr-4": { - "Psr\\SimpleCache\\": "src/" + "Psr\\EventDispatcher\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4404,75 +5352,48 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "homepage": "http://www.php-fig.org/" } ], - "description": "Common interfaces for simple caching", + "description": "Standard interfaces for event handling.", "keywords": [ - "cache", - "caching", + "events", "psr", - "psr-16", - "simple-cache" + "psr-14" ], "support": { - "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" }, - "time": "2021-10-29T13:26:27+00:00" + "time": "2019-01-08T18:20:26+00:00" }, { - "name": "psy/psysh", - "version": "v0.12.21", + "name": "psr/http-client", + "version": "1.0.3", "source": { "type": "git", - "url": "https://github.com/bobthecow/psysh.git", - "reference": "4821fab5b7cd8c49a673a9fd5754dc9162bb9e97" + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4821fab5b7cd8c49a673a9fd5754dc9162bb9e97", - "reference": "4821fab5b7cd8c49a673a9fd5754dc9162bb9e97", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", "shasum": "" }, - "require": { - "ext-json": "*", - "ext-tokenizer": "*", - "nikic/php-parser": "^5.0 || ^4.0", - "php": "^8.0 || ^7.4", - "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", - "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" - }, - "conflict": { - "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.2", - "composer/class-map-generator": "^1.6" - }, - "suggest": { - "composer/class-map-generator": "Improved tab completion performance with better class discovery.", - "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", - "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" }, - "bin": [ - "bin/psysh" - ], "type": "library", "extra": { - "bamarni-bin": { - "bin-links": false, - "forward-command": false - }, "branch-alias": { - "dev-main": "0.12.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { - "files": [ - "src/functions.php" - ], "psr-4": { - "Psy\\": "src/" + "Psr\\Http\\Client\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4481,111 +5402,105 @@ ], "authors": [ { - "name": "Justin Hileman", - "email": "justin@justinhileman.info" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "An interactive shell for modern PHP.", - "homepage": "https://psysh.org", + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", "keywords": [ - "REPL", - "console", - "interactive", - "shell" + "http", + "http-client", + "psr", + "psr-18" ], "support": { - "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.21" + "source": "https://github.com/php-fig/http-client" }, - "time": "2026-03-06T21:21:28+00:00" + "time": "2023-09-23T14:17:50+00:00" }, { - "name": "pusher/pusher-php-server", - "version": "7.2.7", + "name": "psr/http-factory", + "version": "1.1.0", "source": { "type": "git", - "url": "https://github.com/pusher/pusher-http-php.git", - "reference": "148b0b5100d000ed57195acdf548a2b1b38ee3f7" + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/148b0b5100d000ed57195acdf548a2b1b38ee3f7", - "reference": "148b0b5100d000ed57195acdf548a2b1b38ee3f7", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "ext-curl": "*", - "ext-json": "*", - "guzzlehttp/guzzle": "^7.2", - "paragonie/sodium_compat": "^1.6|^2.0", - "php": "^7.3|^8.0", - "psr/log": "^1.0|^2.0|^3.0" - }, - "require-dev": { - "overtrue/phplint": "^2.3", - "phpunit/phpunit": "^9.3" + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { "psr-4": { - "Pusher\\": "src/" + "Psr\\Http\\Message\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Library for interacting with the Pusher REST API", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ - "events", - "messaging", - "php-pusher-server", - "publish", - "push", - "pusher", - "real time", - "real-time", - "realtime", - "rest", - "trigger" + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" ], "support": { - "issues": "https://github.com/pusher/pusher-http-php/issues", - "source": "https://github.com/pusher/pusher-http-php/tree/7.2.7" + "source": "https://github.com/php-fig/http-factory" }, - "time": "2025-01-06T10:56:20+00:00" + "time": "2024-04-15T12:06:14+00:00" }, { - "name": "ralouphie/getallheaders", - "version": "3.0.3", + "name": "psr/http-message", + "version": "2.0", "source": { "type": "git", - "url": "https://github.com/ralouphie/getallheaders.git", - "reference": "120b605dfeb996808c31b6477290a714d356e822" + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", - "reference": "120b605dfeb996808c31b6477290a714d356e822", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", "shasum": "" }, "require": { - "php": ">=5.6" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.1", - "phpunit/phpunit": "^5 || ^6.5" + "php": "^7.2 || ^8.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { - "files": [ - "src/getallheaders.php" - ] + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4593,65 +5508,51 @@ ], "authors": [ { - "name": "Ralph Khattar", - "email": "ralph.khattar@gmail.com" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "A polyfill for getallheaders.", + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], "support": { - "issues": "https://github.com/ralouphie/getallheaders/issues", - "source": "https://github.com/ralouphie/getallheaders/tree/develop" + "source": "https://github.com/php-fig/http-message/tree/2.0" }, - "time": "2019-03-08T08:55:37+00:00" + "time": "2023-04-04T09:54:51+00:00" }, { - "name": "ramsey/collection", - "version": "2.1.1", + "name": "psr/log", + "version": "3.0.2", "source": { "type": "git", - "url": "https://github.com/ramsey/collection.git", - "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", - "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { - "php": "^8.1" - }, - "require-dev": { - "captainhook/plugin-composer": "^5.3", - "ergebnis/composer-normalize": "^2.45", - "fakerphp/faker": "^1.24", - "hamcrest/hamcrest-php": "^2.0", - "jangregor/phpstan-prophecy": "^2.1", - "mockery/mockery": "^1.6", - "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.4", - "phpspec/prophecy-phpunit": "^2.3", - "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^2.1", - "phpstan/phpstan-mockery": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^10.5", - "ramsey/coding-standard": "^2.3", - "ramsey/conventional-commits": "^1.6", - "roave/security-advisories": "dev-latest" + "php": ">=8.0.0" }, "type": "library", "extra": { - "captainhook": { - "force-install": true - }, - "ramsey/conventional-commits": { - "configFile": "conventional-commits.json" + "branch-alias": { + "dev-master": "3.x-dev" } }, "autoload": { "psr-4": { - "Ramsey\\Collection\\": "src/" + "Psr\\Log\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -4660,132 +5561,126 @@ ], "authors": [ { - "name": "Ben Ramsey", - "email": "ben@benramsey.com", - "homepage": "https://benramsey.com" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "A PHP library for representing and manipulating collections.", + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", "keywords": [ - "array", - "collection", - "hash", - "map", - "queue", - "set" + "log", + "psr", + "psr-3" ], "support": { - "issues": "https://github.com/ramsey/collection/issues", - "source": "https://github.com/ramsey/collection/tree/2.1.1" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2025-03-22T05:38:12+00:00" + "time": "2024-09-11T13:17:53+00:00" }, { - "name": "ramsey/uuid", - "version": "4.9.2", + "name": "psr/simple-cache", + "version": "3.0.0", "source": { "type": "git", - "url": "https://github.com/ramsey/uuid.git", - "reference": "8429c78ca35a09f27565311b98101e2826affde0" + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", - "reference": "8429c78ca35a09f27565311b98101e2826affde0", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", "shasum": "" }, "require": { - "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", - "php": "^8.0", - "ramsey/collection": "^1.2 || ^2.0" - }, - "replace": { - "rhumsaa/uuid": "self.version" - }, - "require-dev": { - "captainhook/captainhook": "^5.25", - "captainhook/plugin-composer": "^5.3", - "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "ergebnis/composer-normalize": "^2.47", - "mockery/mockery": "^1.6", - "paragonie/random-lib": "^2", - "php-mock/php-mock": "^2.6", - "php-mock/php-mock-mockery": "^1.5", - "php-parallel-lint/php-parallel-lint": "^1.4.0", - "phpbench/phpbench": "^1.2.14", - "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^2.1", - "phpstan/phpstan-mockery": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^9.6", - "slevomat/coding-standard": "^8.18", - "squizlabs/php_codesniffer": "^3.13" - }, - "suggest": { - "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", - "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", - "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", - "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", - "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + "php": ">=8.0.0" }, "type": "library", "extra": { - "captainhook": { - "force-install": true + "branch-alias": { + "dev-master": "3.0.x-dev" } }, "autoload": { - "files": [ - "src/functions.php" - ], "psr-4": { - "Ramsey\\Uuid\\": "src/" + "Psr\\SimpleCache\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", "keywords": [ - "guid", - "identifier", - "uuid" + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" ], "support": { - "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.9.2" + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" }, - "time": "2025-12-14T04:43:48+00:00" + "time": "2021-10-29T13:26:27+00:00" }, { - "name": "ratchet/rfc6455", - "version": "v0.4.0", + "name": "psy/psysh", + "version": "v0.12.22", "source": { "type": "git", - "url": "https://github.com/ratchetphp/RFC6455.git", - "reference": "859d95f85dda0912c6d5b936d036d044e3af47ef" + "url": "https://github.com/bobthecow/psysh.git", + "reference": "3be75d5b9244936dd4ac62ade2bfb004d13acf0f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/859d95f85dda0912c6d5b936d036d044e3af47ef", - "reference": "859d95f85dda0912c6d5b936d036d044e3af47ef", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/3be75d5b9244936dd4ac62ade2bfb004d13acf0f", + "reference": "3be75d5b9244936dd4ac62ade2bfb004d13acf0f", "shasum": "" }, "require": { - "php": ">=7.4", - "psr/http-factory-implementation": "^1.0", - "symfony/polyfill-php80": "^1.15" + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" }, "require-dev": { - "guzzlehttp/psr7": "^2.7", - "phpunit/phpunit": "^9.5", - "react/socket": "^1.3" + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" + }, + "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." }, + "bin": [ + "bin/psysh" + ], "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { - "Ratchet\\RFC6455\\": "src" + "Psy\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -4794,131 +5689,111 @@ ], "authors": [ { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "role": "Developer" - }, - { - "name": "Matt Bonneau", - "role": "Developer" + "name": "Justin Hileman", + "email": "justin@justinhileman.info" } ], - "description": "RFC6455 WebSocket protocol handler", - "homepage": "http://socketo.me", + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", "keywords": [ - "WebSockets", - "rfc6455", - "websocket" + "REPL", + "console", + "interactive", + "shell" ], "support": { - "chat": "https://gitter.im/reactphp/reactphp", - "issues": "https://github.com/ratchetphp/RFC6455/issues", - "source": "https://github.com/ratchetphp/RFC6455/tree/v0.4.0" + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.22" }, - "time": "2025-02-24T01:18:22+00:00" + "time": "2026-03-22T23:03:24+00:00" }, { - "name": "react/cache", - "version": "v1.2.0", + "name": "pusher/pusher-php-server", + "version": "7.2.7", "source": { "type": "git", - "url": "https://github.com/reactphp/cache.git", - "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + "url": "https://github.com/pusher/pusher-http-php.git", + "reference": "148b0b5100d000ed57195acdf548a2b1b38ee3f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", - "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "url": "https://api.github.com/repos/pusher/pusher-http-php/zipball/148b0b5100d000ed57195acdf548a2b1b38ee3f7", + "reference": "148b0b5100d000ed57195acdf548a2b1b38ee3f7", "shasum": "" }, "require": { - "php": ">=5.3.0", - "react/promise": "^3.0 || ^2.0 || ^1.1" + "ext-curl": "*", + "ext-json": "*", + "guzzlehttp/guzzle": "^7.2", + "paragonie/sodium_compat": "^1.6|^2.0", + "php": "^7.3|^8.0", + "psr/log": "^1.0|^2.0|^3.0" }, "require-dev": { - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + "overtrue/phplint": "^2.3", + "phpunit/phpunit": "^9.3" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, "autoload": { "psr-4": { - "React\\Cache\\": "src/" + "Pusher\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Async, Promise-based cache interface for ReactPHP", + "description": "Library for interacting with the Pusher REST API", "keywords": [ - "cache", - "caching", - "promise", - "reactphp" + "events", + "messaging", + "php-pusher-server", + "publish", + "push", + "pusher", + "real time", + "real-time", + "realtime", + "rest", + "trigger" ], "support": { - "issues": "https://github.com/reactphp/cache/issues", - "source": "https://github.com/reactphp/cache/tree/v1.2.0" + "issues": "https://github.com/pusher/pusher-http-php/issues", + "source": "https://github.com/pusher/pusher-http-php/tree/7.2.7" }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2022-11-30T15:59:55+00:00" + "time": "2025-01-06T10:56:20+00:00" }, { - "name": "react/dns", - "version": "v1.14.0", + "name": "ralouphie/getallheaders", + "version": "3.0.3", "source": { "type": "git", - "url": "https://github.com/reactphp/dns.git", - "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3" + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3", - "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", "shasum": "" }, "require": { - "php": ">=5.3.0", - "react/cache": "^1.0 || ^0.6 || ^0.5", - "react/event-loop": "^1.2", - "react/promise": "^3.2 || ^2.7 || ^1.2.1" + "php": ">=5.6" }, "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", - "react/async": "^4.3 || ^3 || ^2", - "react/promise-timer": "^1.11" + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" }, "type": "library", "autoload": { - "psr-4": { - "React\\Dns\\": "src/" - } + "files": [ + "src/getallheaders.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4926,72 +5801,65 @@ ], "authors": [ { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" } ], - "description": "Async DNS resolver for ReactPHP", - "keywords": [ - "async", - "dns", - "dns-resolver", - "reactphp" - ], + "description": "A polyfill for getallheaders.", "support": { - "issues": "https://github.com/reactphp/dns/issues", - "source": "https://github.com/reactphp/dns/tree/v1.14.0" + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2025-11-18T19:34:28+00:00" + "time": "2019-03-08T08:55:37+00:00" }, { - "name": "react/event-loop", - "version": "v1.6.0", + "name": "ramsey/collection", + "version": "2.1.1", "source": { - "type": "git", - "url": "https://github.com/reactphp/event-loop.git", - "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a" + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a", - "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^8.1" }, "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" - }, - "suggest": { - "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" }, "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, "autoload": { "psr-4": { - "React\\EventLoop\\": "src/" + "Ramsey\\Collection\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -5000,145 +5868,132 @@ ], "authors": [ { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" } ], - "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "description": "A PHP library for representing and manipulating collections.", "keywords": [ - "asynchronous", - "event-loop" + "array", + "collection", + "hash", + "map", + "queue", + "set" ], "support": { - "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2025-11-17T20:46:25+00:00" + "time": "2025-03-22T05:38:12+00:00" }, { - "name": "react/promise", - "version": "v3.3.0", + "name": "ramsey/uuid", + "version": "4.9.2", "source": { "type": "git", - "url": "https://github.com/reactphp/promise.git", - "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" + "url": "https://github.com/ramsey/uuid.git", + "reference": "8429c78ca35a09f27565311b98101e2826affde0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", - "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", "shasum": "" }, "require": { - "php": ">=7.1.0" + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" }, "require-dev": { - "phpstan/phpstan": "1.12.28 || 1.4.10", - "phpunit/phpunit": "^9.6 || ^7.5" + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." }, "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, "autoload": { "files": [ - "src/functions_include.php" + "src/functions.php" ], "psr-4": { - "React\\Promise\\": "src/" + "Ramsey\\Uuid\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", "keywords": [ - "promise", - "promises" + "guid", + "identifier", + "uuid" ], "support": { - "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.3.0" + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.2" }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2025-08-19T18:57:03+00:00" + "time": "2025-12-14T04:43:48+00:00" }, { - "name": "react/promise-timer", - "version": "v1.11.0", + "name": "ratchet/rfc6455", + "version": "v0.4.0", "source": { "type": "git", - "url": "https://github.com/reactphp/promise-timer.git", - "reference": "4f70306ed66b8b44768941ca7f142092600fafc1" + "url": "https://github.com/ratchetphp/RFC6455.git", + "reference": "859d95f85dda0912c6d5b936d036d044e3af47ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/4f70306ed66b8b44768941ca7f142092600fafc1", - "reference": "4f70306ed66b8b44768941ca7f142092600fafc1", + "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/859d95f85dda0912c6d5b936d036d044e3af47ef", + "reference": "859d95f85dda0912c6d5b936d036d044e3af47ef", "shasum": "" }, "require": { - "php": ">=5.3", - "react/event-loop": "^1.2", - "react/promise": "^3.2 || ^2.7.0 || ^1.2.1" + "php": ">=7.4", + "psr/http-factory-implementation": "^1.0", + "symfony/polyfill-php80": "^1.15" }, "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + "guzzlehttp/psr7": "^2.7", + "phpunit/phpunit": "^9.5", + "react/socket": "^1.3" }, "type": "library", "autoload": { - "files": [ - "src/functions_include.php" - ], "psr-4": { - "React\\Promise\\Timer\\": "src/" + "Ratchet\\RFC6455\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -5146,81 +6001,55 @@ "MIT" ], "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, { "name": "Chris Boden", "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" + "role": "Developer" + }, + { + "name": "Matt Bonneau", + "role": "Developer" } ], - "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.", - "homepage": "https://github.com/reactphp/promise-timer", + "description": "RFC6455 WebSocket protocol handler", + "homepage": "http://socketo.me", "keywords": [ - "async", - "event-loop", - "promise", - "reactphp", - "timeout", - "timer" + "WebSockets", + "rfc6455", + "websocket" ], "support": { - "issues": "https://github.com/reactphp/promise-timer/issues", - "source": "https://github.com/reactphp/promise-timer/tree/v1.11.0" + "chat": "https://gitter.im/reactphp/reactphp", + "issues": "https://github.com/ratchetphp/RFC6455/issues", + "source": "https://github.com/ratchetphp/RFC6455/tree/v0.4.0" }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2024-06-04T14:27:45+00:00" + "time": "2025-02-24T01:18:22+00:00" }, { - "name": "react/socket", - "version": "v1.17.0", + "name": "react/cache", + "version": "v1.2.0", "source": { "type": "git", - "url": "https://github.com/reactphp/socket.git", - "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08" + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08", - "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", "shasum": "" }, "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "php": ">=5.3.0", - "react/dns": "^1.13", - "react/event-loop": "^1.2", - "react/promise": "^3.2 || ^2.6 || ^1.2.1", - "react/stream": "^1.4" + "react/promise": "^3.0 || ^2.0 || ^1.1" }, "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", - "react/async": "^4.3 || ^3.3 || ^2", - "react/promise-stream": "^1.4", - "react/promise-timer": "^1.11" + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" }, "type": "library", "autoload": { "psr-4": { - "React\\Socket\\": "src/" + "React\\Cache\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -5249,17 +6078,16 @@ "homepage": "https://cboden.dev/" } ], - "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "description": "Async, Promise-based cache interface for ReactPHP", "keywords": [ - "Connection", - "Socket", - "async", - "reactphp", - "stream" + "cache", + "caching", + "promise", + "reactphp" ], "support": { - "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.17.0" + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" }, "funding": [ { @@ -5267,35 +6095,37 @@ "type": "open_collective" } ], - "time": "2025-11-19T20:47:34+00:00" + "time": "2022-11-30T15:59:55+00:00" }, { - "name": "react/stream", - "version": "v1.4.0", + "name": "react/dns", + "version": "v1.14.0", "source": { "type": "git", - "url": "https://github.com/reactphp/stream.git", - "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + "url": "https://github.com/reactphp/dns.git", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", - "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3", "shasum": "" }, "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.8", - "react/event-loop": "^1.2" + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" }, "require-dev": { - "clue/stream-filter": "~1.2", - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" }, "type": "library", "autoload": { "psr-4": { - "React\\Stream\\": "src/" + "React\\Dns\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -5324,20 +6154,16 @@ "homepage": "https://cboden.dev/" } ], - "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "description": "Async DNS resolver for ReactPHP", "keywords": [ - "event-driven", - "io", - "non-blocking", - "pipe", - "reactphp", - "readable", - "stream", - "writable" + "async", + "dns", + "dns-resolver", + "reactphp" ], "support": { - "issues": "https://github.com/reactphp/stream/issues", - "source": "https://github.com/reactphp/stream/tree/v1.4.0" + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.14.0" }, "funding": [ { @@ -5345,478 +6171,339 @@ "type": "open_collective" } ], - "time": "2024-06-11T12:45:25+00:00" - }, - { - "name": "sabre/dav", - "version": "4.7.0", - "source": { - "type": "git", - "url": "https://github.com/sabre-io/dav.git", - "reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sabre-io/dav/zipball/074373bcd689a30bcf5aaa6bbb20a3395964ce7a", - "reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "ext-date": "*", - "ext-dom": "*", - "ext-iconv": "*", - "ext-json": "*", - "ext-mbstring": "*", - "ext-pcre": "*", - "ext-simplexml": "*", - "ext-spl": "*", - "lib-libxml": ">=2.7.0", - "php": "^7.1.0 || ^8.0", - "psr/log": "^1.0 || ^2.0 || ^3.0", - "sabre/event": "^5.0", - "sabre/http": "^5.0.5", - "sabre/uri": "^2.0", - "sabre/vobject": "^4.2.1", - "sabre/xml": "^2.0.1" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.19", - "monolog/monolog": "^1.27 || ^2.0", - "phpstan/phpstan": "^0.12 || ^1.0", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" - }, - "suggest": { - "ext-curl": "*", - "ext-imap": "*", - "ext-pdo": "*" - }, - "bin": [ - "bin/sabredav", - "bin/naturalselection" - ], - "type": "library", - "autoload": { - "psr-4": { - "Sabre\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Evert Pot", - "email": "me@evertpot.com", - "homepage": "http://evertpot.com/", - "role": "Developer" - } - ], - "description": "WebDAV Framework for PHP", - "homepage": "http://sabre.io/", - "keywords": [ - "CalDAV", - "CardDAV", - "WebDAV", - "framework", - "iCalendar" - ], - "support": { - "forum": "https://groups.google.com/group/sabredav-discuss", - "issues": "https://github.com/sabre-io/dav/issues", - "source": "https://github.com/fruux/sabre-dav" - }, - "time": "2024-10-29T11:46:02+00:00" - }, - { - "name": "sabre/event", - "version": "5.1.7", - "source": { - "type": "git", - "url": "https://github.com/sabre-io/event.git", - "reference": "86d57e305c272898ba3c28e9bd3d65d5464587c2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sabre-io/event/zipball/86d57e305c272898ba3c28e9bd3d65d5464587c2", - "reference": "86d57e305c272898ba3c28e9bd3d65d5464587c2", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "~2.17.1||^3.63", - "phpstan/phpstan": "^0.12", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" - }, - "type": "library", - "autoload": { - "files": [ - "lib/coroutine.php", - "lib/Loop/functions.php", - "lib/Promise/functions.php" - ], - "psr-4": { - "Sabre\\Event\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Evert Pot", - "email": "me@evertpot.com", - "homepage": "http://evertpot.com/", - "role": "Developer" - } - ], - "description": "sabre/event is a library for lightweight event-based programming", - "homepage": "http://sabre.io/event/", - "keywords": [ - "EventEmitter", - "async", - "coroutine", - "eventloop", - "events", - "hooks", - "plugin", - "promise", - "reactor", - "signal" - ], - "support": { - "forum": "https://groups.google.com/group/sabredav-discuss", - "issues": "https://github.com/sabre-io/event/issues", - "source": "https://github.com/fruux/sabre-event" - }, - "time": "2024-08-27T11:23:05+00:00" + "time": "2025-11-18T19:34:28+00:00" }, { - "name": "sabre/http", - "version": "5.1.13", + "name": "react/event-loop", + "version": "v1.6.0", "source": { "type": "git", - "url": "https://github.com/sabre-io/http.git", - "reference": "7c2a14097d1a0de2347dcbdc91a02f38e338f4db" + "url": "https://github.com/reactphp/event-loop.git", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sabre-io/http/zipball/7c2a14097d1a0de2347dcbdc91a02f38e338f4db", - "reference": "7c2a14097d1a0de2347dcbdc91a02f38e338f4db", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a", "shasum": "" }, "require": { - "ext-ctype": "*", - "ext-curl": "*", - "ext-mbstring": "*", - "php": "^7.1 || ^8.0", - "sabre/event": ">=4.0 <6.0", - "sabre/uri": "^2.0" + "php": ">=5.3.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "~2.17.1||3.63.2", - "phpstan/phpstan": "^0.12", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, "suggest": { - "ext-curl": " to make http requests with the Client class" + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" }, "type": "library", "autoload": { - "files": [ - "lib/functions.php" - ], "psr-4": { - "Sabre\\HTTP\\": "lib/" + "React\\EventLoop\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Evert Pot", - "email": "me@evertpot.com", - "homepage": "http://evertpot.com/", - "role": "Developer" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "The sabre/http library provides utilities for dealing with http requests and responses. ", - "homepage": "https://github.com/fruux/sabre-http", + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", "keywords": [ - "http" + "asynchronous", + "event-loop" ], "support": { - "forum": "https://groups.google.com/group/sabredav-discuss", - "issues": "https://github.com/sabre-io/http/issues", - "source": "https://github.com/fruux/sabre-http" + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" }, - "time": "2025-09-09T10:21:47+00:00" + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-17T20:46:25+00:00" }, { - "name": "sabre/uri", - "version": "2.3.4", + "name": "react/promise", + "version": "v3.3.0", "source": { "type": "git", - "url": "https://github.com/sabre-io/uri.git", - "reference": "b76524c22de90d80ca73143680a8e77b1266c291" + "url": "https://github.com/reactphp/promise.git", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sabre-io/uri/zipball/b76524c22de90d80ca73143680a8e77b1266c291", - "reference": "b76524c22de90d80ca73143680a8e77b1266c291", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0" + "php": ">=7.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.63", - "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^1.12", - "phpstan/phpstan-phpunit": "^1.4", - "phpstan/phpstan-strict-rules": "^1.6", - "phpunit/phpunit": "^9.6" + "phpstan/phpstan": "1.12.28 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" }, "type": "library", "autoload": { "files": [ - "lib/functions.php" + "src/functions_include.php" ], "psr-4": { - "Sabre\\Uri\\": "lib/" + "React\\Promise\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Evert Pot", - "email": "me@evertpot.com", - "homepage": "http://evertpot.com/", - "role": "Developer" + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "Functions for making sense out of URIs.", - "homepage": "http://sabre.io/uri/", + "description": "A lightweight implementation of CommonJS Promises/A for PHP", "keywords": [ - "rfc3986", - "uri", - "url" + "promise", + "promises" ], "support": { - "forum": "https://groups.google.com/group/sabredav-discuss", - "issues": "https://github.com/sabre-io/uri/issues", - "source": "https://github.com/fruux/sabre-uri" + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.3.0" }, - "time": "2024-08-27T12:18:16+00:00" + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-08-19T18:57:03+00:00" }, { - "name": "sabre/vobject", - "version": "4.5.8", + "name": "react/promise-timer", + "version": "v1.11.0", "source": { "type": "git", - "url": "https://github.com/sabre-io/vobject.git", - "reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1" + "url": "https://github.com/reactphp/promise-timer.git", + "reference": "4f70306ed66b8b44768941ca7f142092600fafc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sabre-io/vobject/zipball/d554eb24d64232922e1eab5896cc2f84b3b9ffb1", - "reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1", + "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/4f70306ed66b8b44768941ca7f142092600fafc1", + "reference": "4f70306ed66b8b44768941ca7f142092600fafc1", "shasum": "" }, "require": { - "ext-mbstring": "*", - "php": "^7.1 || ^8.0", - "sabre/xml": "^2.1 || ^3.0 || ^4.0" + "php": ">=5.3", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7.0 || ^1.2.1" }, "require-dev": { - "friendsofphp/php-cs-fixer": "~2.17.1", - "phpstan/phpstan": "^0.12 || ^1.12 || ^2.0", - "phpunit/php-invoker": "^2.0 || ^3.1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" - }, - "suggest": { - "hoa/bench": "If you would like to run the benchmark scripts" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, - "bin": [ - "bin/vobject", - "bin/generate_vcards" - ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0.x-dev" - } - }, "autoload": { + "files": [ + "src/functions_include.php" + ], "psr-4": { - "Sabre\\VObject\\": "lib/" + "React\\Promise\\Timer\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Evert Pot", - "email": "me@evertpot.com", - "homepage": "http://evertpot.com/", - "role": "Developer" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" }, { - "name": "Dominik Tobschall", - "email": "dominik@fruux.com", - "homepage": "http://tobschall.de/", - "role": "Developer" + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" }, { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net", - "homepage": "http://mnt.io/", - "role": "Developer" + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects", - "homepage": "http://sabre.io/vobject/", + "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.", + "homepage": "https://github.com/reactphp/promise-timer", "keywords": [ - "availability", - "freebusy", - "iCalendar", - "ical", - "ics", - "jCal", - "jCard", - "recurrence", - "rfc2425", - "rfc2426", - "rfc2739", - "rfc4770", - "rfc5545", - "rfc5546", - "rfc6321", - "rfc6350", - "rfc6351", - "rfc6474", - "rfc6638", - "rfc6715", - "rfc6868", - "vCalendar", - "vCard", - "vcf", - "xCal", - "xCard" + "async", + "event-loop", + "promise", + "reactphp", + "timeout", + "timer" ], "support": { - "forum": "https://groups.google.com/group/sabredav-discuss", - "issues": "https://github.com/sabre-io/vobject/issues", - "source": "https://github.com/fruux/sabre-vobject" + "issues": "https://github.com/reactphp/promise-timer/issues", + "source": "https://github.com/reactphp/promise-timer/tree/v1.11.0" }, - "time": "2026-01-12T10:45:19+00:00" + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-04T14:27:45+00:00" }, { - "name": "sabre/xml", - "version": "2.2.11", + "name": "react/socket", + "version": "v1.17.0", "source": { "type": "git", - "url": "https://github.com/sabre-io/xml.git", - "reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc" + "url": "https://github.com/reactphp/socket.git", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sabre-io/xml/zipball/01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc", - "reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc", + "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-xmlreader": "*", - "ext-xmlwriter": "*", - "lib-libxml": ">=2.6.20", - "php": "^7.1 || ^8.0", - "sabre/uri": ">=1.0,<3.0.0" + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" }, "require-dev": { - "friendsofphp/php-cs-fixer": "~2.17.1||3.63.2", - "phpstan/phpstan": "^0.12", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" }, "type": "library", "autoload": { - "files": [ - "lib/Deserializer/functions.php", - "lib/Serializer/functions.php" - ], "psr-4": { - "Sabre\\Xml\\": "lib/" + "React\\Socket\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Evert Pot", - "email": "me@evertpot.com", - "homepage": "http://evertpot.com/", - "role": "Developer" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" }, { - "name": "Markus Staab", - "email": "markus.staab@redaxo.de", - "role": "Developer" + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "sabre/xml is an XML library that you may not hate.", - "homepage": "https://sabre.io/xml/", + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", "keywords": [ - "XMLReader", - "XMLWriter", - "dom", - "xml" + "Connection", + "Socket", + "async", + "reactphp", + "stream" ], "support": { - "forum": "https://groups.google.com/group/sabredav-discuss", - "issues": "https://github.com/sabre-io/xml/issues", - "source": "https://github.com/fruux/sabre-xml" + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.17.0" }, - "time": "2024-09-06T07:37:46+00:00" + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-19T20:47:34+00:00" }, { - "name": "spatie/db-dumper", - "version": "3.8.3", + "name": "react/stream", + "version": "v1.4.0", "source": { "type": "git", - "url": "https://github.com/spatie/db-dumper.git", - "reference": "eac3221fbe27fac51f388600d27b67b1b079406e" + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/db-dumper/zipball/eac3221fbe27fac51f388600d27b67b1b079406e", - "reference": "eac3221fbe27fac51f388600d27b67b1b079406e", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", "shasum": "" }, "require": { - "php": "^8.0", - "symfony/process": "^5.0|^6.0|^7.0|^8.0" + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" }, "require-dev": { - "pestphp/pest": "^1.22" + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, "type": "library", "autoload": { "psr-4": { - "Spatie\\DbDumper\\": "src" + "React\\Stream\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -5825,1688 +6512,1559 @@ ], "authors": [ { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "homepage": "https://spatie.be", - "role": "Developer" + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" } ], - "description": "Dump databases", - "homepage": "https://github.com/spatie/db-dumper", + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", "keywords": [ - "database", - "db-dumper", - "dump", - "mysqldump", - "spatie" + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" ], "support": { - "source": "https://github.com/spatie/db-dumper/tree/3.8.3" + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" }, "funding": [ { - "url": "https://spatie.be/open-source/support-us", - "type": "custom" - }, - { - "url": "https://github.com/spatie", - "type": "github" + "url": "https://opencollective.com/reactphp", + "type": "open_collective" } ], - "time": "2026-01-05T16:26:03+00:00" + "time": "2024-06-11T12:45:25+00:00" }, { - "name": "spatie/laravel-backup", - "version": "9.4.1", + "name": "sabre/dav", + "version": "4.7.0", "source": { "type": "git", - "url": "https://github.com/spatie/laravel-backup.git", - "reference": "0dff805039617b4a6f48291cef0697a73bc0392e" + "url": "https://github.com/sabre-io/dav.git", + "reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/0dff805039617b4a6f48291cef0697a73bc0392e", - "reference": "0dff805039617b4a6f48291cef0697a73bc0392e", + "url": "https://api.github.com/repos/sabre-io/dav/zipball/074373bcd689a30bcf5aaa6bbb20a3395964ce7a", + "reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a", "shasum": "" }, "require": { - "ext-zip": "^1.14.0", - "illuminate/console": "^12.40", - "illuminate/contracts": "^12.40", - "illuminate/events": "^12.40", - "illuminate/filesystem": "^12.40", - "illuminate/notifications": "^12.40", - "illuminate/support": "^12.40", - "league/flysystem": "^3.30.2", - "php": "^8.3", - "spatie/db-dumper": "^3.8.1", - "spatie/laravel-package-tools": "^1.92.7", - "spatie/laravel-signal-aware-command": "^2.1", - "spatie/temporary-directory": "^2.3", - "symfony/console": "^7.3.6|^8.0", - "symfony/finder": "^7.3.5|^8.0" + "ext-ctype": "*", + "ext-date": "*", + "ext-dom": "*", + "ext-iconv": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "ext-spl": "*", + "lib-libxml": ">=2.7.0", + "php": "^7.1.0 || ^8.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "sabre/event": "^5.0", + "sabre/http": "^5.0.5", + "sabre/uri": "^2.0", + "sabre/vobject": "^4.2.1", + "sabre/xml": "^2.0.1" }, "require-dev": { - "composer-runtime-api": "^2.0", - "ext-pcntl": "*", - "larastan/larastan": "^3.8", - "laravel/slack-notification-channel": "^3.7", - "league/flysystem-aws-s3-v3": "^3.30.1", - "mockery/mockery": "^1.6.12", - "orchestra/testbench": "^10.8", - "pestphp/pest": "^4.1.5", - "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan-deprecation-rules": "^2.0.3", - "phpstan/phpstan-phpunit": "^2.0.8", - "rector/rector": "^2.2.8" + "friendsofphp/php-cs-fixer": "^2.19", + "monolog/monolog": "^1.27 || ^2.0", + "phpstan/phpstan": "^0.12 || ^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" }, "suggest": { - "laravel/slack-notification-channel": "Required for sending notifications via Slack" + "ext-curl": "*", + "ext-imap": "*", + "ext-pdo": "*" }, + "bin": [ + "bin/sabredav", + "bin/naturalselection" + ], "type": "library", - "extra": { - "laravel": { - "providers": [ - "Spatie\\Backup\\BackupServiceProvider" - ] - } - }, "autoload": { - "files": [ - "src/Helpers/functions.php" - ], "psr-4": { - "Spatie\\Backup\\": "src" + "Sabre\\": "lib/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", - "homepage": "https://spatie.be", + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", "role": "Developer" } ], - "description": "A Laravel package to backup your application", - "homepage": "https://github.com/spatie/laravel-backup", + "description": "WebDAV Framework for PHP", + "homepage": "http://sabre.io/", "keywords": [ - "backup", - "database", - "laravel-backup", - "spatie" + "CalDAV", + "CardDAV", + "WebDAV", + "framework", + "iCalendar" ], "support": { - "issues": "https://github.com/spatie/laravel-backup/issues", - "source": "https://github.com/spatie/laravel-backup/tree/9.4.1" + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/dav/issues", + "source": "https://github.com/fruux/sabre-dav" }, - "funding": [ - { - "url": "https://github.com/sponsors/spatie", - "type": "github" - }, - { - "url": "https://spatie.be/open-source/support-us", - "type": "other" - } - ], - "time": "2026-02-15T19:05:20+00:00" + "time": "2024-10-29T11:46:02+00:00" }, { - "name": "spatie/laravel-package-tools", - "version": "1.93.0", + "name": "sabre/event", + "version": "5.1.8", "source": { "type": "git", - "url": "https://github.com/spatie/laravel-package-tools.git", - "reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7" + "url": "https://github.com/sabre-io/event.git", + "reference": "1dd5f55421b0092006510264131a4d632d02d2c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/0d097bce95b2bf6802fb1d83e1e753b0f5a948e7", - "reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7", + "url": "https://api.github.com/repos/sabre-io/event/zipball/1dd5f55421b0092006510264131a4d632d02d2c1", + "reference": "1dd5f55421b0092006510264131a4d632d02d2c1", "shasum": "" }, "require": { - "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0", - "php": "^8.1" + "php": "^7.1 || ^8.0" }, "require-dev": { - "mockery/mockery": "^1.5", - "orchestra/testbench": "^8.0|^9.2|^10.0|^11.0", - "pestphp/pest": "^2.1|^3.1|^4.0", - "phpunit/php-code-coverage": "^10.0|^11.0|^12.0", - "phpunit/phpunit": "^10.5|^11.5|^12.5", - "spatie/pest-plugin-test-time": "^2.2|^3.0" + "friendsofphp/php-cs-fixer": "~2.17.1||^3.95", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" }, "type": "library", "autoload": { + "files": [ + "lib/coroutine.php", + "lib/Loop/functions.php", + "lib/Promise/functions.php" + ], "psr-4": { - "Spatie\\LaravelPackageTools\\": "src" + "Sabre\\Event\\": "lib/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", "role": "Developer" } ], - "description": "Tools for creating Laravel packages", - "homepage": "https://github.com/spatie/laravel-package-tools", + "description": "sabre/event is a library for lightweight event-based programming", + "homepage": "http://sabre.io/event/", "keywords": [ - "laravel-package-tools", - "spatie" + "EventEmitter", + "async", + "coroutine", + "eventloop", + "events", + "hooks", + "plugin", + "promise", + "reactor", + "signal" ], "support": { - "issues": "https://github.com/spatie/laravel-package-tools/issues", - "source": "https://github.com/spatie/laravel-package-tools/tree/1.93.0" + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/event/issues", + "source": "https://github.com/fruux/sabre-event" }, - "funding": [ - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2026-02-21T12:49:54+00:00" + "time": "2026-04-27T04:19:36+00:00" }, { - "name": "spatie/laravel-signal-aware-command", - "version": "2.1.2", + "name": "sabre/http", + "version": "5.1.13", "source": { "type": "git", - "url": "https://github.com/spatie/laravel-signal-aware-command.git", - "reference": "54dcc1efd152bfb3eb0faf56a5fc28569b864b5d" + "url": "https://github.com/sabre-io/http.git", + "reference": "7c2a14097d1a0de2347dcbdc91a02f38e338f4db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-signal-aware-command/zipball/54dcc1efd152bfb3eb0faf56a5fc28569b864b5d", - "reference": "54dcc1efd152bfb3eb0faf56a5fc28569b864b5d", + "url": "https://api.github.com/repos/sabre-io/http/zipball/7c2a14097d1a0de2347dcbdc91a02f38e338f4db", + "reference": "7c2a14097d1a0de2347dcbdc91a02f38e338f4db", "shasum": "" }, "require": { - "illuminate/contracts": "^11.0|^12.0|^13.0", - "php": "^8.2", - "spatie/laravel-package-tools": "^1.4.3", - "symfony/console": "^7.0|^8.0" + "ext-ctype": "*", + "ext-curl": "*", + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabre/event": ">=4.0 <6.0", + "sabre/uri": "^2.0" }, "require-dev": { - "brianium/paratest": "^6.2|^7.0", - "ext-pcntl": "*", - "nunomaduro/collision": "^5.3|^6.0|^7.0|^8.0", - "orchestra/testbench": "^9.0|^10.0", - "pestphp/pest-plugin-laravel": "^1.3|^2.0|^3.0", - "phpunit/phpunit": "^9.5|^10|^11", - "spatie/laravel-ray": "^1.17" + "friendsofphp/php-cs-fixer": "~2.17.1||3.63.2", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" }, - "type": "library", - "extra": { - "laravel": { - "aliases": { - "Signal": "Spatie\\SignalAwareCommand\\Facades\\Signal" - }, - "providers": [ - "Spatie\\SignalAwareCommand\\SignalAwareCommandServiceProvider" - ] - } + "suggest": { + "ext-curl": " to make http requests with the Client class" }, + "type": "library", "autoload": { + "files": [ + "lib/functions.php" + ], "psr-4": { - "Spatie\\SignalAwareCommand\\": "src" + "Sabre\\HTTP\\": "lib/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Freek Van der Herten", - "email": "freek@spatie.be", + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", "role": "Developer" } ], - "description": "Handle signals in artisan commands", - "homepage": "https://github.com/spatie/laravel-signal-aware-command", + "description": "The sabre/http library provides utilities for dealing with http requests and responses. ", + "homepage": "https://github.com/fruux/sabre-http", "keywords": [ - "laravel", - "laravel-signal-aware-command", - "spatie" + "http" ], "support": { - "issues": "https://github.com/spatie/laravel-signal-aware-command/issues", - "source": "https://github.com/spatie/laravel-signal-aware-command/tree/2.1.2" + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/http/issues", + "source": "https://github.com/fruux/sabre-http" }, - "funding": [ - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2026-02-22T08:16:31+00:00" + "time": "2025-09-09T10:21:47+00:00" }, { - "name": "spatie/temporary-directory", - "version": "2.3.1", + "name": "sabre/uri", + "version": "2.3.4", "source": { "type": "git", - "url": "https://github.com/spatie/temporary-directory.git", - "reference": "662e481d6ec07ef29fd05010433428851a42cd07" + "url": "https://github.com/sabre-io/uri.git", + "reference": "b76524c22de90d80ca73143680a8e77b1266c291" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/662e481d6ec07ef29fd05010433428851a42cd07", - "reference": "662e481d6ec07ef29fd05010433428851a42cd07", + "url": "https://api.github.com/repos/sabre-io/uri/zipball/b76524c22de90d80ca73143680a8e77b1266c291", + "reference": "b76524c22de90d80ca73143680a8e77b1266c291", "shasum": "" }, "require": { - "php": "^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "friendsofphp/php-cs-fixer": "^3.63", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^1.12", + "phpstan/phpstan-phpunit": "^1.4", + "phpstan/phpstan-strict-rules": "^1.6", + "phpunit/phpunit": "^9.6" }, "type": "library", "autoload": { + "files": [ + "lib/functions.php" + ], "psr-4": { - "Spatie\\TemporaryDirectory\\": "src" + "Sabre\\Uri\\": "lib/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Alex Vanderbist", - "email": "alex@spatie.be", - "homepage": "https://spatie.be", + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", "role": "Developer" } ], - "description": "Easily create, use and destroy temporary directories", - "homepage": "https://github.com/spatie/temporary-directory", + "description": "Functions for making sense out of URIs.", + "homepage": "http://sabre.io/uri/", "keywords": [ - "php", - "spatie", - "temporary-directory" + "rfc3986", + "uri", + "url" ], "support": { - "issues": "https://github.com/spatie/temporary-directory/issues", - "source": "https://github.com/spatie/temporary-directory/tree/2.3.1" + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/uri/issues", + "source": "https://github.com/fruux/sabre-uri" }, - "funding": [ - { - "url": "https://spatie.be/open-source/support-us", - "type": "custom" - }, - { - "url": "https://github.com/spatie", - "type": "github" - } - ], - "time": "2026-01-12T07:42:22+00:00" + "time": "2024-08-27T12:18:16+00:00" }, { - "name": "symfony/clock", - "version": "v8.0.0", + "name": "sabre/vobject", + "version": "4.5.8", "source": { "type": "git", - "url": "https://github.com/symfony/clock.git", - "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f" + "url": "https://github.com/sabre-io/vobject.git", + "reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/832119f9b8dbc6c8e6f65f30c5969eca1e88764f", - "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f", + "url": "https://api.github.com/repos/sabre-io/vobject/zipball/d554eb24d64232922e1eab5896cc2f84b3b9ffb1", + "reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1", "shasum": "" }, "require": { - "php": ">=8.4", - "psr/clock": "^1.0" + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabre/xml": "^2.1 || ^3.0 || ^4.0" }, - "provide": { - "psr/clock-implementation": "1.0" + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1", + "phpstan/phpstan": "^0.12 || ^1.12 || ^2.0", + "phpunit/php-invoker": "^2.0 || ^3.1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "suggest": { + "hoa/bench": "If you would like to run the benchmark scripts" }, + "bin": [ + "bin/vobject", + "bin/generate_vcards" + ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, "autoload": { - "files": [ - "Resources/now.php" - ], "psr-4": { - "Symfony\\Component\\Clock\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Sabre\\VObject\\": "lib/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Decouples applications from the system clock", - "homepage": "https://symfony.com", - "keywords": [ - "clock", - "psr20", - "time" - ], - "support": { - "source": "https://github.com/symfony/clock/tree/v8.0.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" }, { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "name": "Dominik Tobschall", + "email": "dominik@fruux.com", + "homepage": "http://tobschall.de/", + "role": "Developer" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net", + "homepage": "http://mnt.io/", + "role": "Developer" } ], - "time": "2025-11-12T15:46:48+00:00" + "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects", + "homepage": "http://sabre.io/vobject/", + "keywords": [ + "availability", + "freebusy", + "iCalendar", + "ical", + "ics", + "jCal", + "jCard", + "recurrence", + "rfc2425", + "rfc2426", + "rfc2739", + "rfc4770", + "rfc5545", + "rfc5546", + "rfc6321", + "rfc6350", + "rfc6351", + "rfc6474", + "rfc6638", + "rfc6715", + "rfc6868", + "vCalendar", + "vCard", + "vcf", + "xCal", + "xCard" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/vobject/issues", + "source": "https://github.com/fruux/sabre-vobject" + }, + "time": "2026-01-12T10:45:19+00:00" }, { - "name": "symfony/console", - "version": "v7.4.7", + "name": "sabre/xml", + "version": "2.2.11", "source": { "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d" + "url": "https://github.com/sabre-io/xml.git", + "reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/e1e6770440fb9c9b0cf725f81d1361ad1835329d", - "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d", + "url": "https://api.github.com/repos/sabre-io/xml/zipball/01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc", + "reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2|^8.0" - }, - "conflict": { - "symfony/dependency-injection": "<6.4", - "symfony/dotenv": "<6.4", - "symfony/event-dispatcher": "<6.4", - "symfony/lock": "<6.4", - "symfony/process": "<6.4" - }, - "provide": { - "psr/log-implementation": "1.0|2.0|3.0" + "ext-dom": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "lib-libxml": ">=2.6.20", + "php": "^7.1 || ^8.0", + "sabre/uri": ">=1.0,<3.0.0" }, "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/event-dispatcher": "^6.4|^7.0|^8.0", - "symfony/http-foundation": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/lock": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/stopwatch": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0" + "friendsofphp/php-cs-fixer": "~2.17.1||3.63.2", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" }, "type": "library", "autoload": { + "files": [ + "lib/Deserializer/functions.php", + "lib/Serializer/functions.php" + ], "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Sabre\\Xml\\": "lib/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Markus Staab", + "email": "markus.staab@redaxo.de", + "role": "Developer" } ], - "description": "Eases the creation of beautiful and testable command line interfaces", - "homepage": "https://symfony.com", + "description": "sabre/xml is an XML library that you may not hate.", + "homepage": "https://sabre.io/xml/", "keywords": [ - "cli", - "command-line", - "console", - "terminal" + "XMLReader", + "XMLWriter", + "dom", + "xml" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.7" + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/xml/issues", + "source": "https://github.com/fruux/sabre-xml" }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2026-03-06T14:06:20+00:00" + "time": "2024-09-06T07:37:46+00:00" }, { - "name": "symfony/css-selector", - "version": "v8.0.6", + "name": "sebastian/cli-parser", + "version": "5.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/css-selector.git", - "reference": "2a178bf80f05dbbe469a337730eba79d61315262" + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "48a4654fa5e48c1c81214e9930048a572d4b23ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/2a178bf80f05dbbe469a337730eba79d61315262", - "reference": "2a178bf80f05dbbe469a337730eba79d61315262", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/48a4654fa5e48c1c81214e9930048a572d4b23ca", + "reference": "48a4654fa5e48c1c81214e9930048a572d4b23ca", "shasum": "" }, "require": { "php": ">=8.4" }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, "autoload": { - "psr-4": { - "Symfony\\Component\\CssSelector\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Jean-François Simon", - "email": "jeanfrancois.simon@sensiolabs.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Converts CSS selectors to XPath expressions", - "homepage": "https://symfony.com", + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { - "source": "https://github.com/symfony/css-selector/tree/v8.0.6" + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/5.0.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" + "url": "https://github.com/sebastianbergmann", + "type": "github" }, { - "url": "https://github.com/fabpot", - "type": "github" + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" }, { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", "type": "tidelift" } ], - "time": "2026-02-17T13:07:04+00:00" + "time": "2026-02-06T04:39:44+00:00" }, { - "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "name": "sebastian/comparator", + "version": "8.1.2", "source": { "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "b3d09f4360ad97dcad8f82d1c047ad16ff38b7e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/b3d09f4360ad97dcad8f82d1c047ad16ff38b7e1", + "reference": "b3d09f4360ad97dcad8f82d1c047ad16ff38b7e1", "shasum": "" }, "require": { - "php": ">=8.1" + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.4", + "sebastian/diff": "^8.1", + "sebastian/exporter": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" }, "type": "library", "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "8.1-dev" } }, "autoload": { - "files": [ - "function.php" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" } ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/8.1.2" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" + "url": "https://github.com/sebastianbergmann", + "type": "github" }, { - "url": "https://github.com/fabpot", - "type": "github" + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-04-14T08:24:42+00:00" }, { - "name": "symfony/error-handler", - "version": "v7.4.4", + "name": "sebastian/complexity", + "version": "6.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/error-handler.git", - "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8" + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/8da531f364ddfee53e36092a7eebbbd0b775f6b8", - "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/c5651c795c98093480df79350cb050813fc7a2f3", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3", "shasum": "" }, "require": { - "php": ">=8.2", - "psr/log": "^1|^2|^3", - "symfony/polyfill-php85": "^1.32", - "symfony/var-dumper": "^6.4|^7.0|^8.0" - }, - "conflict": { - "symfony/deprecation-contracts": "<2.5", - "symfony/http-kernel": "<6.4" + "nikic/php-parser": "^5.0", + "php": ">=8.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0|^8.0", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/serializer": "^6.4|^7.0|^8.0", - "symfony/webpack-encore-bundle": "^1.0|^2.0" + "phpunit/phpunit": "^13.0" }, - "bin": [ - "Resources/bin/patch-type-declarations" - ], "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, "autoload": { - "psr-4": { - "Symfony\\Component\\ErrorHandler\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Provides tools to manage errors and ease debugging PHP code", - "homepage": "https://symfony.com", + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.4.4" + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/6.0.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" + "url": "https://github.com/sebastianbergmann", + "type": "github" }, { - "url": "https://github.com/fabpot", - "type": "github" + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" }, { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/sebastian/complexity", "type": "tidelift" } ], - "time": "2026-01-20T16:42:42+00:00" + "time": "2026-02-06T04:41:32+00:00" }, { - "name": "symfony/event-dispatcher", - "version": "v8.0.4", + "name": "sebastian/diff", + "version": "8.1.0", "source": { "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "99301401da182b6cfaa4700dbe9987bb75474b47" + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "9c957d730257f49c873f3761674559bd90098a7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47", - "reference": "99301401da182b6cfaa4700dbe9987bb75474b47", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/9c957d730257f49c873f3761674559bd90098a7d", + "reference": "9c957d730257f49c873f3761674559bd90098a7d", "shasum": "" }, "require": { - "php": ">=8.4", - "symfony/event-dispatcher-contracts": "^2.5|^3" - }, - "conflict": { - "symfony/security-http": "<7.4", - "symfony/service-contracts": "<2.5" - }, - "provide": { - "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "2.0|3.0" + "php": ">=8.4" }, "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^7.4|^8.0", - "symfony/dependency-injection": "^7.4|^8.0", - "symfony/error-handler": "^7.4|^8.0", - "symfony/expression-language": "^7.4|^8.0", - "symfony/framework-bundle": "^7.4|^8.0", - "symfony/http-foundation": "^7.4|^8.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^7.4|^8.0" + "phpunit/phpunit": "^13.0", + "symfony/process": "^7.2" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.1-dev" + } + }, "autoload": { - "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" } ], - "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", - "homepage": "https://symfony.com", + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.4" + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/8.1.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" + "url": "https://github.com/sebastianbergmann", + "type": "github" }, { - "url": "https://github.com/fabpot", - "type": "github" + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" }, { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/sebastian/diff", "type": "tidelift" } ], - "time": "2026-01-05T11:45:55+00:00" + "time": "2026-04-05T12:02:33+00:00" }, { - "name": "symfony/event-dispatcher-contracts", - "version": "v3.6.0", + "name": "sebastian/environment", + "version": "9.3.0", "source": { "type": "git", - "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "6767059a30e4277ac95ee034809e793528464768" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", - "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6767059a30e4277ac95ee034809e793528464768", + "reference": "6767059a30e4277ac95ee034809e793528464768", "shasum": "" }, "require": { - "php": ">=8.1", - "psr/event-dispatcher": "^1" + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "suggest": { + "ext-posix": "*" }, "type": "library", "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "9.3-dev" } }, "autoload": { - "psr-4": { - "Symfony\\Contracts\\EventDispatcher\\": "" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "Generic abstractions related to dispatching event", - "homepage": "https://symfony.com", + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "Xdebug", + "environment", + "hhvm" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/9.3.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" + "url": "https://github.com/sebastianbergmann", + "type": "github" }, { - "url": "https://github.com/fabpot", - "type": "github" + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-04-15T12:14:03+00:00" }, { - "name": "symfony/filesystem", - "version": "v8.0.6", + "name": "sebastian/exporter", + "version": "8.0.2", "source": { "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "7bf9162d7a0dff98d079b72948508fa48018a770" + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "9cee180ebe62259e3ed48df2212d1fc8cfd971bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/7bf9162d7a0dff98d079b72948508fa48018a770", - "reference": "7bf9162d7a0dff98d079b72948508fa48018a770", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/9cee180ebe62259e3ed48df2212d1fc8cfd971bb", + "reference": "9cee180ebe62259e3ed48df2212d1fc8cfd971bb", "shasum": "" }, "require": { + "ext-mbstring": "*", "php": ">=8.4", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.8" + "sebastian/recursion-context": "^8.0" }, "require-dev": { - "symfony/process": "^7.4|^8.0" + "phpunit/phpunit": "^13.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, "autoload": { - "psr-4": { - "Symfony\\Component\\Filesystem\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" } ], - "description": "Provides basic utilities for the filesystem", - "homepage": "https://symfony.com", + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], "support": { - "source": "https://github.com/symfony/filesystem/tree/v8.0.6" + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/8.0.2" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" + "url": "https://github.com/sebastianbergmann", + "type": "github" }, { - "url": "https://github.com/fabpot", - "type": "github" + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" }, { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", "type": "tidelift" } ], - "time": "2026-02-25T16:59:43+00:00" + "time": "2026-04-15T12:38:05+00:00" }, { - "name": "symfony/finder", - "version": "v7.4.6", + "name": "sebastian/git-state", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf" + "url": "https://github.com/sebastianbergmann/git-state.git", + "reference": "792a952e0eba55b6960a48aeceb9f371aad1f76b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf", - "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "url": "https://api.github.com/repos/sebastianbergmann/git-state/zipball/792a952e0eba55b6960a48aeceb9f371aad1f76b", + "reference": "792a952e0eba55b6960a48aeceb9f371aad1f76b", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0|^8.0" + "phpunit/phpunit": "^13.0" }, "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Finder\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Finds files and directories via an intuitive fluent interface", - "homepage": "https://symfony.com", + "description": "Library for describing the state of a Git checkout", + "homepage": "https://github.com/sebastianbergmann/git-state", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.6" + "issues": "https://github.com/sebastianbergmann/git-state/issues", + "security": "https://github.com/sebastianbergmann/git-state/security/policy", + "source": "https://github.com/sebastianbergmann/git-state/tree/1.0.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" + "url": "https://github.com/sebastianbergmann", + "type": "github" }, { - "url": "https://github.com/fabpot", - "type": "github" + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" }, { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/sebastian/git-state", "type": "tidelift" } ], - "time": "2026-01-29T09:40:50+00:00" + "time": "2026-03-21T12:54:28+00:00" }, { - "name": "symfony/http-foundation", - "version": "v7.4.7", + "name": "sebastian/global-state", + "version": "9.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/http-foundation.git", - "reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81" + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "e52e3dc22441e6218c710afe72c3042f8fc41ea7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f94b3e7b7dafd40e666f0c9ff2084133bae41e81", - "reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e52e3dc22441e6218c710afe72c3042f8fc41ea7", + "reference": "e52e3dc22441e6218c710afe72c3042f8fc41ea7", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "^1.1" - }, - "conflict": { - "doctrine/dbal": "<3.6", - "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + "php": ">=8.4", + "sebastian/object-reflector": "^6.0", + "sebastian/recursion-context": "^8.0" }, "require-dev": { - "doctrine/dbal": "^3.6|^4", - "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.4.12|^7.1.5|^8.0", - "symfony/clock": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/expression-language": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/mime": "^6.4|^7.0|^8.0", - "symfony/rate-limiter": "^6.4|^7.0|^8.0" + "ext-dom": "*", + "phpunit/phpunit": "^13.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, "autoload": { - "psr-4": { - "Symfony\\Component\\HttpFoundation\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "Defines an object-oriented layer for the HTTP specification", - "homepage": "https://symfony.com", + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.7" + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/9.0.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" + "url": "https://github.com/sebastianbergmann", + "type": "github" }, { - "url": "https://github.com/fabpot", - "type": "github" + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" }, { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", "type": "tidelift" } ], - "time": "2026-03-06T13:15:18+00:00" + "time": "2026-02-06T04:45:13+00:00" }, { - "name": "symfony/http-kernel", - "version": "v7.4.7", + "name": "sebastian/lines-of-code", + "version": "5.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/http-kernel.git", - "reference": "3b3fcf386c809be990c922e10e4c620d6367cab1" + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "4f21bb7768e1c997722ccc7efb1d6b5c11bfd471" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/3b3fcf386c809be990c922e10e4c620d6367cab1", - "reference": "3b3fcf386c809be990c922e10e4c620d6367cab1", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/4f21bb7768e1c997722ccc7efb1d6b5c11bfd471", + "reference": "4f21bb7768e1c997722ccc7efb1d6b5c11bfd471", "shasum": "" }, "require": { - "php": ">=8.2", - "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/error-handler": "^6.4|^7.0|^8.0", - "symfony/event-dispatcher": "^7.3|^8.0", - "symfony/http-foundation": "^7.4|^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "symfony/browser-kit": "<6.4", - "symfony/cache": "<6.4", - "symfony/config": "<6.4", - "symfony/console": "<6.4", - "symfony/dependency-injection": "<6.4", - "symfony/doctrine-bridge": "<6.4", - "symfony/flex": "<2.10", - "symfony/form": "<6.4", - "symfony/http-client": "<6.4", - "symfony/http-client-contracts": "<2.5", - "symfony/mailer": "<6.4", - "symfony/messenger": "<6.4", - "symfony/translation": "<6.4", - "symfony/translation-contracts": "<2.5", - "symfony/twig-bridge": "<6.4", - "symfony/validator": "<6.4", - "symfony/var-dumper": "<6.4", - "twig/twig": "<3.12" - }, - "provide": { - "psr/log-implementation": "1.0|2.0|3.0" + "nikic/php-parser": "^5.0", + "php": ">=8.4" }, "require-dev": { - "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^6.4|^7.0|^8.0", - "symfony/clock": "^6.4|^7.0|^8.0", - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/console": "^6.4|^7.0|^8.0", - "symfony/css-selector": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", - "symfony/dom-crawler": "^6.4|^7.0|^8.0", - "symfony/expression-language": "^6.4|^7.0|^8.0", - "symfony/finder": "^6.4|^7.0|^8.0", - "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/property-access": "^7.1|^8.0", - "symfony/routing": "^6.4|^7.0|^8.0", - "symfony/serializer": "^7.1|^8.0", - "symfony/stopwatch": "^6.4|^7.0|^8.0", - "symfony/translation": "^6.4|^7.0|^8.0", - "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^6.4|^7.0|^8.0", - "symfony/validator": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0", - "symfony/var-exporter": "^6.4|^7.0|^8.0", - "twig/twig": "^3.12" + "phpunit/phpunit": "^13.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, "autoload": { - "psr-4": { - "Symfony\\Component\\HttpKernel\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Provides a structured process for converting a Request into a Response", - "homepage": "https://symfony.com", + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.4.7" + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/5.0.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" + "url": "https://github.com/sebastianbergmann", + "type": "github" }, { - "url": "https://github.com/fabpot", - "type": "github" + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" }, { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code", "type": "tidelift" } ], - "time": "2026-03-06T16:33:18+00:00" + "time": "2026-02-06T04:45:54+00:00" }, { - "name": "symfony/mailer", - "version": "v7.4.6", + "name": "sebastian/object-enumerator", + "version": "8.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/mailer.git", - "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9" + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "b39ab125fd9a7434b0ecbc4202eebce11a98cfc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/b02726f39a20bc65e30364f5c750c4ddbf1f58e9", - "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/b39ab125fd9a7434b0ecbc4202eebce11a98cfc5", + "reference": "b39ab125fd9a7434b0ecbc4202eebce11a98cfc5", "shasum": "" }, "require": { - "egulias/email-validator": "^2.1.10|^3|^4", - "php": ">=8.2", - "psr/event-dispatcher": "^1", - "psr/log": "^1|^2|^3", - "symfony/event-dispatcher": "^6.4|^7.0|^8.0", - "symfony/mime": "^7.2|^8.0", - "symfony/service-contracts": "^2.5|^3" - }, - "conflict": { - "symfony/http-client-contracts": "<2.5", - "symfony/http-kernel": "<6.4", - "symfony/messenger": "<6.4", - "symfony/mime": "<6.4", - "symfony/twig-bridge": "<6.4" + "php": ">=8.4", + "sebastian/object-reflector": "^6.0", + "sebastian/recursion-context": "^8.0" }, "require-dev": { - "symfony/console": "^6.4|^7.0|^8.0", - "symfony/http-client": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", - "symfony/twig-bridge": "^6.4|^7.0|^8.0" + "phpunit/phpunit": "^13.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, "autoload": { - "psr-4": { - "Symfony\\Component\\Mailer\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "Helps sending emails", - "homepage": "https://symfony.com", + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.4.6" + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/8.0.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" + "url": "https://github.com/sebastianbergmann", + "type": "github" }, { - "url": "https://github.com/fabpot", - "type": "github" + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" }, { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/sebastian/object-enumerator", "type": "tidelift" } ], - "time": "2026-02-25T16:50:00+00:00" + "time": "2026-02-06T04:46:36+00:00" }, { - "name": "symfony/mime", - "version": "v7.4.7", + "name": "sebastian/object-reflector", + "version": "6.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/mime.git", - "reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1" + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "3ca042c2c60b0eab094f8a1b6a7093f4d4c72200" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/da5ab4fde3f6c88ab06e96185b9922f48b677cd1", - "reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/3ca042c2c60b0eab094f8a1b6a7093f4d4c72200", + "reference": "3ca042c2c60b0eab094f8a1b6a7093f4d4c72200", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-intl-idn": "^1.10", - "symfony/polyfill-mbstring": "^1.0" - }, - "conflict": { - "egulias/email-validator": "~3.0.0", - "phpdocumentor/reflection-docblock": "<5.2|>=7", - "phpdocumentor/type-resolver": "<1.5.1", - "symfony/mailer": "<6.4", - "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + "php": ">=8.4" }, "require-dev": { - "egulias/email-validator": "^2.1.10|^3.1|^4", - "league/html-to-markdown": "^5.0", - "phpdocumentor/reflection-docblock": "^5.2|^6.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/property-access": "^6.4|^7.0|^8.0", - "symfony/property-info": "^6.4|^7.0|^8.0", - "symfony/serializer": "^6.4.3|^7.0.3|^8.0" + "phpunit/phpunit": "^13.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, "autoload": { - "psr-4": { - "Symfony\\Component\\Mime\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "Allows manipulating MIME messages", - "homepage": "https://symfony.com", - "keywords": [ - "mime", - "mime-type" - ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { - "source": "https://github.com/symfony/mime/tree/v7.4.7" + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/6.0.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" + "url": "https://github.com/sebastianbergmann", + "type": "github" }, { - "url": "https://github.com/fabpot", - "type": "github" + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" }, { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/sebastian/object-reflector", "type": "tidelift" } ], - "time": "2026-03-05T15:24:09+00:00" + "time": "2026-02-06T04:47:13+00:00" }, { - "name": "symfony/polyfill-ctype", - "version": "v1.33.0", + "name": "sebastian/recursion-context", + "version": "8.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "74c5af21f6a5833e91767ca068c4d3dfec15317e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/74c5af21f6a5833e91767ca068c4d3dfec15317e", + "reference": "74c5af21f6a5833e91767ca068c4d3dfec15317e", "shasum": "" }, "require": { - "php": ">=7.2" - }, - "provide": { - "ext-ctype": "*" + "php": ">=8.4" }, - "suggest": { - "ext-ctype": "For best performance" + "require-dev": { + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" + "branch-alias": { + "dev-main": "8.0-dev" } }, "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" } ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/8.0.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" + "url": "https://github.com/sebastianbergmann", + "type": "github" }, { - "url": "https://github.com/fabpot", - "type": "github" + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" }, { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-02-06T04:51:28+00:00" }, { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.33.0", + "name": "sebastian/type", + "version": "7.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "42412224607bd3931241bbd17f38e0f972f5a916" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/42412224607bd3931241bbd17f38e0f972f5a916", + "reference": "42412224607bd3931241bbd17f38e0f972f5a916", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=8.4" }, - "suggest": { - "ext-intl": "For best performance" + "require-dev": { + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" + "branch-alias": { + "dev-main": "7.0-dev" } }, "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Symfony polyfill for intl's grapheme_* functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "grapheme", - "intl", - "polyfill", - "portable", - "shim" - ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/7.0.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" + "url": "https://github.com/sebastianbergmann", + "type": "github" }, { - "url": "https://github.com/fabpot", - "type": "github" + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" }, { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", "type": "tidelift" } ], - "time": "2025-06-27T09:58:17+00:00" + "time": "2026-02-06T04:52:09+00:00" }, { - "name": "symfony/polyfill-intl-idn", - "version": "v1.33.0", + "name": "sebastian/version", + "version": "7.0.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", - "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/ad37a5552c8e2b88572249fdc19b6da7792e021b", + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b", "shasum": "" }, "require": { - "php": ">=7.2", - "symfony/polyfill-intl-normalizer": "^1.10" - }, - "suggest": { - "ext-intl": "For best performance" + "php": ">=8.4" }, "type": "library", "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" + "branch-alias": { + "dev-main": "7.0-dev" } }, "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Idn\\": "" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Laurent Bassin", - "email": "laurent@bassin.info" - }, - { - "name": "Trevor Rowbotham", - "email": "trevor.rowbotham@pm.me" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "idn", - "intl", - "polyfill", - "portable", - "shim" - ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/7.0.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" + "url": "https://github.com/sebastianbergmann", + "type": "github" }, { - "url": "https://github.com/fabpot", - "type": "github" + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" }, { - "url": "https://github.com/nicolas-grekas", - "type": "github" + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "url": "https://tidelift.com/funding/github/packagist/sebastian/version", "type": "tidelift" } ], - "time": "2024-09-10T14:38:51+00:00" + "time": "2026-02-06T04:52:52+00:00" }, { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", + "name": "spatie/db-dumper", + "version": "4.1.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "url": "https://github.com/spatie/db-dumper.git", + "reference": "f8f2785574de84aa4642a4df8b59a6dd578dd5d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/spatie/db-dumper/zipball/f8f2785574de84aa4642a4df8b59a6dd578dd5d3", + "reference": "f8f2785574de84aa4642a4df8b59a6dd578dd5d3", "shasum": "" }, "require": { - "php": ">=7.2" + "php": "^8.3", + "symfony/process": "^7.0|^8.0" }, - "suggest": { - "ext-intl": "For best performance" + "require-dev": { + "pestphp/pest": "^4.0", + "phpstan/phpstan": "^2.1" }, "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, - "classmap": [ - "Resources/stubs" - ] + "Spatie\\DbDumper\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -7514,84 +8072,98 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" } ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", - "homepage": "https://symfony.com", + "description": "Dump databases", + "homepage": "https://github.com/spatie/db-dumper", "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" + "database", + "db-dumper", + "dump", + "mysqldump", + "spatie" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + "source": "https://github.com/spatie/db-dumper/tree/4.1.1" }, "funding": [ { - "url": "https://symfony.com/sponsor", + "url": "https://spatie.be/open-source/support-us", "type": "custom" }, { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", + "url": "https://github.com/spatie", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-04T11:43:33+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", + "name": "spatie/laravel-backup", + "version": "10.2.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + "url": "https://github.com/spatie/laravel-backup.git", + "reference": "e69bda927005e4909e67b7b86eb7697ed1fe51bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/e69bda927005e4909e67b7b86eb7697ed1fe51bc", + "reference": "e69bda927005e4909e67b7b86eb7697ed1fe51bc", "shasum": "" }, "require": { - "ext-iconv": "*", - "php": ">=7.2" + "ext-zip": "^1.14.0", + "illuminate/console": "^12.40|^13.0", + "illuminate/contracts": "^12.40|^13.0", + "illuminate/events": "^12.40|^13.0", + "illuminate/filesystem": "^12.40|^13.0", + "illuminate/notifications": "^12.40|^13.0", + "illuminate/support": "^12.40|^13.0", + "league/flysystem": "^3.30.2", + "php": "^8.3", + "spatie/db-dumper": "^4.0", + "spatie/laravel-package-tools": "^1.92.7", + "spatie/laravel-signal-aware-command": "^2.1", + "spatie/temporary-directory": "^2.3", + "symfony/console": "^7.3.6|^8.0", + "symfony/finder": "^7.3.5|^8.0" }, - "provide": { - "ext-mbstring": "*" + "require-dev": { + "composer-runtime-api": "^2.0", + "ext-pcntl": "*", + "larastan/larastan": "^3.8", + "laravel/slack-notification-channel": "^3.7", + "league/flysystem-aws-s3-v3": "^3.30.1", + "mockery/mockery": "^1.6.12", + "orchestra/testbench": "^10.8|^11.0", + "pestphp/pest": "^4.1.5", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan-deprecation-rules": "^2.0.3", + "phpstan/phpstan-phpunit": "^2.0.8", + "rector/rector": "^2.2.8" }, "suggest": { - "ext-mbstring": "For best performance" + "laravel/slack-notification-channel": "Required for sending notifications via Slack" }, "type": "library", "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" + "laravel": { + "providers": [ + "Spatie\\Backup\\BackupServiceProvider" + ] } }, "autoload": { "files": [ - "bootstrap.php" + "src/Helpers/functions.php" ], "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" + "Spatie\\Backup\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -7600,80 +8172,67 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" } ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", + "description": "A Laravel package to backup your application", + "homepage": "https://github.com/spatie/laravel-backup", "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" + "backup", + "database", + "laravel-backup", + "spatie" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + "issues": "https://github.com/spatie/laravel-backup/issues", + "source": "https://github.com/spatie/laravel-backup/tree/10.2.1" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", + "url": "https://github.com/sponsors/spatie", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" + "url": "https://spatie.be/open-source/support-us", + "type": "other" } ], - "time": "2024-12-23T08:48:59+00:00" + "time": "2026-03-24T10:30:33+00:00" }, { - "name": "symfony/polyfill-php80", - "version": "v1.33.0", + "name": "spatie/laravel-package-tools", + "version": "1.93.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + "url": "https://github.com/spatie/laravel-package-tools.git", + "reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/0d097bce95b2bf6802fb1d83e1e753b0f5a948e7", + "reference": "0d097bce95b2bf6802fb1d83e1e753b0f5a948e7", "shasum": "" }, "require": { - "php": ">=7.2" + "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0", + "php": "^8.1" }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } + "require-dev": { + "mockery/mockery": "^1.5", + "orchestra/testbench": "^8.0|^9.2|^10.0|^11.0", + "pestphp/pest": "^2.1|^3.1|^4.0", + "phpunit/php-code-coverage": "^10.0|^11.0|^12.0", + "phpunit/phpunit": "^10.5|^11.5|^12.5", + "spatie/pest-plugin-test-time": "^2.2|^3.0" }, + "type": "library", "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, - "classmap": [ - "Resources/stubs" - ] + "Spatie\\LaravelPackageTools\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -7681,83 +8240,73 @@ ], "authors": [ { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", - "homepage": "https://symfony.com", + "description": "Tools for creating Laravel packages", + "homepage": "https://github.com/spatie/laravel-package-tools", "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" + "laravel-package-tools", + "spatie" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + "issues": "https://github.com/spatie/laravel-package-tools/issues", + "source": "https://github.com/spatie/laravel-package-tools/tree/1.93.0" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", + "url": "https://github.com/spatie", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" } ], - "time": "2025-01-02T08:10:11+00:00" + "time": "2026-02-21T12:49:54+00:00" }, { - "name": "symfony/polyfill-php83", - "version": "v1.33.0", + "name": "spatie/laravel-signal-aware-command", + "version": "2.1.2", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + "url": "https://github.com/spatie/laravel-signal-aware-command.git", + "reference": "54dcc1efd152bfb3eb0faf56a5fc28569b864b5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", - "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "url": "https://api.github.com/repos/spatie/laravel-signal-aware-command/zipball/54dcc1efd152bfb3eb0faf56a5fc28569b864b5d", + "reference": "54dcc1efd152bfb3eb0faf56a5fc28569b864b5d", "shasum": "" }, "require": { - "php": ">=7.2" + "illuminate/contracts": "^11.0|^12.0|^13.0", + "php": "^8.2", + "spatie/laravel-package-tools": "^1.4.3", + "symfony/console": "^7.0|^8.0" + }, + "require-dev": { + "brianium/paratest": "^6.2|^7.0", + "ext-pcntl": "*", + "nunomaduro/collision": "^5.3|^6.0|^7.0|^8.0", + "orchestra/testbench": "^9.0|^10.0", + "pestphp/pest-plugin-laravel": "^1.3|^2.0|^3.0", + "phpunit/phpunit": "^9.5|^10|^11", + "spatie/laravel-ray": "^1.17" }, "type": "library", "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" + "laravel": { + "aliases": { + "Signal": "Spatie\\SignalAwareCommand\\Facades\\Signal" + }, + "providers": [ + "Spatie\\SignalAwareCommand\\SignalAwareCommandServiceProvider" + ] } }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Php83\\": "" - }, - "classmap": [ - "Resources/stubs" - ] + "Spatie\\SignalAwareCommand\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -7765,79 +8314,55 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" } ], - "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", - "homepage": "https://symfony.com", + "description": "Handle signals in artisan commands", + "homepage": "https://github.com/spatie/laravel-signal-aware-command", "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" + "laravel", + "laravel-signal-aware-command", + "spatie" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + "issues": "https://github.com/spatie/laravel-signal-aware-command/issues", + "source": "https://github.com/spatie/laravel-signal-aware-command/tree/2.1.2" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", + "url": "https://github.com/spatie", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" } ], - "time": "2025-07-08T02:45:35+00:00" + "time": "2026-02-22T08:16:31+00:00" }, { - "name": "symfony/polyfill-php84", - "version": "v1.33.0", + "name": "spatie/temporary-directory", + "version": "2.3.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + "url": "https://github.com/spatie/temporary-directory.git", + "reference": "662e481d6ec07ef29fd05010433428851a42cd07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/662e481d6ec07ef29fd05010433428851a42cd07", + "reference": "662e481d6ec07ef29fd05010433428851a42cd07", "shasum": "" }, "require": { - "php": ">=7.2" + "php": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" }, "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Php84\\": "" - }, - "classmap": [ - "Resources/stubs" - ] + "Spatie\\TemporaryDirectory\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -7845,162 +8370,119 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Alex Vanderbist", + "email": "alex@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" } ], - "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", - "homepage": "https://symfony.com", + "description": "Easily create, use and destroy temporary directories", + "homepage": "https://github.com/spatie/temporary-directory", "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" + "php", + "spatie", + "temporary-directory" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + "issues": "https://github.com/spatie/temporary-directory/issues", + "source": "https://github.com/spatie/temporary-directory/tree/2.3.1" }, "funding": [ { - "url": "https://symfony.com/sponsor", + "url": "https://spatie.be/open-source/support-us", "type": "custom" }, { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", + "url": "https://github.com/spatie", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" } ], - "time": "2025-06-24T13:30:11+00:00" + "time": "2026-01-12T07:42:22+00:00" }, { - "name": "symfony/polyfill-php85", - "version": "v1.33.0", + "name": "staabm/side-effects-detector", + "version": "1.0.5", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", - "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", "shasum": "" }, "require": { - "php": ">=7.2" + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" }, + "type": "library", "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php85\\": "" - }, "classmap": [ - "Resources/stubs" + "lib/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", - "homepage": "https://symfony.com", + "description": "A static analysis tool to detect side effects in PHP code", "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" + "static analysis" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" }, "funding": [ { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", + "url": "https://github.com/staabm", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" } ], - "time": "2025-06-23T16:12:55+00:00" + "time": "2024-10-20T05:08:20+00:00" }, { - "name": "symfony/polyfill-uuid", - "version": "v1.33.0", + "name": "symfony/clock", + "version": "v8.0.8", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-uuid.git", - "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" + "url": "https://github.com/symfony/clock.git", + "reference": "b55a638b189a6faa875e0ccdb00908fb87af95b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", - "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "url": "https://api.github.com/repos/symfony/clock/zipball/b55a638b189a6faa875e0ccdb00908fb87af95b3", + "reference": "b55a638b189a6faa875e0ccdb00908fb87af95b3", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=8.4", + "psr/clock": "^1.0" }, "provide": { - "ext-uuid": "*" - }, - "suggest": { - "ext-uuid": "For best performance" + "psr/clock-implementation": "1.0" }, "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, "autoload": { "files": [ - "bootstrap.php" + "Resources/now.php" ], "psr-4": { - "Symfony\\Polyfill\\Uuid\\": "" - } + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -8008,24 +8490,23 @@ ], "authors": [ { - "name": "Grégoire Pineau", - "email": "lyrixx@lyrixx.info" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for uuid functions", + "description": "Decouples applications from the system clock", "homepage": "https://symfony.com", "keywords": [ - "compatibility", - "polyfill", - "portable", - "uuid" + "clock", + "psr20", + "time" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0" + "source": "https://github.com/symfony/clock/tree/v8.0.8" }, "funding": [ { @@ -8045,29 +8526,48 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-03-30T15:14:47+00:00" }, { - "name": "symfony/process", - "version": "v7.4.5", + "name": "symfony/console", + "version": "v8.0.9", "source": { "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "608476f4604102976d687c483ac63a79ba18cc97" + "url": "https://github.com/symfony/console.git", + "reference": "7113778e2e91f4709cb3194a75dfa9c0d028d94d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", - "reference": "608476f4604102976d687c483ac63a79ba18cc97", + "url": "https://api.github.com/repos/symfony/console/zipball/7113778e2e91f4709cb3194a75dfa9c0d028d94d", + "reference": "7113778e2e91f4709cb3194a75dfa9c0d028d94d", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.4|^8.0" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Process\\": "" + "Symfony\\Component\\Console\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -8087,10 +8587,16 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Executes commands in sub-processes", + "description": "Eases the creation of beautiful and testable command line interfaces", "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], "support": { - "source": "https://github.com/symfony/process/tree/v7.4.5" + "source": "https://github.com/symfony/console/tree/v8.0.9" }, "funding": [ { @@ -8110,43 +8616,29 @@ "type": "tidelift" } ], - "time": "2026-01-26T15:07:59+00:00" + "time": "2026-04-29T15:02:55+00:00" }, { - "name": "symfony/routing", - "version": "v7.4.6", + "name": "symfony/css-selector", + "version": "v8.0.9", "source": { "type": "git", - "url": "https://github.com/symfony/routing.git", - "reference": "238d749c56b804b31a9bf3e26519d93b65a60938" + "url": "https://github.com/symfony/css-selector.git", + "reference": "3665cfade90565430909b906394c73c8739e57d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/238d749c56b804b31a9bf3e26519d93b65a60938", - "reference": "238d749c56b804b31a9bf3e26519d93b65a60938", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/3665cfade90565430909b906394c73c8739e57d0", + "reference": "3665cfade90565430909b906394c73c8739e57d0", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3" - }, - "conflict": { - "symfony/config": "<6.4", - "symfony/dependency-injection": "<6.4", - "symfony/yaml": "<6.4" - }, - "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "symfony/expression-language": "^6.4|^7.0|^8.0", - "symfony/http-foundation": "^6.4|^7.0|^8.0", - "symfony/yaml": "^6.4|^7.0|^8.0" + "php": ">=8.4" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Routing\\": "" + "Symfony\\Component\\CssSelector\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -8161,21 +8653,19 @@ "name": "Fabien Potencier", "email": "fabien@symfony.com" }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Maps an HTTP request to a set of configuration variables", + "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", - "keywords": [ - "router", - "routing", - "uri", - "url" - ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.4.6" + "source": "https://github.com/symfony/css-selector/tree/v8.0.9" }, "funding": [ { @@ -8195,29 +8685,24 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:50:00+00:00" + "time": "2026-04-18T13:51:42+00:00" }, { - "name": "symfony/service-contracts", - "version": "v3.6.1", + "name": "symfony/deprecation-contracts", + "version": "v3.7.0", "source": { "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { - "php": ">=8.1", - "psr/container": "^1.1|^2.0", - "symfony/deprecation-contracts": "^2.5|^3" - }, - "conflict": { - "ext-psr": "<1.1|>=2" + "php": ">=8.1" }, "type": "library", "extra": { @@ -8226,15 +8711,12 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { - "psr-4": { - "Symfony\\Contracts\\Service\\": "" - }, - "exclude-from-classmap": [ - "/Test/" + "files": [ + "function.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -8251,18 +8733,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to writing services", + "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -8282,46 +8756,45 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:30:57+00:00" + "time": "2026-04-13T15:52:40+00:00" }, { - "name": "symfony/string", - "version": "v8.0.6", + "name": "symfony/error-handler", + "version": "v8.0.8", "source": { "type": "git", - "url": "https://github.com/symfony/string.git", - "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" + "url": "https://github.com/symfony/error-handler.git", + "reference": "c1119fe8dcfc3825ec74ec061b96ef0c8f281517" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", - "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/c1119fe8dcfc3825ec74ec061b96ef0c8f281517", + "reference": "c1119fe8dcfc3825ec74ec061b96ef0c8f281517", "shasum": "" }, "require": { "php": ">=8.4", - "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-intl-grapheme": "^1.33", - "symfony/polyfill-intl-normalizer": "^1.0", - "symfony/polyfill-mbstring": "^1.0" + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^7.4|^8.0" }, "conflict": { - "symfony/translation-contracts": "<2.5" + "symfony/deprecation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.4|^8.0", - "symfony/http-client": "^7.4|^8.0", - "symfony/intl": "^7.4|^8.0", - "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^7.4|^8.0" + "symfony/console": "^7.4|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], "type": "library", "autoload": { - "files": [ - "Resources/functions.php" - ], "psr-4": { - "Symfony\\Component\\String\\": "" + "Symfony\\Component\\ErrorHandler\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -8333,26 +8806,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", - "keywords": [ - "grapheme", - "i18n", - "string", - "unicode", - "utf-8", - "utf8" - ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.6" + "source": "https://github.com/symfony/error-handler/tree/v8.0.8" }, "funding": [ { @@ -8372,57 +8837,49 @@ "type": "tidelift" } ], - "time": "2026-02-09T10:14:57+00:00" + "time": "2026-03-30T15:14:47+00:00" }, { - "name": "symfony/translation", - "version": "v8.0.6", + "name": "symfony/event-dispatcher", + "version": "v8.0.9", "source": { "type": "git", - "url": "https://github.com/symfony/translation.git", - "reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b" + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "0c3c1a17604c4dbbec4b93fe162c538482096e1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b", - "reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0c3c1a17604c4dbbec4b93fe162c538482096e1f", + "reference": "0c3c1a17604c4dbbec4b93fe162c538482096e1f", "shasum": "" }, "require": { "php": ">=8.4", - "symfony/polyfill-mbstring": "^1.0", - "symfony/translation-contracts": "^3.6.1" + "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "nikic/php-parser": "<5.0", - "symfony/http-client-contracts": "<2.5", + "symfony/security-http": "<7.4", "symfony/service-contracts": "<2.5" }, "provide": { - "symfony/translation-implementation": "2.3|3.0" + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" }, "require-dev": { - "nikic/php-parser": "^5.0", "psr/log": "^1|^2|^3", "symfony/config": "^7.4|^8.0", - "symfony/console": "^7.4|^8.0", "symfony/dependency-injection": "^7.4|^8.0", - "symfony/finder": "^7.4|^8.0", - "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^7.4|^8.0", - "symfony/intl": "^7.4|^8.0", - "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^7.4|^8.0" + "symfony/stopwatch": "^7.4|^8.0" }, "type": "library", "autoload": { - "files": [ - "Resources/functions.php" - ], "psr-4": { - "Symfony\\Component\\Translation\\": "" + "Symfony\\Component\\EventDispatcher\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -8442,10 +8899,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides tools to internationalize your application", + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v8.0.6" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.9" }, "funding": [ { @@ -8465,24 +8922,25 @@ "type": "tidelift" } ], - "time": "2026-02-17T13:07:04+00:00" + "time": "2026-04-18T13:51:42+00:00" }, { - "name": "symfony/translation-contracts", - "version": "v3.6.1", + "name": "symfony/event-dispatcher-contracts", + "version": "v3.7.0", "source": { "type": "git", - "url": "https://github.com/symfony/translation-contracts.git", - "reference": "65a8bc82080447fae78373aa10f8d13b38338977" + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", - "reference": "65a8bc82080447fae78373aa10f8d13b38338977", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.1", + "psr/event-dispatcher": "^1" }, "type": "library", "extra": { @@ -8491,16 +8949,13 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { "psr-4": { - "Symfony\\Contracts\\Translation\\": "" - }, - "exclude-from-classmap": [ - "/Test/" - ] + "Symfony\\Contracts\\EventDispatcher\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -8516,7 +8971,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to translation", + "description": "Generic abstractions related to dispatching event", "homepage": "https://symfony.com", "keywords": [ "abstractions", @@ -8527,7 +8982,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" }, "funding": [ { @@ -8547,33 +9002,34 @@ "type": "tidelift" } ], - "time": "2025-07-15T13:41:35+00:00" + "time": "2026-01-05T13:30:16+00:00" }, { - "name": "symfony/uid", - "version": "v7.4.4", + "name": "symfony/filesystem", + "version": "v8.0.9", "source": { "type": "git", - "url": "https://github.com/symfony/uid.git", - "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36" + "url": "https://github.com/symfony/filesystem.git", + "reference": "d1ec4543d5c6c2dac78503c2fae5ea0b3608ce40" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/7719ce8aba76be93dfe249192f1fbfa52c588e36", - "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d1ec4543d5c6c2dac78503c2fae5ea0b3608ce40", + "reference": "d1ec4543d5c6c2dac78503c2fae5ea0b3608ce40", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-uuid": "^1.15" + "php": ">=8.4", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/console": "^6.4|^7.0|^8.0" + "symfony/process": "^7.4|^8.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Uid\\": "" + "Symfony\\Component\\Filesystem\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -8585,27 +9041,18 @@ ], "authors": [ { - "name": "Grégoire Pineau", - "email": "lyrixx@lyrixx.info" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides an object-oriented API to generate and represent UIDs", + "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", - "keywords": [ - "UID", - "ulid", - "uuid" - ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.4.4" + "source": "https://github.com/symfony/filesystem/tree/v8.0.9" }, "funding": [ { @@ -8625,47 +9072,32 @@ "type": "tidelift" } ], - "time": "2026-01-03T23:30:35+00:00" + "time": "2026-04-18T13:51:42+00:00" }, { - "name": "symfony/var-dumper", - "version": "v7.4.6", + "name": "symfony/finder", + "version": "v8.0.8", "source": { "type": "git", - "url": "https://github.com/symfony/var-dumper.git", - "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291" + "url": "https://github.com/symfony/finder.git", + "reference": "8da41214757b87d97f181e3d14a4179286151007" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/045321c440ac18347b136c63d2e9bf28a2dc0291", - "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291", + "url": "https://api.github.com/repos/symfony/finder/zipball/8da41214757b87d97f181e3d14a4179286151007", + "reference": "8da41214757b87d97f181e3d14a4179286151007", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0" - }, - "conflict": { - "symfony/console": "<6.4" + "php": ">=8.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0|^8.0", - "symfony/http-kernel": "^6.4|^7.0|^8.0", - "symfony/process": "^6.4|^7.0|^8.0", - "symfony/uid": "^6.4|^7.0|^8.0", - "twig/twig": "^3.12" + "symfony/filesystem": "^7.4|^8.0" }, - "bin": [ - "Resources/bin/var-dump-server" - ], "type": "library", "autoload": { - "files": [ - "Resources/functions/dump.php" - ], "psr-4": { - "Symfony\\Component\\VarDumper\\": "" + "Symfony\\Component\\Finder\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -8677,22 +9109,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", - "keywords": [ - "debug", - "dump" - ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.4.6" + "source": "https://github.com/symfony/finder/tree/v8.0.8" }, "funding": [ { @@ -8712,175 +9140,232 @@ "type": "tidelift" } ], - "time": "2026-02-15T10:53:20+00:00" + "time": "2026-03-30T15:14:47+00:00" }, { - "name": "tijsverkoyen/css-to-inline-styles", - "version": "v2.4.0", + "name": "symfony/http-foundation", + "version": "v8.0.8", "source": { "type": "git", - "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", - "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + "url": "https://github.com/symfony/http-foundation.git", + "reference": "02656f7ebeae5c155d659e946f6b3a33df24051b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", - "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/02656f7ebeae5c155d659e946f6b3a33df24051b", + "reference": "02656f7ebeae5c155d659e946f6b3a33df24051b", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-libxml": "*", - "php": "^7.4 || ^8.0", - "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<4.3" }, "require-dev": { - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^8.5.21 || ^9.5.10" + "doctrine/dbal": "^4.3", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, "autoload": { "psr-4": { - "TijsVerkoyen\\CssToInlineStyles\\": "src" - } + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Tijs Verkoyen", - "email": "css_to_inline_styles@verkoyen.eu", - "role": "Developer" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", - "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", "support": { - "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", - "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" + "source": "https://github.com/symfony/http-foundation/tree/v8.0.8" }, - "time": "2025-12-02T11:56:42+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T15:14:47+00:00" }, { - "name": "vlucas/phpdotenv", - "version": "v5.6.3", + "name": "symfony/http-kernel", + "version": "v8.0.10", "source": { "type": "git", - "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "955e7815d677a3eaa7075231212f2110983adecc" + "url": "https://github.com/symfony/http-kernel.git", + "reference": "fb3f65b3d4ca2dad31c80d323819a762ca31d6ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", - "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/fb3f65b3d4ca2dad31c80d323819a762ca31d6ac", + "reference": "fb3f65b3d4ca2dad31c80d323819a762ca31d6ac", "shasum": "" }, "require": { - "ext-pcre": "*", - "graham-campbell/result-type": "^1.1.4", - "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.5", - "symfony/polyfill-ctype": "^1.26", - "symfony/polyfill-mbstring": "^1.26", - "symfony/polyfill-php80": "^1.26" + "php": ">=8.4", + "psr/log": "^1|^2|^3", + "symfony/error-handler": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.2", - "ext-filter": "*", - "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + "conflict": { + "symfony/flex": "<2.10", + "symfony/http-client-contracts": "<2.5", + "symfony/translation-contracts": "<2.5", + "twig/twig": "<3.21" }, - "suggest": { - "ext-filter": "Required to use the boolean validator." + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" }, - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - }, - "branch-alias": { - "dev-master": "5.6-dev" - } + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/css-selector": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/dom-crawler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/routing": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0", + "twig/twig": "^3.21" }, + "type": "library", "autoload": { "psr-4": { - "Dotenv\\": "src/" - } + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { - "name": "Vance Lucas", - "email": "vance@vancelucas.com", - "homepage": "https://github.com/vlucas" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", - "keywords": [ - "dotenv", - "env", - "environment" - ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", "support": { - "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + "source": "https://github.com/symfony/http-kernel/tree/v8.0.10" }, "funding": [ { - "url": "https://github.com/GrahamCampbell", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-12-27T19:49:13+00:00" + "time": "2026-05-06T12:27:31+00:00" }, { - "name": "voku/portable-ascii", - "version": "2.0.3", + "name": "symfony/mailer", + "version": "v8.0.8", "source": { "type": "git", - "url": "https://github.com/voku/portable-ascii.git", - "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + "url": "https://github.com/symfony/mailer.git", + "reference": "ca5f6edaf8780ece814404b58a4482b22b509c56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", - "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "url": "https://api.github.com/repos/symfony/mailer/zipball/ca5f6edaf8780ece814404b58a4482b22b509c56", + "reference": "ca5f6edaf8780ece814404b58a4482b22b509c56", "shasum": "" }, "require": { - "php": ">=7.0.0" + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.4", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3" }, - "require-dev": { - "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + "conflict": { + "symfony/http-client-contracts": "<2.5" }, - "suggest": { - "ext-intl": "Use Intl for transliterator_transliterate() support" + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/twig-bridge": "^7.4|^8.0" }, "type": "library", "autoload": { "psr-4": { - "voku\\": "src/voku/" - } + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -8888,88 +9373,81 @@ ], "authors": [ { - "name": "Lars Moelleken", - "homepage": "https://www.moelleken.org/" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", - "homepage": "https://github.com/voku/portable-ascii", - "keywords": [ - "ascii", - "clean", - "php" - ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", "support": { - "issues": "https://github.com/voku/portable-ascii/issues", - "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + "source": "https://github.com/symfony/mailer/tree/v8.0.8" }, "funding": [ { - "url": "https://www.paypal.me/moelleken", + "url": "https://symfony.com/sponsor", "type": "custom" }, { - "url": "https://github.com/voku", + "url": "https://github.com/fabpot", "type": "github" }, { - "url": "https://opencollective.com/portable-ascii", - "type": "open_collective" - }, - { - "url": "https://www.patreon.com/voku", - "type": "patreon" + "url": "https://github.com/nicolas-grekas", + "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-21T01:49:47+00:00" - } - ], - "packages-dev": [ + "time": "2026-03-30T15:14:47+00:00" + }, { - "name": "fakerphp/faker", - "version": "v1.24.1", + "name": "symfony/mime", + "version": "v8.0.9", "source": { "type": "git", - "url": "https://github.com/FakerPHP/Faker.git", - "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + "url": "https://github.com/symfony/mime.git", + "reference": "a9fcb293650c054b62a5b406f4e92e7b711ea333" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", - "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "url": "https://api.github.com/repos/symfony/mime/zipball/a9fcb293650c054b62a5b406f4e92e7b711ea333", + "reference": "a9fcb293650c054b62a5b406f4e92e7b711ea333", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0", - "psr/container": "^1.0 || ^2.0", - "symfony/deprecation-contracts": "^2.2 || ^3.0" + "php": ">=8.4", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { - "fzaninotto/faker": "*" + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4.1", - "doctrine/persistence": "^1.3 || ^2.0", - "ext-intl": "*", - "phpunit/phpunit": "^9.5.26", - "symfony/phpunit-bridge": "^5.4.16" - }, - "suggest": { - "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", - "ext-curl": "Required by Faker\\Provider\\Image to download images.", - "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", - "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", - "ext-mbstring": "Required for multibyte Unicode string functionality." + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/property-info": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0" }, "type": "library", "autoload": { "psr-4": { - "Faker\\": "src/Faker/" - } + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -8977,57 +9455,79 @@ ], "authors": [ { - "name": "François Zaninotto" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Faker is a PHP library that generates fake data for you.", + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", "keywords": [ - "data", - "faker", - "fixtures" + "mime", + "mime-type" ], "support": { - "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + "source": "https://github.com/symfony/mime/tree/v8.0.9" }, - "time": "2024-11-21T13:46:39+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-29T15:02:55+00:00" }, { - "name": "filp/whoops", - "version": "2.18.4", + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", "source": { "type": "git", - "url": "https://github.com/filp/whoops.git", - "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", - "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0", - "psr/log": "^1.0.1 || ^2.0 || ^3.0" + "php": ">=7.2" }, - "require-dev": { - "mockery/mockery": "^1.0", - "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", - "symfony/var-dumper": "^4.0 || ^5.0" + "provide": { + "ext-ctype": "*" }, "suggest": { - "symfony/var-dumper": "Pretty print complex values better with var-dumper available", - "whoops/soap": "Formats errors as SOAP responses" + "ext-ctype": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.7-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Whoops\\": "src/Whoops/" + "Symfony\\Polyfill\\Ctype\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -9036,124 +9536,161 @@ ], "authors": [ { - "name": "Filipe Dobreira", - "homepage": "https://github.com/filp", - "role": "Developer" + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "php error handling for cool kids", - "homepage": "https://filp.github.io/whoops/", + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", "keywords": [ - "error", - "exception", - "handling", - "library", - "throwable", - "whoops" + "compatibility", + "ctype", + "polyfill", + "portable" ], "support": { - "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.18.4" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { - "url": "https://github.com/denis-sokolov", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2025-08-08T12:00:00+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { - "name": "hamcrest/hamcrest-php", - "version": "v2.1.1", + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.37.0", "source": { "type": "git", - "url": "https://github.com/hamcrest/hamcrest-php.git", - "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", - "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e", + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e", "shasum": "" }, "require": { - "php": "^7.4|^8.0" - }, - "replace": { - "cordoval/hamcrest-php": "*", - "davedevelopment/hamcrest-php": "*", - "kodova/hamcrest-php": "*" + "php": ">=7.2" }, - "require-dev": { - "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", - "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + "suggest": { + "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.1-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "classmap": [ - "hamcrest" - ] + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], - "description": "This is the PHP port of Hamcrest Matchers", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", "keywords": [ - "test" + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" ], "support": { - "issues": "https://github.com/hamcrest/hamcrest-php/issues", - "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0" }, - "time": "2025-04-30T06:54:44+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-26T13:13:48+00:00" }, { - "name": "laravel/pint", - "version": "v1.28.0", + "name": "symfony/polyfill-intl-idn", + "version": "v1.37.0", "source": { "type": "git", - "url": "https://github.com/laravel/pint.git", - "reference": "1feae84bf9c1649d99ba8f7b8193bf0f09f04cc9" + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/1feae84bf9c1649d99ba8f7b8193bf0f09f04cc9", - "reference": "1feae84bf9c1649d99ba8f7b8193bf0f09f04cc9", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", "shasum": "" }, "require": { - "ext-json": "*", - "ext-mbstring": "*", - "ext-tokenizer": "*", - "ext-xml": "*", - "php": "^8.2.0" + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.94.2", - "illuminate/view": "^12.54.1", - "larastan/larastan": "^3.9.3", - "laravel-zero/framework": "^12.0.5", - "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.4.0", - "pestphp/pest": "^3.8.5", - "shipfastlabs/agent-detector": "^1.0.2" + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } }, - "bin": [ - "builds/pint" - ], - "type": "project", "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "App\\": "app/", - "Database\\Seeders\\": "database/seeders/", - "Database\\Factories\\": "database/factories/" + "Symfony\\Polyfill\\Intl\\Idn\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -9162,67 +9699,88 @@ ], "authors": [ { - "name": "Nuno Maduro", - "email": "enunomaduro@gmail.com" + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "An opinionated code formatter for PHP.", - "homepage": "https://laravel.com", + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", "keywords": [ - "dev", - "format", - "formatter", - "lint", - "linter", - "php" + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" ], "support": { - "issues": "https://github.com/laravel/pint/issues", - "source": "https://github.com/laravel/pint" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.37.0" }, - "time": "2026-03-10T20:37:18+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" }, { - "name": "laravel/sail", - "version": "v1.53.0", + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.37.0", "source": { "type": "git", - "url": "https://github.com/laravel/sail.git", - "reference": "e340eaa2bea9b99192570c48ed837155dbf24fbb" + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/e340eaa2bea9b99192570c48ed837155dbf24fbb", - "reference": "e340eaa2bea9b99192570c48ed837155dbf24fbb", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0|^13.0", - "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0|^13.0", - "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0|^13.0", - "php": "^8.0", - "symfony/console": "^6.0|^7.0|^8.0", - "symfony/yaml": "^6.0|^7.0|^8.0" + "php": ">=7.2" }, - "require-dev": { - "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0|^11.0", - "phpstan/phpstan": "^2.0" + "suggest": { + "ext-intl": "For best performance" }, - "bin": [ - "bin/sail" - ], "type": "library", "extra": { - "laravel": { - "providers": [ - "Laravel\\Sail\\SailServiceProvider" - ] + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Laravel\\Sail\\": "src/" - } + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -9230,215 +9788,250 @@ ], "authors": [ { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Docker files for running a basic Laravel application.", + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", "keywords": [ - "docker", - "laravel" + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" ], "support": { - "issues": "https://github.com/laravel/sail/issues", - "source": "https://github.com/laravel/sail" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0" }, - "time": "2026-02-06T12:16:02+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "mockery/mockery", - "version": "1.6.12", + "name": "symfony/polyfill-mbstring", + "version": "v1.37.0", "source": { "type": "git", - "url": "https://github.com/mockery/mockery.git", - "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", - "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", "shasum": "" }, "require": { - "hamcrest/hamcrest-php": "^2.0.1", - "lib-pcre": ">=7.0", - "php": ">=7.3" + "ext-iconv": "*", + "php": ">=7.2" }, - "conflict": { - "phpunit/phpunit": "<8.0" + "provide": { + "ext-mbstring": "*" }, - "require-dev": { - "phpunit/phpunit": "^8.5 || ^9.6.17", - "symplify/easy-coding-standard": "^12.1.14" + "suggest": { + "ext-mbstring": "For best performance" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { "files": [ - "library/helpers.php", - "library/Mockery.php" + "bootstrap.php" ], "psr-4": { - "Mockery\\": "library/Mockery" + "Symfony\\Polyfill\\Mbstring\\": "" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Pádraic Brady", - "email": "padraic.brady@gmail.com", - "homepage": "https://github.com/padraic", - "role": "Author" - }, - { - "name": "Dave Marshall", - "email": "dave.marshall@atstsolutions.co.uk", - "homepage": "https://davedevelopment.co.uk", - "role": "Developer" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { - "name": "Nathanael Esayeas", - "email": "nathanael.esayeas@protonmail.com", - "homepage": "https://github.com/ghostwriter", - "role": "Lead Developer" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Mockery is a simple yet flexible PHP mock object framework", - "homepage": "https://github.com/mockery/mockery", + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", "keywords": [ - "BDD", - "TDD", - "library", - "mock", - "mock objects", - "mockery", - "stub", - "test", - "test double", - "testing" + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" ], "support": { - "docs": "https://docs.mockery.io/", - "issues": "https://github.com/mockery/mockery/issues", - "rss": "https://github.com/mockery/mockery/releases.atom", - "security": "https://github.com/mockery/mockery/security/advisories", - "source": "https://github.com/mockery/mockery" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" }, - "time": "2024-05-16T03:13:13+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T17:25:58+00:00" }, { - "name": "myclabs/deep-copy", - "version": "1.13.4", + "name": "symfony/polyfill-php80", + "version": "v1.37.0", "source": { "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", - "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3 <3.2.2" - }, - "require-dev": { - "doctrine/collections": "^1.6.8", - "doctrine/common": "^2.13.3 || ^3.2.2", - "phpspec/prophecy": "^1.10", - "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + "php": ">=7.2" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { "files": [ - "src/DeepCopy/deep_copy.php" + "bootstrap.php" ], "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - } + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Create deep copies (clones) of your objects", + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" + "compatibility", + "polyfill", + "portable", + "shim" ], "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" }, "funding": [ { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-08-01T08:46:24+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { - "name": "nunomaduro/collision", - "version": "v8.9.1", + "name": "symfony/polyfill-php84", + "version": "v1.37.0", "source": { "type": "git", - "url": "https://github.com/nunomaduro/collision.git", - "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935" + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/a1ed3fa530fd60bc515f9303e8520fcb7d4bd935", - "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06", "shasum": "" }, "require": { - "filp/whoops": "^2.18.4", - "nunomaduro/termwind": "^2.4.0", - "php": "^8.2.0", - "symfony/console": "^7.4.4 || ^8.0.4" - }, - "conflict": { - "laravel/framework": "<11.48.0 || >=14.0.0", - "phpunit/phpunit": "<11.5.50 || >=14.0.0" - }, - "require-dev": { - "brianium/paratest": "^7.8.5", - "larastan/larastan": "^3.9.2", - "laravel/framework": "^11.48.0 || ^12.52.0", - "laravel/pint": "^1.27.1", - "orchestra/testbench-core": "^9.12.0 || ^10.9.0", - "pestphp/pest": "^3.8.5 || ^4.4.1 || ^5.0.0", - "sebastian/environment": "^7.2.1 || ^8.0.3 || ^9.0.0" + "php": ">=7.2" }, "type": "library", "extra": { - "laravel": { - "providers": [ - "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" - ] - }, - "branch-alias": { - "dev-8.x": "8.x-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { "files": [ - "./src/Adapters/Phpunit/Autoload.php" + "bootstrap.php" ], "psr-4": { - "NunoMaduro\\Collision\\": "src/" - } + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -9446,822 +10039,971 @@ ], "authors": [ { - "name": "Nuno Maduro", - "email": "enunomaduro@gmail.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Cli error handling for console/command-line PHP applications.", + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", "keywords": [ - "artisan", - "cli", - "command-line", - "console", - "dev", - "error", - "handling", - "laravel", - "laravel-zero", - "php", - "symfony" + "compatibility", + "polyfill", + "portable", + "shim" ], "support": { - "issues": "https://github.com/nunomaduro/collision/issues", - "source": "https://github.com/nunomaduro/collision" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.37.0" }, "funding": [ { - "url": "https://www.paypal.com/paypalme/enunomaduro", + "url": "https://symfony.com/sponsor", "type": "custom" }, { - "url": "https://github.com/nunomaduro", + "url": "https://github.com/fabpot", "type": "github" }, { - "url": "https://www.patreon.com/nunomaduro", - "type": "patreon" + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2026-02-17T17:33:08+00:00" + "time": "2026-04-10T18:47:49+00:00" }, { - "name": "phar-io/manifest", - "version": "2.0.4", + "name": "symfony/polyfill-php85", + "version": "v1.37.0", "source": { "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "54750ef60c58e43759730615a392c31c80e23176" + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", - "reference": "54750ef60c58e43759730615a392c31c80e23176", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/fcfa4973a9917cef23f2e38774da74a2b7d115ee", + "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-phar": "*", - "ext-xmlwriter": "*", - "phar-io/version": "^3.0.1", - "php": "^7.2 || ^8.0" + "php": ">=7.2" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, "classmap": [ - "src/" + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], "support": { - "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.4" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.37.0" }, "funding": [ { - "url": "https://github.com/theseer", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2024-03-03T12:33:53+00:00" + "time": "2026-04-26T13:10:57+00:00" }, { - "name": "phar-io/version", - "version": "3.2.1", + "name": "symfony/polyfill-php86", + "version": "v1.37.0", "source": { "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + "url": "https://github.com/symfony/polyfill-php86.git", + "reference": "33d8fc5a705481e21fe3a81212b26f9b1f61749c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "url": "https://api.github.com/repos/symfony/polyfill-php86/zipball/33d8fc5a705481e21fe3a81212b26f9b1f61749c", + "reference": "33d8fc5a705481e21fe3a81212b26f9b1f61749c", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": ">=7.2" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php86\\": "" + }, "classmap": [ - "src/" + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Library for handling version information and constraints", + "description": "Symfony polyfill backporting some PHP 8.6+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], "support": { - "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.2.1" + "source": "https://github.com/symfony/polyfill-php86/tree/v1.37.0" }, - "time": "2022-02-21T01:04:05+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-26T13:13:48+00:00" }, { - "name": "phpunit/php-code-coverage", - "version": "11.0.12", + "name": "symfony/polyfill-uuid", + "version": "v1.37.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", - "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-xmlwriter": "*", - "nikic/php-parser": "^5.7.0", - "php": ">=8.2", - "phpunit/php-file-iterator": "^5.1.0", - "phpunit/php-text-template": "^4.0.1", - "sebastian/code-unit-reverse-lookup": "^4.0.1", - "sebastian/complexity": "^4.0.1", - "sebastian/environment": "^7.2.1", - "sebastian/lines-of-code": "^3.0.1", - "sebastian/version": "^5.0.2", - "theseer/tokenizer": "^1.3.1" + "php": ">=7.2" }, - "require-dev": { - "phpunit/phpunit": "^11.5.46" + "provide": { + "ext-uuid": "*" }, "suggest": { - "ext-pcov": "PHP extension that provides line coverage", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + "ext-uuid": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "11.0.x-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { - "classmap": [ - "src/" - ] + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", "keywords": [ - "coverage", - "testing", - "xunit" + "compatibility", + "polyfill", + "portable", + "uuid" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://symfony.com/sponsor", + "type": "custom" }, { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" + "url": "https://github.com/fabpot", + "type": "github" }, { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" + "url": "https://github.com/nicolas-grekas", + "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-12-24T07:01:01+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { - "name": "phpunit/php-file-iterator", - "version": "5.1.1", + "name": "symfony/process", + "version": "v8.0.8", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + "url": "https://github.com/symfony/process.git", + "reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", - "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "url": "https://api.github.com/repos/symfony/process/zipball/cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc", + "reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc", "shasum": "" }, "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.3" + "php": ">=8.4" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "5.1-dev" - } - }, "autoload": { - "classmap": [ - "src/" + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", "support": { - "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" + "source": "https://github.com/symfony/process/tree/v8.0.8" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", - "type": "github" + "url": "https://symfony.com/sponsor", + "type": "custom" }, { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" + "url": "https://github.com/fabpot", + "type": "github" }, { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" + "url": "https://github.com/nicolas-grekas", + "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2026-02-02T13:52:54+00:00" + "time": "2026-03-30T15:14:47+00:00" }, { - "name": "phpunit/php-invoker", - "version": "5.0.1", + "name": "symfony/routing", + "version": "v8.0.9", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + "url": "https://github.com/symfony/routing.git", + "reference": "75d1bd8e5da3424e4db2fc3ff0222cb4d0c73038" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", - "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "url": "https://api.github.com/repos/symfony/routing/zipball/75d1bd8e5da3424e4db2fc3ff0222cb4d0c73038", + "reference": "75d1bd8e5da3424e4db2fc3ff0222cb4d0c73038", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4", + "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { - "ext-pcntl": "*", - "phpunit/phpunit": "^11.0" - }, - "suggest": { - "ext-pcntl": "*" + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "5.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Invoke callables with a timeout", - "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", "keywords": [ - "process" + "router", + "routing", + "uri", + "url" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + "source": "https://github.com/symfony/routing/tree/v8.0.9" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2024-07-03T05:07:44+00:00" + "time": "2026-04-29T15:02:55+00:00" }, { - "name": "phpunit/php-text-template", - "version": "4.0.1", + "name": "symfony/service-contracts", + "version": "v3.7.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + "url": "https://github.com/symfony/service-contracts.git", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", - "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, - "require-dev": { - "phpunit/phpunit": "^11.0" + "conflict": { + "ext-psr": "<1.1|>=2" }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "3.7-dev" } }, "autoload": { - "classmap": [ - "src/" + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", "keywords": [ - "template" + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2024-07-03T05:08:43+00:00" + "time": "2026-03-28T09:44:51+00:00" }, { - "name": "phpunit/php-timer", - "version": "7.0.1", + "name": "symfony/string", + "version": "v8.0.8", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + "url": "https://github.com/symfony/string.git", + "reference": "ae9488f874d7603f9d2dfbf120203882b645d963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", - "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "url": "https://api.github.com/repos/symfony/string/zipball/ae9488f874d7603f9d2dfbf120203882b645d963", + "reference": "ae9488f874d7603f9d2dfbf120203882b645d963", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "7.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", "keywords": [ - "timer" + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" ], "support": { - "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "security": "https://github.com/sebastianbergmann/php-timer/security/policy", - "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + "source": "https://github.com/symfony/string/tree/v8.0.8" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2024-07-03T05:09:35+00:00" + "time": "2026-03-30T15:14:47+00:00" }, { - "name": "phpunit/phpunit", - "version": "11.5.55", + "name": "symfony/translation", + "version": "v8.0.10", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" + "url": "https://github.com/symfony/translation.git", + "reference": "f63e9342e12646a57c91ef8a366a4f9d8e557b67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", - "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", + "url": "https://api.github.com/repos/symfony/translation/zipball/f63e9342e12646a57c91ef8a366a4f9d8e557b67", + "reference": "f63e9342e12646a57c91ef8a366a4f9d8e557b67", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.4", - "phar-io/manifest": "^2.0.4", - "phar-io/version": "^3.2.1", - "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.12", - "phpunit/php-file-iterator": "^5.1.1", - "phpunit/php-invoker": "^5.0.1", - "phpunit/php-text-template": "^4.0.1", - "phpunit/php-timer": "^7.0.1", - "sebastian/cli-parser": "^3.0.2", - "sebastian/code-unit": "^3.0.3", - "sebastian/comparator": "^6.3.3", - "sebastian/diff": "^6.0.2", - "sebastian/environment": "^7.2.1", - "sebastian/exporter": "^6.3.2", - "sebastian/global-state": "^7.0.2", - "sebastian/object-enumerator": "^6.0.1", - "sebastian/recursion-context": "^6.0.3", - "sebastian/type": "^5.1.3", - "sebastian/version": "^5.0.2", - "staabm/side-effects-detector": "^1.0.5" + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation-contracts": "^3.6.1" }, - "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files" + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/http-client-contracts": "<2.5", + "symfony/service-contracts": "<2.5" }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "11.5-dev" - } + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^7.4|^8.0" }, + "type": "library", "autoload": { "files": [ - "src/Framework/Assert/Functions.php" + "Resources/functions.php" ], - "classmap": [ - "src/" + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", "support": { - "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" + "source": "https://github.com/symfony/translation/tree/v8.0.10" }, "funding": [ { - "url": "https://phpunit.de/sponsors.html", + "url": "https://symfony.com/sponsor", "type": "custom" }, { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" + "url": "https://github.com/fabpot", + "type": "github" }, { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" + "url": "https://github.com/nicolas-grekas", + "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2026-02-18T12:37:06+00:00" + "time": "2026-05-06T11:30:54+00:00" }, { - "name": "sebastian/cli-parser", - "version": "3.0.2", + "name": "symfony/translation-contracts", + "version": "v3.7.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", - "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d", "shasum": "" }, "require": { - "php": ">=8.2" - }, - "require-dev": { - "phpunit/phpunit": "^11.0" + "php": ">=8.1" }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "3.7-dev" } }, "autoload": { - "classmap": [ - "src/" + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Library for parsing CLI options", - "homepage": "https://github.com/sebastianbergmann/cli-parser", + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], "support": { - "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.0" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2024-07-03T04:41:36+00:00" + "time": "2026-01-05T13:30:16+00:00" }, { - "name": "sebastian/code-unit", - "version": "3.0.3", + "name": "symfony/uid", + "version": "v8.0.9", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + "url": "https://github.com/symfony/uid.git", + "reference": "4d9d6510bbe88ebb4608b7200d18606cdf80825c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", - "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "url": "https://api.github.com/repos/symfony/uid/zipball/4d9d6510bbe88ebb4608b7200d18606cdf80825c", + "reference": "4d9d6510bbe88ebb4608b7200d18606cdf80825c", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4", + "symfony/polyfill-uuid": "^1.15" }, "require-dev": { - "phpunit/phpunit": "^11.5" + "symfony/console": "^7.4|^8.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "security": "https://github.com/sebastianbergmann/code-unit/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + "source": "https://github.com/symfony/uid/tree/v8.0.9" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2025-03-19T07:56:08+00:00" + "time": "2026-04-30T16:10:06+00:00" }, { - "name": "sebastian/code-unit-reverse-lookup", - "version": "4.0.1", + "name": "symfony/var-dumper", + "version": "v8.0.8", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + "url": "https://github.com/symfony/var-dumper.git", + "reference": "cfb7badd53bf4177f6e9416cfbbccc13c0e773a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", - "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/cfb7badd53bf4177f6e9416cfbbccc13c0e773a1", + "reference": "cfb7badd53bf4177f6e9416cfbbccc13c0e773a1", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/console": "<7.4", + "symfony/error-handler": "<7.4" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "symfony/console": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "twig/twig": "^3.12" }, + "bin": [ + "Resources/bin/var-dump-server" + ], "type": "library", - "extra": { - "branch-alias": { - "dev-main": "4.0-dev" - } - }, "autoload": { - "classmap": [ - "src/" + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + "source": "https://github.com/symfony/var-dumper/tree/v8.0.8" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "time": "2024-07-03T04:45:54+00:00" + "time": "2026-03-31T07:15:36+00:00" }, { - "name": "sebastian/comparator", - "version": "6.3.3", + "name": "theseer/tokenizer", + "version": "2.0.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", - "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", "shasum": "" }, "require": { "ext-dom": "*", - "ext-mbstring": "*", - "php": ">=8.2", - "sebastian/diff": "^6.0", - "sebastian/exporter": "^6.0" - }, - "require-dev": { - "phpunit/phpunit": "^11.4" - }, - "suggest": { - "ext-bcmath": "For comparing BcMath\\Number objects" + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "6.3-dev" - } - }, "autoload": { "classmap": [ "src/" @@ -10273,85 +11015,59 @@ ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" } ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "https://github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { - "issues": "https://github.com/sebastianbergmann/comparator/issues", - "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://github.com/theseer", "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", - "type": "tidelift" } ], - "time": "2026-01-24T09:26:40+00:00" + "time": "2025-12-08T11:19:18+00:00" }, { - "name": "sebastian/complexity", - "version": "4.0.1", + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", - "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", "shasum": "" }, "require": { - "nikic/php-parser": "^5.0", - "php": ">=8.2" + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-master": "2.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -10359,57 +11075,64 @@ ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" } ], - "description": "Library for calculating the complexity of PHP code units", - "homepage": "https://github.com/sebastianbergmann/complexity", + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", "support": { - "issues": "https://github.com/sebastianbergmann/complexity/issues", - "security": "https://github.com/sebastianbergmann/complexity/security/policy", - "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T04:49:50+00:00" + "time": "2025-12-02T11:56:42+00:00" }, { - "name": "sebastian/diff", - "version": "6.0.2", + "name": "vlucas/phpdotenv", + "version": "v5.6.3", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "955e7815d677a3eaa7075231212f2110983adecc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", - "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", "shasum": "" }, "require": { - "php": ">=8.2" + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.4", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" }, "require-dev": { - "phpunit/phpunit": "^11.0", - "symfony/process": "^4.2 || ^5" + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." }, "type": "library", "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, "branch-alias": { - "dev-main": "6.0-dev" + "dev-master": "5.6-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Dotenv\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -10417,541 +11140,588 @@ ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" }, { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" } ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" + "dotenv", + "env", + "environment" ], "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://github.com/GrahamCampbell", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" } ], - "time": "2024-07-03T04:53:05+00:00" + "time": "2025-12-27T19:49:13+00:00" }, { - "name": "sebastian/environment", - "version": "7.2.1", + "name": "voku/portable-ascii", + "version": "2.1.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + "url": "https://github.com/voku/portable-ascii.git", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", - "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f", "shasum": "" }, "require": { - "php": ">=8.2" + "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "^11.3" + "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5" }, "suggest": { - "ext-posix": "*" + "ext-intl": "Use Intl for transliterator_transliterate() support" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "7.2-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "voku\\": "src/voku/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" } ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "https://github.com/sebastianbergmann/environment", + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", "keywords": [ - "Xdebug", - "environment", - "hhvm" + "ascii", + "clean", + "php" ], "support": { - "issues": "https://github.com/sebastianbergmann/environment/issues", - "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.1.1" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", "type": "github" }, { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" }, { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" + "url": "https://www.patreon.com/voku", + "type": "patreon" }, { - "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", "type": "tidelift" } ], - "time": "2025-05-21T11:55:47+00:00" - }, + "time": "2026-04-26T05:33:54+00:00" + } + ], + "packages-dev": [ { - "name": "sebastian/exporter", - "version": "6.3.2", + "name": "cloudcreativity/json-api-testing", + "version": "v6.4.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" + "url": "https://github.com/cloudcreativity/json-api-testing.git", + "reference": "e7626f6d4e54094d7bfaf3751ef7e50cf5c0af1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", - "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", + "url": "https://api.github.com/repos/cloudcreativity/json-api-testing/zipball/e7626f6d4e54094d7bfaf3751ef7e50cf5c0af1f", + "reference": "e7626f6d4e54094d7bfaf3751ef7e50cf5c0af1f", "shasum": "" }, "require": { - "ext-mbstring": "*", - "php": ">=8.2", - "sebastian/recursion-context": "^6.0" - }, - "require-dev": { - "phpunit/phpunit": "^11.3" + "ext-json": "*", + "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "php": "^8.2", + "phpunit/phpunit": "^10.5|^11.0|^12.0|^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.3-dev" + "dev-develop": "6.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "CloudCreativity\\JsonApi\\Testing\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" + "name": "Cloud Creativity Ltd", + "email": "info@cloudcreativity.co.uk" } ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "https://www.github.com/sebastianbergmann/exporter", + "description": "PHPUnit test helpers to check JSON API documents.", + "homepage": "https://github.com/cloudcreativity/json-api", "keywords": [ - "export", - "exporter" + "JSON-API", + "api", + "cloudcreativity", + "json", + "jsonapi", + "jsonapi.org" ], "support": { - "issues": "https://github.com/sebastianbergmann/exporter/issues", - "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" + "issues": "https://github.com/cloudcreativity/json-api/issues", + "source": "https://github.com/cloudcreativity/json-api-testing/tree/v6.4.0" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, + "time": "2026-04-02T09:17:01+00:00" + }, + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ { - "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", - "type": "tidelift" + "name": "François Zaninotto" } ], - "time": "2025-09-24T06:12:51+00:00" + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" }, { - "name": "sebastian/global-state", - "version": "7.0.2", + "name": "filp/whoops", + "version": "2.18.4", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + "url": "https://github.com/filp/whoops.git", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", - "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", "shasum": "" }, "require": { - "php": ">=8.2", - "sebastian/object-reflector": "^4.0", - "sebastian/recursion-context": "^6.0" + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" }, "require-dev": { - "ext-dom": "*", - "phpunit/phpunit": "^11.0" + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-master": "2.7-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Whoops\\": "src/Whoops/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" } ], - "description": "Snapshotting of global state", - "homepage": "https://www.github.com/sebastianbergmann/global-state", + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", "keywords": [ - "global state" + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" ], "support": { - "issues": "https://github.com/sebastianbergmann/global-state/issues", - "security": "https://github.com/sebastianbergmann/global-state/security/policy", - "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.4" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://github.com/denis-sokolov", "type": "github" } ], - "time": "2024-07-03T04:57:36+00:00" + "time": "2025-08-08T12:00:00+00:00" }, { - "name": "sebastian/lines-of-code", - "version": "3.0.1", + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", - "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", "shasum": "" }, "require": { - "nikic/php-parser": "^5.0", - "php": ">=8.2" + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { "classmap": [ - "src/" + "hamcrest" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" ], - "description": "Library for counting the lines of code in PHP source code", - "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { - "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T04:58:38+00:00" + "time": "2025-04-30T06:54:44+00:00" }, { - "name": "sebastian/object-enumerator", - "version": "6.0.1", + "name": "laravel-json-api/testing", + "version": "v3.2.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + "url": "https://github.com/laravel-json-api/testing.git", + "reference": "506d434a47ebd9bcc7dfca0096c63b79d28427a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", - "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "url": "https://api.github.com/repos/laravel-json-api/testing/zipball/506d434a47ebd9bcc7dfca0096c63b79d28427a8", + "reference": "506d434a47ebd9bcc7dfca0096c63b79d28427a8", "shasum": "" }, "require": { - "php": ">=8.2", - "sebastian/object-reflector": "^4.0", - "sebastian/recursion-context": "^6.0" + "cloudcreativity/json-api-testing": "^6.3", + "ext-json": "*", + "illuminate/http": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "illuminate/testing": "^11.0|^12.0|^13.0", + "php": "^8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "laravel/framework": "^11.0|^12.0|^13.0", + "phpunit/phpunit": "^10.5|^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-develop": "3.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "LaravelJsonApi\\Testing\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Cloud Creativity Ltd", + "email": "info@cloudcreativity.co.uk" + }, + { + "name": "Christopher Gammie", + "email": "chris@cloudcreativity.co.uk" } ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "description": "Test helpers for JSON:API compliant APIs.", + "homepage": "https://github.com/laravel-json-api/testing", + "keywords": [ + "JSON-API", + "jsonapi", + "jsonapi.org", + "laravel" + ], "support": { - "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + "issues": "https://github.com/laravel-json-api/testing/issues", + "source": "https://github.com/laravel-json-api/testing/tree/v3.2.0" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T05:00:13+00:00" + "time": "2026-03-28T18:06:47+00:00" }, { - "name": "sebastian/object-reflector", - "version": "4.0.1", + "name": "laravel/pint", + "version": "v1.29.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + "url": "https://github.com/laravel/pint.git", + "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", - "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "url": "https://api.github.com/repos/laravel/pint/zipball/0770e9b7fafd50d4586881d456d6eb41c9247a80", + "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80", "shasum": "" }, "require": { - "php": ">=8.2" + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" }, "require-dev": { - "phpunit/phpunit": "^11.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "4.0-dev" - } + "friendsofphp/php-cs-fixer": "^3.95.1", + "illuminate/view": "^12.56.0", + "larastan/larastan": "^3.9.6", + "laravel-zero/framework": "^12.1.0", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6", + "shipfastlabs/agent-detector": "^1.1.3" }, + "bin": [ + "builds/pint" + ], + "type": "project", "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" } ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "dev", + "format", + "formatter", + "lint", + "linter", + "php" + ], "support": { - "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-07-03T05:01:32+00:00" + "time": "2026-04-20T15:26:14+00:00" }, { - "name": "sebastian/recursion-context", - "version": "6.0.3", + "name": "laravel/sail", + "version": "v1.58.0", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + "url": "https://github.com/laravel/sail.git", + "reference": "2e5e968138ca52ed87d712449697a8364d73b466" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", - "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "url": "https://api.github.com/repos/laravel/sail/zipball/2e5e968138ca52ed87d712449697a8364d73b466", + "reference": "2e5e968138ca52ed87d712449697a8364d73b466", "shasum": "" }, "require": { - "php": ">=8.2" + "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "php": "^8.0", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/yaml": "^6.0|^7.0|^8.0" }, "require-dev": { - "phpunit/phpunit": "^11.3" + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0|^11.0", + "phpstan/phpstan": "^2.0" }, + "bin": [ + "bin/sail" + ], "type": "library", "extra": { - "branch-alias": { - "dev-main": "6.0-dev" + "laravel": { + "providers": [ + "Laravel\\Sail\\SailServiceProvider" + ] } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Laravel\\Sail\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" + "name": "Taylor Otwell", + "email": "taylor@laravel.com" } ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "https://github.com/sebastianbergmann/recursion-context", - "support": { - "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", - "type": "tidelift" - } + "description": "Docker files for running a basic Laravel application.", + "keywords": [ + "docker", + "laravel" ], - "time": "2025-08-13T04:42:22+00:00" + "support": { + "issues": "https://github.com/laravel/sail/issues", + "source": "https://github.com/laravel/sail" + }, + "time": "2026-04-27T13:38:34+00:00" }, { - "name": "sebastian/type", - "version": "5.1.3", + "name": "mockery/mockery", + "version": "1.6.12", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", - "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", "shasum": "" }, "require": { - "php": ">=8.2" + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" }, "require-dev": { - "phpunit/phpunit": "^11.3" + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "5.1-dev" - } - }, "autoload": { - "classmap": [ - "src/" - ] + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -10959,91 +11729,142 @@ ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the types of the PHP type system", - "homepage": "https://github.com/sebastianbergmann/type", - "support": { - "issues": "https://github.com/sebastianbergmann/type/issues", - "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" }, { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" }, { - "url": "https://tidelift.com/funding/github/packagist/sebastian/type", - "type": "tidelift" + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" } ], - "time": "2025-08-09T06:55:48+00:00" + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" }, { - "name": "sebastian/version", - "version": "5.0.2", + "name": "nunomaduro/collision", + "version": "v8.9.4", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + "url": "https://github.com/nunomaduro/collision.git", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", - "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5", "shasum": "" }, "require": { - "php": ">=8.2" + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", + "php": "^8.2.0", + "symfony/console": "^7.4.8 || ^8.0.8" + }, + "conflict": { + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.9.6", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0", + "laravel/pint": "^1.29.1", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1", + "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0" }, "type": "library", "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, "branch-alias": { - "dev-main": "5.0-dev" + "dev-8.x": "8.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" } ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "security": "https://github.com/sebastianbergmann/version/security/policy", - "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" }, "funding": [ { - "url": "https://github.com/sebastianbergmann", + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" } ], - "time": "2024-10-09T05:16:32+00:00" + "time": "2026-04-21T14:04:20+00:00" }, { "name": "spatie/backtrace", @@ -11185,26 +12006,26 @@ }, { "name": "spatie/flare-client-php", - "version": "1.10.1", + "version": "1.11.0", "source": { "type": "git", "url": "https://github.com/spatie/flare-client-php.git", - "reference": "bf1716eb98bd689451b071548ae9e70738dce62f" + "reference": "fb3ffb946675dba811fbde9122224db2f84daca9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/flare-client-php/zipball/bf1716eb98bd689451b071548ae9e70738dce62f", - "reference": "bf1716eb98bd689451b071548ae9e70738dce62f", + "url": "https://api.github.com/repos/spatie/flare-client-php/zipball/fb3ffb946675dba811fbde9122224db2f84daca9", + "reference": "fb3ffb946675dba811fbde9122224db2f84daca9", "shasum": "" }, "require": { - "illuminate/pipeline": "^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/pipeline": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", "php": "^8.0", "spatie/backtrace": "^1.6.1", - "symfony/http-foundation": "^5.2|^6.0|^7.0", - "symfony/mime": "^5.2|^6.0|^7.0", - "symfony/process": "^5.2|^6.0|^7.0", - "symfony/var-dumper": "^5.2|^6.0|^7.0" + "symfony/http-foundation": "^5.2|^6.0|^7.0|^8.0", + "symfony/mime": "^5.2|^6.0|^7.0|^8.0", + "symfony/process": "^5.2|^6.0|^7.0|^8.0", + "symfony/var-dumper": "^5.2|^6.0|^7.0|^8.0" }, "require-dev": { "dms/phpunit-arraysubset-asserts": "^0.5.0", @@ -11242,7 +12063,7 @@ ], "support": { "issues": "https://github.com/spatie/flare-client-php/issues", - "source": "https://github.com/spatie/flare-client-php/tree/1.10.1" + "source": "https://github.com/spatie/flare-client-php/tree/1.11.0" }, "funding": [ { @@ -11250,41 +12071,44 @@ "type": "github" } ], - "time": "2025-02-14T13:42:06+00:00" + "time": "2026-03-17T08:06:16+00:00" }, { "name": "spatie/ignition", - "version": "1.15.1", + "version": "1.16.0", "source": { "type": "git", "url": "https://github.com/spatie/ignition.git", - "reference": "31f314153020aee5af3537e507fef892ffbf8c85" + "reference": "b59385bb7aa24dae81bcc15850ebecfda7b40838" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/ignition/zipball/31f314153020aee5af3537e507fef892ffbf8c85", - "reference": "31f314153020aee5af3537e507fef892ffbf8c85", + "url": "https://api.github.com/repos/spatie/ignition/zipball/b59385bb7aa24dae81bcc15850ebecfda7b40838", + "reference": "b59385bb7aa24dae81bcc15850ebecfda7b40838", "shasum": "" }, "require": { "ext-json": "*", "ext-mbstring": "*", "php": "^8.0", - "spatie/error-solutions": "^1.0", - "spatie/flare-client-php": "^1.7", - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "spatie/backtrace": "^1.7.1", + "spatie/error-solutions": "^1.1.2", + "spatie/flare-client-php": "^1.9", + "symfony/console": "^5.4.42|^6.0|^7.0|^8.0", + "symfony/http-foundation": "^5.4.42|^6.0|^7.0|^8.0", + "symfony/mime": "^5.4.42|^6.0|^7.0|^8.0", + "symfony/var-dumper": "^5.4.42|^6.0|^7.0|^8.0" }, "require-dev": { - "illuminate/cache": "^9.52|^10.0|^11.0|^12.0", + "illuminate/cache": "^9.52|^10.0|^11.0|^12.0|^13.0", "mockery/mockery": "^1.4", - "pestphp/pest": "^1.20|^2.0", + "pestphp/pest": "^1.20|^2.0|^3.0", "phpstan/extension-installer": "^1.1", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-phpunit": "^1.0", "psr/simple-cache-implementation": "*", - "symfony/cache": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", + "symfony/cache": "^5.4.38|^6.0|^7.0|^8.0", + "symfony/process": "^5.4.35|^6.0|^7.0|^8.0", "vlucas/phpdotenv": "^5.5" }, "suggest": { @@ -11333,20 +12157,20 @@ "type": "github" } ], - "time": "2025-02-21T14:31:39+00:00" + "time": "2026-03-17T10:51:08+00:00" }, { "name": "spatie/laravel-ignition", - "version": "2.11.0", + "version": "2.12.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-ignition.git", - "reference": "11f38d1ff7abc583a61c96bf3c1b03610a69cccd" + "reference": "45b3b6e1e73fc161cba2149972698644b99594ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/11f38d1ff7abc583a61c96bf3c1b03610a69cccd", - "reference": "11f38d1ff7abc583a61c96bf3c1b03610a69cccd", + "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/45b3b6e1e73fc161cba2149972698644b99594ee", + "reference": "45b3b6e1e73fc161cba2149972698644b99594ee", "shasum": "" }, "require": { @@ -11356,7 +12180,7 @@ "illuminate/support": "^11.0|^12.0|^13.0", "nesbot/carbon": "^2.72|^3.0", "php": "^8.2", - "spatie/ignition": "^1.15.1", + "spatie/ignition": "^1.16", "symfony/console": "^7.4|^8.0", "symfony/var-dumper": "^7.4|^8.0" }, @@ -11425,72 +12249,20 @@ "type": "github" } ], - "time": "2026-02-22T19:14:05+00:00" - }, - { - "name": "staabm/side-effects-detector", - "version": "1.0.5", - "source": { - "type": "git", - "url": "https://github.com/staabm/side-effects-detector.git", - "reference": "d8334211a140ce329c13726d4a715adbddd0a163" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", - "reference": "d8334211a140ce329c13726d4a715adbddd0a163", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^1.12.6", - "phpunit/phpunit": "^9.6.21", - "symfony/var-dumper": "^5.4.43", - "tomasvotruba/type-coverage": "1.0.0", - "tomasvotruba/unused-public": "1.0.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "lib/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A static analysis tool to detect side effects in PHP code", - "keywords": [ - "static analysis" - ], - "support": { - "issues": "https://github.com/staabm/side-effects-detector/issues", - "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" - }, - "funding": [ - { - "url": "https://github.com/staabm", - "type": "github" - } - ], - "time": "2024-10-20T05:08:20+00:00" + "time": "2026-03-17T12:20:04+00:00" }, { "name": "symfony/yaml", - "version": "v8.0.6", + "version": "v8.0.10", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "5f006c50a981e1630bbb70ad409c5d85f9a716e0" + "reference": "aa9ee60c41d9b20a2468c41ff0a32e2a7405ac05" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/5f006c50a981e1630bbb70ad409c5d85f9a716e0", - "reference": "5f006c50a981e1630bbb70ad409c5d85f9a716e0", + "url": "https://api.github.com/repos/symfony/yaml/zipball/aa9ee60c41d9b20a2468c41ff0a32e2a7405ac05", + "reference": "aa9ee60c41d9b20a2468c41ff0a32e2a7405ac05", "shasum": "" }, "require": { @@ -11532,7 +12304,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v8.0.6" + "source": "https://github.com/symfony/yaml/tree/v8.0.10" }, "funding": [ { @@ -11552,57 +12324,7 @@ "type": "tidelift" } ], - "time": "2026-02-09T10:14:57+00:00" - }, - { - "name": "theseer/tokenizer", - "version": "1.3.1", - "source": { - "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", - "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - } - ], - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "support": { - "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.3.1" - }, - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], - "time": "2025-11-17T20:03:58+00:00" + "time": "2026-05-05T08:10:04+00:00" } ], "aliases": [], @@ -11611,7 +12333,7 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.2", + "php": "^8.3", "ext-pdo": "*" }, "platform-dev": {}, diff --git a/config/app.php b/config/app.php index 7eed12769..b376753e9 100644 --- a/config/app.php +++ b/config/app.php @@ -127,6 +127,8 @@ | */ + 'api_page_size' => (int) env('APP_API_PAGE_SIZE', 15), + 'maintenance' => [ 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), 'store' => env('APP_MAINTENANCE_STORE', 'database'), diff --git a/config/cache.php b/config/cache.php index 6b57b1833..755e8e47e 100644 --- a/config/cache.php +++ b/config/cache.php @@ -104,4 +104,19 @@ 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), + /* + |-------------------------------------------------------------------------- + | Serializable Classes + |-------------------------------------------------------------------------- + | + | This option allows you to specify which classes are allowed to be + | unserialized from the cache. By default, no classes are allowed, + | which helps prevent PHP deserialization gadget chain attacks + | if your application's APP_KEY is leaked. You may set this + | to an array of class names or false to disable. + | + */ + + 'serializable_classes' => false, + ]; diff --git a/config/jsonapi.php b/config/jsonapi.php new file mode 100644 index 000000000..c12b189eb --- /dev/null +++ b/config/jsonapi.php @@ -0,0 +1,32 @@ + 'JsonApi', + + /* + |-------------------------------------------------------------------------- + | Servers + |-------------------------------------------------------------------------- + | + | A list of the JSON:API compliant APIs in your application, referred to + | as "servers". They must be listed below, with the array key being the + | unique name for each server, and the value being the fully-qualified + | class name of the server class. + */ + 'servers' => [ + 'v1' => \App\JsonApi\V1\Server::class, + ], +]; diff --git a/config/sanctum.php b/config/sanctum.php index 3b90d42a0..6f175239e 100644 --- a/config/sanctum.php +++ b/config/sanctum.php @@ -80,7 +80,7 @@ 'middleware' => [ 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, - 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\PreventRequestForgery::class, ], ]; diff --git a/database/factories/Assistants/AssistantFactory.php b/database/factories/Assistants/AssistantFactory.php new file mode 100644 index 000000000..ea5068e69 --- /dev/null +++ b/database/factories/Assistants/AssistantFactory.php @@ -0,0 +1,51 @@ + fake()->name(), + 'handle' => fake()->unique()->slug(3), + 'system_prompt' => fake()->sentence(), + 'greeting' => fake()->sentence(), + 'description' => fake()->sentence(), + 'detail_description' => fake()->paragraph(), + 'allow_remix' => fake()->boolean(), + 'allow_model_select' => fake()->boolean(), + 'language_id' => Language::factory(), + 'category_id' => Category::factory(), + 'release_stage' => 'private', + 'formality' => 'neutral', + 'model' => 'gpt-4', + 'model_length' => fake()->numberBetween(100, 4096), + 'model_temp' => fake()->randomFloat(2, 0, 1), + 'model_top_p' => fake()->randomFloat(2, 0, 1), + 'creator_id' => User::factory(), + 'remixed_creator_id' => User::factory(), + 'organization_id' => fn () => Organization::first()?->id, + ]; + } + + public function configure(): static + { + return $this->afterCreating(function (Assistant $assistant) { + $assistant->versions()->create([ + 'text' => 'Initial version', + 'version' => 1.0, + ]); + }); + } +} diff --git a/database/factories/Assistants/CategoryFactory.php b/database/factories/Assistants/CategoryFactory.php new file mode 100644 index 000000000..7a42331da --- /dev/null +++ b/database/factories/Assistants/CategoryFactory.php @@ -0,0 +1,18 @@ + fake()->unique()->word(), + ]; + } +} diff --git a/database/factories/Assistants/LanguageFactory.php b/database/factories/Assistants/LanguageFactory.php new file mode 100644 index 000000000..c5113c941 --- /dev/null +++ b/database/factories/Assistants/LanguageFactory.php @@ -0,0 +1,18 @@ + fake()->unique()->languageCode(), + ]; + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 584104c9c..756fbb443 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -2,43 +2,22 @@ namespace Database\Factories; +use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; -use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; -/** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User> - */ class UserFactory extends Factory { - /** - * The current password being used by the factory. - */ - protected static ?string $password; + protected $model = User::class; - /** - * Define the model's default state. - * - * @return array - */ public function definition(): array { return [ 'name' => fake()->name(), 'email' => fake()->unique()->safeEmail(), - 'email_verified_at' => now(), - 'password' => static::$password ??= Hash::make('password'), - 'remember_token' => Str::random(10), + 'username' => fake()->unique()->userName(), + 'publicKey' => Str::random(32), + 'employeetype' => 'staff', ]; } - - /** - * Indicate that the model's email address should be unverified. - */ - public function unverified(): static - { - return $this->state(fn (array $attributes) => [ - 'email_verified_at' => null, - ]); - } } diff --git a/database/migrations/2025_04_04_123507_update_enum_type_on_usage_records_table.php b/database/migrations/2025_04_04_123507_update_enum_type_on_usage_records_table.php index 3569dee7d..01fd6ec66 100644 --- a/database/migrations/2025_04_04_123507_update_enum_type_on_usage_records_table.php +++ b/database/migrations/2025_04_04_123507_update_enum_type_on_usage_records_table.php @@ -8,7 +8,7 @@ class UpdateEnumTypeOnUsageRecordsTable extends Migration { public function up() { - if(env('DB_CONNECTION') == 'pgsql') { + if (env('DB_CONNECTION') == 'pgsql') { // Check if the column uses a custom enum type or is just a varchar with a check constraint: // Query information_schema to get the column type $columnType = DB::table('information_schema.columns') @@ -32,8 +32,12 @@ public function up() CHECK (type IN ('private', 'group', 'api'));" ); } - } - else{ + } elseif (DB::getDriverName() === 'sqlite') { + // SQLite doesn't support enum → use string instead + Schema::table('usage_records', function (Blueprint $table) { + $table->string('type', 50)->change(); + }); + } else { // This updates the 'type' column to include 'api'. DB::statement(" ALTER TABLE `usage_records` diff --git a/database/migrations/2026_04_28_150049_create_assistent.php b/database/migrations/2026_04_28_150049_create_assistent.php new file mode 100644 index 000000000..97408a81f --- /dev/null +++ b/database/migrations/2026_04_28_150049_create_assistent.php @@ -0,0 +1,194 @@ +id(); + $table->string('text')->unique(); + $table->timestamps(); + }); + + Schema::create('languages', function (Blueprint $table) { + $table->id(); + $table->string('text')->unique(); + $table->timestamps(); + }); + + Schema::create('assistants', function (Blueprint $table) { + $table->id(); + $table->timestamps(); + + $table->string('name'); + $table->string('handle')->unique()->nullable(); + + $table->text('system_prompt'); + + $table->text('greeting'); + $table->text('description'); + $table->text('detail_description'); + + $table->boolean('allow_remix'); + + $table->foreignId('language_id') + ->nullable() + ->constrained('languages') + ->nullOnDelete(); + + $table->foreignId('category_id') + ->nullable() + ->constrained('categories') + ->nullOnDelete(); + + $table->string('release_stage'); + + $table->string('formality'); + + $table->boolean('allow_model_select'); + + $table->text('model'); + $table->integer('model_length'); + $table->float('model_temp'); + $table->float('model_top_p'); + + $table->foreignId('creator_id') + ->nullable() + ->constrained('users') + ->nullOnDelete(); + + $table->foreignId('remixed_creator_id') + ->nullable() + ->constrained('users') + ->nullOnDelete(); + + $table->foreignId('remixed_assistant_id') + ->nullable() + ->constrained('assistants') + ->nullOnDelete(); + }); + + Schema::create('versions', function (Blueprint $table) { + $table->id(); + $table->foreignId('assistant_id') + ->constrained('assistants') + ->cascadeOnDelete(); + $table->text('text'); + $table->decimal('version', 8, 1)->default(1.0); + $table->json('changed_keys')->nullable(); + $table->timestamps(); + }); + + Schema::create('assistant_tools', function (Blueprint $table) { + $table->id(); + $table->foreignId('assistant_id')->nullable()->constrained()->nullOnDelete(); + $table->foreignId('ai_tool_id')->nullable()->constrained()->nullOnDelete(); + $table->timestamps(); + }); + + Schema::create('user_prompts', function (Blueprint $table) { + $table->id(); + + $table->text('text'); + + $table->foreignId('assistant_id') + ->constrained('assistants') + ->cascadeOnDelete(); + + $table->timestamps(); + }); + + Schema::create('tags', function (Blueprint $table) { + $table->id(); + + $table->string('text'); + + $table->timestamps(); + }); + + Schema::create('assistant_tag', function (Blueprint $table) { + $table->id(); + + $table->foreignId('assistant_id') + ->constrained() + ->cascadeOnDelete(); + + $table->foreignId('tag_id') + ->constrained() + ->cascadeOnDelete(); + + $table->timestamps(); + + $table->unique(['assistant_id', 'tag_id']); + }); + + Schema::create('reviews', function (Blueprint $table) { + $table->id(); + $table->foreignId('assistant_id') + ->unique() + ->constrained('assistants') + ->cascadeOnDelete(); + $table->string('status')->default('pending'); + $table->text('reason')->nullable(); + $table->timestamps(); + }); + + Schema::create('feedback', function (Blueprint $table) { + $table->id(); + + $table->text('text'); + + $table->foreignId('assistant_id') + ->constrained('assistants') + ->cascadeOnDelete(); + + $table->foreignId('user_id') + ->constrained('users') + ->cascadeOnDelete(); + + $table->timestamps(); + }); + + Schema::create('assistant_favorite_users', function (Blueprint $table) { + $table->id(); + + $table->foreignId('assistant_id') + ->constrained() + ->cascadeOnDelete(); + + $table->foreignId('user_id') + ->constrained() + ->cascadeOnDelete(); + + $table->timestamps(); + + $table->unique(['assistant_id', 'user_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('versions'); + Schema::dropIfExists('assistant_tag'); + Schema::dropIfExists('assistant_tools'); + Schema::dropIfExists('user_prompts'); + Schema::dropIfExists('assistants'); + Schema::dropIfExists('tags'); + Schema::dropIfExists('categories'); + Schema::dropIfExists('languages'); + Schema::dropIfExists('assistant_favorite_users'); + Schema::dropIfExists('feedback'); + Schema::dropIfExists('reviews'); + } +}; diff --git a/database/migrations/2026_05_05_000000_create_organizations_table.php b/database/migrations/2026_05_05_000000_create_organizations_table.php new file mode 100644 index 000000000..ff62a6c18 --- /dev/null +++ b/database/migrations/2026_05_05_000000_create_organizations_table.php @@ -0,0 +1,70 @@ +id(); + $table->string('name'); + $table->timestamps(); + }); + + Schema::create('organization_user', function (Blueprint $table) { + $table->id(); + $table->foreignId('organization_id') + ->constrained('organizations') + ->cascadeOnDelete(); + $table->foreignId('user_id') + ->constrained('users') + ->cascadeOnDelete(); + $table->string('role')->default('member'); + $table->timestamps(); + + $table->unique(['organization_id', 'user_id']); + }); + + Schema::table('assistants', function (Blueprint $table) { + $table->foreignId('organization_id') + ->nullable() + ->after('id') + ->constrained('organizations') + ->nullOnDelete(); + }); + + $orgId = DB::table('organizations')->insertGetId([ + 'name' => 'HAWKI', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $userIds = DB::table('users')->pluck('id'); + $now = now(); + + DB::table('organization_user')->insert( + $userIds->map(fn($id) => [ + 'organization_id' => $orgId, + 'user_id' => $id, + 'role' => 'member', + 'created_at' => $now, + 'updated_at' => $now, + ])->toArray() + ); + } + + public function down(): void + { + Schema::table('assistants', function (Blueprint $table) { + $table->dropForeign(['organization_id']); + $table->dropColumn('organization_id'); + }); + + Schema::dropIfExists('organization_user'); + Schema::dropIfExists('organizations'); + } +}; diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml new file mode 100644 index 000000000..7af3a52f3 --- /dev/null +++ b/docker-compose.ci.yml @@ -0,0 +1,16 @@ +services: + test: + container_name: ${PROJECT_NAME:-hawki}-test + build: + context: . + target: app_test + environment: + - APP_KEY=${APP_KEY:-N9bC6l&vZY2EO4be%Sty3hDOQSfPB7e0} + - APP_ENV=testing + - DB_CONNECTION=sqlite + - "DB_DATABASE=:memory:" + - CACHE_STORE=array + - SESSION_DRIVER=array + - QUEUE_CONNECTION=sync + - MAIL_MAILER=array + entrypoint: ["sh", "-c", "APP_ENV=testing php vendor/bin/phpunit"] \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index 506b9a38e..b99615641 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -18,16 +18,19 @@ - + - - + + + + + diff --git a/resources/js/echo.js b/resources/js/echo.js index 08547b44e..b31d3abad 100644 --- a/resources/js/echo.js +++ b/resources/js/echo.js @@ -3,7 +3,7 @@ import Echo from 'laravel-echo'; import Pusher from 'pusher-js'; window.Pusher = Pusher; - +console.log(import.meta.env) window.Echo = new Echo({ broadcaster: 'reverb', key: import.meta.env.VITE_REVERB_APP_KEY ?? 'hawki2', diff --git a/routes/api.php b/routes/api.php index e9443e24e..d6e42d3f8 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,11 +1,19 @@ get('/user', function (Request $request) { return $request->user(); @@ -15,7 +23,83 @@ Route::post('ai-req', [StreamController::class, 'handleExternalRequest']); - // ADD OTHER ENDPOINTS HERE +}); + +Route::middleware(['auth:sanctum'])->group(function () { + JsonApiRoute::server('v1') + ->prefix('') + ->resources(function ($server) { + $server->resource('assistants', AssistantController::class) + ->relationships(function ($relationships) { + $relationships->hasOne('language')->readOnly(); + $relationships->hasOne('category')->readOnly(); + $relationships->hasMany('user_prompts')->readOnly(); + $relationships->hasMany('ai_tools')->readOnly(); + $relationships->hasMany('tags')->readOnly(); + $relationships->hasOne('creator')->readOnly(); + $relationships->hasOne('remix_creator')->readOnly(); + $relationships->hasOne('remixed_assistant')->readOnly(); + $relationships->hasMany('versions')->readOnly(); + $relationships->hasOne('organization')->readOnly(); + $relationships->hasOne('review')->readOnly(); + }) + ->actions('actions', function (ActionRegistrar $actions) { + $actions->withId()->post('remix'); + $actions->withId()->post('release'); + $actions->withId()->post('feedback'); + $actions->withId()->post('favorite'); + $actions->withId()->post('chat-test'); + }); + + $server->resource('assistant-categories', CategoryController::class) + ->only('index', 'show') + ->relationships(function ($relationships) { + $relationships->hasMany('assistants')->readOnly(); + }); + $server->resource('tags', TagController::class) + ->only('index', 'show', 'store', 'destroy') + ->relationships(function ($relationships) { + $relationships->hasMany('assistants')->readOnly(); + }); -}); \ No newline at end of file + $server->resource('assistant-languages', AssistantLanguageController::class) + ->only('index', 'show') + ->relationships(function ($relationships) { + $relationships->hasMany('assistants')->readOnly(); + }); + + $server->resource('assistant-reviews', ReviewController::class) + ->only('index', 'show', 'update') + ->relationships(function ($relationships) { + $relationships->hasOne('assistant')->readOnly(); + }); + + $server->resource('ai-tools', AiToolController::class) + ->only('index', 'show') + ->relationships(function ($relationships) { + $relationships->hasOne('server')->readOnly(); + $relationships->hasMany('models')->readOnly(); + }); + + $server->resource('mcp-servers', McpServerController::class) + ->only('index', 'show') + ->relationships(function ($relationships) { + $relationships->hasMany('tools')->readOnly(); + }); + + $server->resource('ai-models', AiModelController::class) + ->only('index', 'show') + ->relationships(function ($relationships) { + $relationships->hasOne('provider')->readOnly(); + $relationships->hasMany('assignedTools')->readOnly(); + $relationships->hasOne('status')->readOnly(); + }); + + $server->resource('ai-providers', AiProviderController::class) + ->only('index', 'show') + ->relationships(function ($relationships) { + $relationships->hasMany('models')->readOnly(); + }); + }); +}); diff --git a/tests/Feature/Api/AiModelTest.php b/tests/Feature/Api/AiModelTest.php new file mode 100644 index 000000000..e49316b4b --- /dev/null +++ b/tests/Feature/Api/AiModelTest.php @@ -0,0 +1,281 @@ +jsonApi('get', '/api/ai-models') + ->assertUnauthorized() + ->assertJson(['errors' => [['detail' => 'Unauthenticated.']]]); + } + + public function test_can_list_ai_models(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $provider = AiProvider::create([ + 'provider_id' => 'test-provider', + 'name' => 'Test Provider', + 'active' => true, + 'api_url' => 'https://api.example.com', + ]); + + $model = AiModel::create([ + 'model_id' => 'test-model-1', + 'label' => 'Test Model', + 'active' => true, + 'input' => ['text', 'image'], + 'output' => ['text'], + 'tools' => ['stream'], + 'default_params' => ['temperature' => 0.7], + 'provider_id' => $provider->id, + ]); + + $response = $this->jsonApi('get', '/api/ai-models') + ->assertOk(); + + $data = collect($response->json('data')); + $modelResource = $data->first(fn($item) => $item['id'] === (string) $model->id); + + $response->assertJson([ + 'data' => [ + array_search($modelResource, $data->all()) => [ + 'id' => (string) $model->id, + 'type' => 'ai-models', + 'attributes' => [ + 'active' => true, + 'model_id' => 'test-model-1', + 'label' => 'Test Model', + 'input' => ['text', 'image'], + 'output' => ['text'], + 'tools' => ['stream'], + 'default_params' => ['temperature' => 0.7], + 'created_at' => $model->created_at->toJson(), + 'updated_at' => $model->updated_at->toJson(), + ], + ], + ], + ]); + } + + public function test_can_list_ai_models_with_provider(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $provider = AiProvider::create([ + 'provider_id' => 'openai', + 'name' => 'OpenAI', + 'active' => true, + 'api_url' => 'https://api.openai.com', + ]); + + $model = AiModel::create([ + 'model_id' => 'gpt-4', + 'label' => 'GPT-4', + 'active' => true, + 'provider_id' => $provider->id, + ]); + + $response = $this->jsonApi('get', '/api/ai-models?include=provider') + ->assertOk(); + + $data = collect($response->json('data')); + $modelResource = $data->first(fn($item) => $item['id'] === (string) $model->id); + $idx = array_search($modelResource, $data->all()); + + $response->assertJson([ + 'data' => [ + $idx => [ + 'id' => (string) $model->id, + 'type' => 'ai-models', + 'relationships' => [ + 'provider' => [ + 'data' => [ + 'id' => (string) $provider->id, + 'type' => 'ai-providers', + ], + ], + ], + ], + ], + ]); + + $included = collect($response->json('included')); + $providerResource = $included->first(fn($item) => $item['type'] === 'ai-providers' && $item['id'] === (string) $provider->id); + + $response->assertJson([ + 'included' => [ + array_search($providerResource, $included->all()) => [ + 'id' => (string) $provider->id, + 'type' => 'ai-providers', + 'attributes' => [ + 'provider_id' => 'openai', + 'name' => 'OpenAI', + 'active' => true, + 'api_url' => 'https://api.openai.com', + 'ping_url' => null, + 'created_at' => $provider->created_at->toJson(), + 'updated_at' => $provider->updated_at->toJson(), + ], + ], + ], + ]); + } + + public function test_can_list_ai_models_with_assigned_tools(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $provider = AiProvider::create([ + 'provider_id' => 'test-provider', + 'name' => 'Test Provider', + 'active' => true, + 'api_url' => 'https://api.example.com', + ]); + + $model = AiModel::create([ + 'model_id' => 'test-model-1', + 'label' => 'Test Model', + 'active' => true, + 'provider_id' => $provider->id, + ]); + + $tool = AiTool::create([ + 'type' => 'function', + 'name' => 'assigned-tool', + 'status' => 'active', + ]); + + $model->assignedTools()->attach($tool->id); + + $response = $this->jsonApi('get', '/api/ai-models?include=assignedTools') + ->assertOk(); + + $data = collect($response->json('data')); + $modelResource = $data->first(fn($item) => $item['id'] === (string) $model->id); + $idx = array_search($modelResource, $data->all()); + + $response->assertJson([ + 'data' => [ + $idx => [ + 'id' => (string) $model->id, + 'type' => 'ai-models', + 'relationships' => [ + 'assignedTools' => [ + 'data' => [ + [ + 'id' => (string) $tool->id, + 'type' => 'ai-tools', + ], + ], + ], + ], + ], + ], + ]); + + $included = collect($response->json('included')); + $toolResource = $included->first(fn($item) => $item['type'] === 'ai-tools' && $item['id'] === (string) $tool->id); + + $response->assertJson([ + 'included' => [ + array_search($toolResource, $included->all()) => [ + 'id' => (string) $tool->id, + 'type' => 'ai-tools', + 'attributes' => [ + 'name' => 'assigned-tool', + 'status' => 'active', + ], + ], + ], + ]); + } + + public function test_can_show_single_ai_model(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $provider = AiProvider::create([ + 'provider_id' => 'test-provider', + 'name' => 'Test Provider', + 'active' => true, + 'api_url' => 'https://api.example.com', + ]); + + $model = AiModel::create([ + 'model_id' => 'test-model-1', + 'label' => 'Test Model', + 'active' => true, + 'provider_id' => $provider->id, + ]); + + $this->jsonApi('get', "/api/ai-models/{$model->id}") + ->assertOk() + ->assertJson([ + 'data' => [ + 'id' => (string) $model->id, + 'type' => 'ai-models', + 'attributes' => [ + 'active' => true, + 'model_id' => 'test-model-1', + 'label' => 'Test Model', + 'input' => null, + 'output' => null, + 'tools' => null, + 'default_params' => null, + 'created_at' => $model->created_at->toJson(), + 'updated_at' => $model->updated_at->toJson(), + ], + ], + ]); + } + + public function test_ai_models_pagination(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $provider = AiProvider::create([ + 'provider_id' => 'test-provider', + 'name' => 'Test Provider', + 'active' => true, + 'api_url' => 'https://api.example.com', + ]); + + foreach (range(1, 15) as $i) { + AiModel::create([ + 'model_id' => "model-{$i}", + 'label' => "Model {$i}", + 'active' => true, + 'provider_id' => $provider->id, + ]); + } + + $response = $this->jsonApi('get', '/api/ai-models?' . http_build_query(['page' => ['size' => 5]])) + ->assertOk() + ->assertJsonCount(5, 'data'); + + $response->assertJsonStructure([ + 'meta' => [ + 'page' => ['currentPage', 'from', 'to', 'perPage', 'lastPage', 'total'], + ], + 'links' => ['first', 'last', 'next'], + ]); + } +} diff --git a/tests/Feature/Api/AiProviderTest.php b/tests/Feature/Api/AiProviderTest.php new file mode 100644 index 000000000..9ebc125e0 --- /dev/null +++ b/tests/Feature/Api/AiProviderTest.php @@ -0,0 +1,377 @@ +jsonApi('get', '/api/ai-providers') + ->assertUnauthorized() + ->assertJson(['errors' => [['detail' => 'Unauthenticated.']]]); + } + + public function test_can_list_ai_providers(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $provider = AiProvider::create([ + 'provider_id' => 'openai', + 'name' => 'OpenAI', + 'active' => true, + 'api_url' => 'https://api.openai.com', + ]); + + $response = $this->jsonApi('get', '/api/ai-providers') + ->assertOk(); + + $data = collect($response->json('data')); + $providerResource = $data->first(fn ($item) => $item['id'] === (string) $provider->id); + + $response->assertJson([ + 'data' => [ + array_search($providerResource, $data->all()) => [ + 'id' => (string) $provider->id, + 'type' => 'ai-providers', + 'attributes' => [ + 'provider_id' => 'openai', + 'name' => 'OpenAI', + 'active' => true, + 'api_url' => 'https://api.openai.com', + 'ping_url' => null, + 'created_at' => $provider->created_at->toJson(), + 'updated_at' => $provider->updated_at->toJson(), + ], + ], + ], + ]); + } + + public function test_can_list_ai_providers_with_models(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $provider = AiProvider::create([ + 'provider_id' => 'openai', + 'name' => 'OpenAI', + 'active' => true, + 'api_url' => 'https://api.openai.com', + ]); + + $model = AiModel::create([ + 'model_id' => 'gpt-4', + 'label' => 'GPT-4', + 'active' => true, + 'provider_id' => $provider->id, + ]); + + $response = $this->jsonApi('get', '/api/ai-providers?include=models') + ->assertOk(); + + $data = collect($response->json('data')); + $providerResource = $data->first(fn ($item) => $item['id'] === (string) $provider->id); + $idx = array_search($providerResource, $data->all()); + + $response->assertJson([ + 'data' => [ + $idx => [ + 'id' => (string) $provider->id, + 'type' => 'ai-providers', + 'relationships' => [ + 'models' => [ + 'data' => [ + [ + 'id' => (string) $model->id, + 'type' => 'ai-models', + ], + ], + ], + ], + ], + ], + ]); + + $included = collect($response->json('included')); + $modelResource = $included->first(fn ($item) => $item['type'] === 'ai-models' && $item['id'] === (string) $model->id); + + $response->assertJson([ + 'included' => [ + array_search($modelResource, $included->all()) => [ + 'id' => (string) $model->id, + 'type' => 'ai-models', + 'attributes' => [ + 'active' => true, + 'model_id' => 'gpt-4', + 'label' => 'GPT-4', + ], + ], + ], + ]); + } + + public function test_can_list_ai_providers_with_nested_models_and_assigned_tools(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $provider = AiProvider::create([ + 'provider_id' => 'openai', + 'name' => 'OpenAI', + 'active' => true, + 'api_url' => 'https://api.openai.com', + ]); + + $model = AiModel::create([ + 'model_id' => 'gpt-4', + 'label' => 'GPT-4', + 'active' => true, + 'provider_id' => $provider->id, + ]); + + $tool = AiTool::create([ + 'type' => 'function', + 'name' => 'web-search', + 'status' => 'active', + ]); + + $model->assignedTools()->attach($tool->id); + + $response = $this->jsonApi('get', '/api/ai-providers?include=models.assignedTools') + ->assertOk(); + + $data = collect($response->json('data')); + $providerResource = $data->first(fn ($item) => $item['id'] === (string) $provider->id); + $idx = array_search($providerResource, $data->all()); + + $response->assertJson([ + 'data' => [ + $idx => [ + 'id' => (string) $provider->id, + 'type' => 'ai-providers', + 'relationships' => [ + 'models' => [ + 'data' => [ + [ + 'id' => (string) $model->id, + 'type' => 'ai-models', + ], + ], + ], + ], + ], + ], + ]); + + $included = collect($response->json('included')); + + $modelResource = $included->first(fn ($item) => $item['type'] === 'ai-models' && $item['id'] === (string) $model->id); + $this->assertNotNull($modelResource, 'Model should be included'); + $this->assertEquals([ + ['id' => (string) $tool->id, 'type' => 'ai-tools'], + ], $modelResource['relationships']['assignedTools']['data']); + + $toolResource = $included->first(fn ($item) => $item['type'] === 'ai-tools' && $item['id'] === (string) $tool->id); + $this->assertNotNull($toolResource, 'Tool should be included'); + $this->assertEquals('web-search', $toolResource['attributes']['name']); + $this->assertEquals('active', $toolResource['attributes']['status']); + } + + public function test_can_list_ai_providers_with_nested_models_and_status(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $provider = AiProvider::create([ + 'provider_id' => 'openai', + 'name' => 'OpenAI', + 'active' => true, + 'api_url' => 'https://api.openai.com', + ]); + + $model = AiModel::create([ + 'model_id' => 'gpt-4', + 'label' => 'GPT-4', + 'active' => true, + 'provider_id' => $provider->id, + ]); + + AiModelStatus::create([ + 'model_id' => $model->model_id, + 'status' => 'online', + ]); + + $response = $this->jsonApi('get', '/api/ai-providers?include=models.status') + ->assertOk(); + + $data = collect($response->json('data')); + $providerResource = $data->first(fn ($item) => $item['id'] === (string) $provider->id); + + $response->assertJson([ + 'data' => [ + array_search($providerResource, $data->all()) => [ + 'id' => (string) $provider->id, + 'type' => 'ai-providers', + 'relationships' => [ + 'models' => [ + 'data' => [ + [ + 'id' => (string) $model->id, + 'type' => 'ai-models', + ], + ], + ], + ], + ], + ], + ]); + + $included = collect($response->json('included')); + + $modelResource = $included->first(fn ($item) => $item['type'] === 'ai-models' && $item['id'] === (string) $model->id); + $this->assertNotNull($modelResource, 'Model should be included'); + $this->assertEquals([ + 'id' => $model->model_id, + 'type' => 'ai-model-statuses', + ], $modelResource['relationships']['status']['data']); + + $statusResource = $included->first(fn ($item) => $item['type'] === 'ai-model-statuses' && $item['id'] === $model->model_id); + $this->assertNotNull($statusResource, 'Status should be included'); + $this->assertEquals('online', $statusResource['attributes']['status']); + } + + public function test_can_show_single_ai_provider(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $provider = AiProvider::create([ + 'provider_id' => 'openai', + 'name' => 'OpenAI', + 'active' => true, + 'api_url' => 'https://api.openai.com', + 'ping_url' => 'https://api.openai.com/ping', + ]); + + $this->jsonApi('get', "/api/ai-providers/{$provider->id}") + ->assertOk() + ->assertJson([ + 'data' => [ + 'id' => (string) $provider->id, + 'type' => 'ai-providers', + 'attributes' => [ + 'provider_id' => 'openai', + 'name' => 'OpenAI', + 'active' => true, + 'api_url' => 'https://api.openai.com', + 'ping_url' => 'https://api.openai.com/ping', + 'created_at' => $provider->created_at->toJson(), + 'updated_at' => $provider->updated_at->toJson(), + ], + ], + ]); + } + + public function test_can_filter_ai_providers_by_tool_capability(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $providerA = AiProvider::create([ + 'provider_id' => 'openai', + 'name' => 'OpenAI', + 'active' => true, + 'api_url' => 'https://api.openai.com', + ]); + + $providerB = AiProvider::create([ + 'provider_id' => 'anthropic', + 'name' => 'Anthropic', + 'active' => true, + 'api_url' => 'https://api.anthropic.com', + ]); + + $modelA = AiModel::create([ + 'model_id' => 'gpt-4', + 'label' => 'GPT-4', + 'active' => true, + 'provider_id' => $providerA->id, + ]); + + $modelB = AiModel::create([ + 'model_id' => 'claude-3', + 'label' => 'Claude 3', + 'active' => true, + 'provider_id' => $providerB->id, + ]); + + $toolWebSearch = AiTool::create([ + 'type' => 'function', + 'name' => 'web-search', + 'status' => 'active', + 'capability' => 'web-search', + ]); + + $toolCodeExec = AiTool::create([ + 'type' => 'function', + 'name' => 'code-exec', + 'status' => 'active', + 'capability' => 'code-execution', + ]); + + $modelA->assignedTools()->attach($toolWebSearch->id); + $modelB->assignedTools()->attach($toolCodeExec->id); + + $response = $this->jsonApi('get', '/api/ai-providers?' . http_build_query(['filter' => ['tool_capability' => 'web-search']])) + ->assertOk(); + + $ids = collect($response->json('data'))->pluck('id')->all(); + $this->assertContains((string) $providerA->id, $ids); + $this->assertNotContains((string) $providerB->id, $ids); + + $response = $this->jsonApi('get', '/api/ai-providers?' . http_build_query(['filter' => ['tool_capability' => 'code-execution']])) + ->assertOk(); + + $ids = collect($response->json('data'))->pluck('id')->all(); + $this->assertContains((string) $providerB->id, $ids); + $this->assertNotContains((string) $providerA->id, $ids); + } + + public function test_ai_providers_pagination(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + foreach (range(1, 15) as $i) { + AiProvider::create([ + 'provider_id' => "provider-{$i}", + 'name' => "Provider {$i}", + 'active' => true, + 'api_url' => "https://api{$i}.example.com", + ]); + } + + $response = $this->jsonApi('get', '/api/ai-providers?' . http_build_query(['page' => ['size' => 5]])) + ->assertOk() + ->assertJsonCount(5, 'data'); + + $response->assertJsonStructure([ + 'meta' => [ + 'page' => ['currentPage', 'from', 'to', 'perPage', 'lastPage', 'total'], + ], + 'links' => ['first', 'last', 'next'], + ]); + } +} diff --git a/tests/Feature/Api/AiToolTest.php b/tests/Feature/Api/AiToolTest.php new file mode 100644 index 000000000..5f5d750e5 --- /dev/null +++ b/tests/Feature/Api/AiToolTest.php @@ -0,0 +1,314 @@ +jsonApi('get', '/api/ai-tools') + ->assertUnauthorized() + ->assertJson(['errors' => [['detail' => 'Unauthenticated.']]]); + } + + public function test_can_list_ai_tools(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $server = McpServer::create([ + 'url' => 'https://example.com/mcp', + 'server_label' => 'Test Server', + 'timeout' => '10', + 'discovery_timeout' => '10', + ]); + + $tool = AiTool::create([ + 'type' => 'mcp', + 'name' => 'my-test-tool', + 'description' => 'A test tool', + 'capability' => 'search', + 'status' => 'active', + 'active' => true, + 'server_id' => $server->id, + ]); + + $response = $this->jsonApi('get', '/api/ai-tools') + ->assertOk(); + + $data = collect($response->json('data')); + $toolResource = $data->first(fn($item) => $item['id'] === (string) $tool->id); + + $response->assertJson([ + 'data' => [ + array_search($toolResource, $data->all()) => [ + 'id' => (string) $tool->id, + 'type' => 'ai-tools', + 'attributes' => [ + 'type' => 'mcp', + 'name' => 'my-test-tool', + 'class_name' => null, + 'description' => 'A test tool', + 'capability' => 'search', + 'status' => 'active', + 'active' => true, + 'inputSchema' => null, + 'created_at' => $tool->created_at->toJson(), + 'updated_at' => $tool->updated_at->toJson(), + ], + ], + ], + ]); + } + + public function test_can_list_ai_tools_with_server(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $server = McpServer::create([ + 'url' => 'https://example.com/mcp', + 'server_label' => 'Test Server', + 'timeout' => '10', + 'discovery_timeout' => '10', + ]); + + $tool = AiTool::create([ + 'type' => 'mcp', + 'name' => 'my-test-tool', + 'status' => 'active', + 'server_id' => $server->id, + ]); + + $response = $this->jsonApi('get', '/api/ai-tools?include=server') + ->assertOk(); + + $data = collect($response->json('data')); + $toolResource = $data->first(fn($item) => $item['id'] === (string) $tool->id); + $idx = array_search($toolResource, $data->all()); + + $response->assertJson([ + 'data' => [ + $idx => [ + 'id' => (string) $tool->id, + 'type' => 'ai-tools', + 'relationships' => [ + 'server' => [ + 'data' => [ + 'id' => (string) $server->id, + 'type' => 'mcp-servers', + ], + ], + ], + ], + ], + ]); + + $included = collect($response->json('included')); + $serverResource = $included->first(fn($item) => $item['type'] === 'mcp-servers' && $item['id'] === (string) $server->id); + + $response->assertJson([ + 'included' => [ + array_search($serverResource, $included->all()) => [ + 'id' => (string) $server->id, + 'type' => 'mcp-servers', + 'attributes' => [ + 'url' => 'https://example.com/mcp', + 'server_label' => 'Test Server', + 'version' => null, + 'protocolVersion' => null, + 'description' => null, + 'require_approval' => 'never', + 'timeout' => '10', + 'discovery_timeout' => '10', + 'created_at' => $server->created_at->toJson(), + 'updated_at' => $server->updated_at->toJson(), + ], + ], + ], + ]); + + $this->assertArrayNotHasKey('api_key', $serverResource['attributes']); + } + + public function test_can_list_ai_tools_with_models(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $tool = AiTool::create([ + 'type' => 'function', + 'name' => 'my-model-tool', + 'status' => 'active', + ]); + + $provider = AiProvider::create([ + 'provider_id' => 'test-provider', + 'name' => 'Test Provider', + 'active' => true, + 'api_url' => 'https://api.example.com', + ]); + + $model = AiModel::create([ + 'model_id' => 'test-model-1', + 'label' => 'Test Model', + 'active' => true, + 'provider_id' => $provider->id, + ]); + + $tool->models()->attach($model->id); + + $response = $this->jsonApi('get', '/api/ai-tools?include=models') + ->assertOk(); + + $data = collect($response->json('data')); + $toolResource = $data->first(fn($item) => $item['id'] === (string) $tool->id); + $idx = array_search($toolResource, $data->all()); + + $response->assertJson([ + 'data' => [ + $idx => [ + 'id' => (string) $tool->id, + 'type' => 'ai-tools', + 'relationships' => [ + 'models' => [ + 'data' => [ + [ + 'id' => (string) $model->id, + 'type' => 'ai-models', + ], + ], + ], + ], + ], + ], + ]); + + $included = collect($response->json('included')); + $modelResource = $included->first(fn($item) => $item['type'] === 'ai-models' && $item['id'] === (string) $model->id); + + $response->assertJson([ + 'included' => [ + array_search($modelResource, $included->all()) => [ + 'id' => (string) $model->id, + 'type' => 'ai-models', + 'attributes' => [ + 'active' => true, + 'model_id' => 'test-model-1', + 'label' => 'Test Model', + 'input' => null, + 'output' => null, + 'tools' => null, + 'default_params' => null, + 'created_at' => $model->created_at->toJson(), + 'updated_at' => $model->updated_at->toJson(), + ], + ], + ], + ]); + } + + public function test_ai_tool_without_server_has_null_server_relationship(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $tool = AiTool::create([ + 'type' => 'function', + 'name' => 'standalone-tool', + 'status' => 'active', + ]); + + $response = $this->jsonApi('get', '/api/ai-tools') + ->assertOk(); + + $data = collect($response->json('data')); + $toolResource = $data->first(fn($item) => $item['id'] === (string) $tool->id); + + $response->assertJson([ + 'data' => [ + array_search($toolResource, $data->all()) => [ + 'id' => (string) $tool->id, + 'type' => 'ai-tools', + 'attributes' => [ + 'type' => 'function', + 'name' => 'standalone-tool', + 'status' => 'active', + ], + ], + ], + ]); + + $this->assertNull($toolResource['relationships']['server']['data'] ?? null); + } + + public function test_ai_tools_pagination(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + foreach (range(1, 15) as $i) { + AiTool::create([ + 'type' => 'function', + 'name' => "tool-{$i}", + 'status' => 'active', + ]); + } + + $response = $this->jsonApi('get', '/api/ai-tools?' . http_build_query(['page' => ['size' => 5]])) + ->assertOk() + ->assertJsonCount(5, 'data'); + + $response->assertJsonStructure([ + 'meta' => [ + 'page' => ['currentPage', 'from', 'to', 'perPage', 'lastPage', 'total'], + ], + 'links' => ['first', 'last', 'next'], + ]); + } + + public function test_can_show_single_ai_tool(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $tool = AiTool::create([ + 'type' => 'function', + 'name' => 'single-tool', + 'description' => 'Single tool desc', + 'status' => 'active', + ]); + + $this->jsonApi('get', "/api/ai-tools/{$tool->id}") + ->assertOk() + ->assertJson([ + 'data' => [ + 'id' => (string) $tool->id, + 'type' => 'ai-tools', + 'attributes' => [ + 'type' => 'function', + 'name' => 'single-tool', + 'class_name' => null, + 'description' => 'Single tool desc', + 'capability' => null, + 'status' => 'active', + 'active' => true, + 'inputSchema' => null, + 'created_at' => $tool->created_at->toJson(), + 'updated_at' => $tool->updated_at->toJson(), + ], + ], + ]); + } +} diff --git a/tests/Feature/Api/Assistant/ChatTestTest.php b/tests/Feature/Api/Assistant/ChatTestTest.php new file mode 100644 index 000000000..01eb056ca --- /dev/null +++ b/tests/Feature/Api/Assistant/ChatTestTest.php @@ -0,0 +1,359 @@ + [ + 'type' => 'assistants', + 'id' => (string) ($overrides['id'] ?? '1'), + 'attributes' => array_merge([ + 'messages' => [ + ['role' => 'user', 'content' => ['text' => 'Hello']], + ], + ], $overrides['attributes'] ?? []), + ], + ]; + } + + private function parseSseEvents(string $body): array + { + $events = []; + $currentEvent = null; + + foreach (explode("\n", $body) as $line) { + if (str_starts_with($line, 'event: ')) { + $currentEvent = ['event' => substr($line, 7), 'data' => null]; + } elseif (str_starts_with($line, 'data: ') && $currentEvent !== null) { + $currentEvent['data'] = json_decode(substr($line, 6), true); + } elseif ($line === '' && $currentEvent !== null) { + $events[] = $currentEvent; + $currentEvent = null; + } + } + + if ($currentEvent !== null) { + $events[] = $currentEvent; + } + + return $events; + } + + private function performStreamingRequest(string $uri, array $data): array + { + $captured = ''; + ob_start(function (string $buffer) use (&$captured): string { + $captured .= $buffer; + + return ''; + }); + $response = $this->jsonApi('post', $uri, $data); + ob_end_clean(); + + if ($captured === '') { + ob_start(function (string $buffer) use (&$captured): string { + $captured .= $buffer; + + return ''; + }); + $response->baseResponse->sendContent(); + ob_end_clean(); + } + + return [$response, $captured]; + } + + private function mockRunner(array $chunks): void + { + $runner = $this->createStub(AssistantChatRunnerInterface::class); + $runner->method('stream')->willReturn((function () use ($chunks) { + foreach ($chunks as $chunk) { + yield $chunk; + } + })()); + + $this->app->instance(AssistantChatRunnerInterface::class, $runner); + } + + public function test_guest_cannot_chat_test(): void + { + $owner = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'public', + ]); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/chat-test", $this->createChatTestPayload(['id' => $assistant->id])) + ->assertUnauthorized(); + } + + public function test_cannot_chat_test_nonexistent_assistant(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $this->jsonApi('post', '/api/assistants/999/actions/chat-test', $this->createChatTestPayload(['id' => 999])) + ->assertNotFound(); + } + + public function test_cannot_chat_test_private_assistant_of_other_user(): void + { + $owner = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'private', + ]); + + $otherUser = User::factory()->create(); + Sanctum::actingAs($otherUser); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/chat-test", $this->createChatTestPayload(['id' => $assistant->id])) + ->assertForbidden(); + } + + public function test_chat_test_returns_sse_content_type(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => 'private', + 'model' => 'gpt-4', + ]); + Sanctum::actingAs($user); + + $this->mockRunner([ + ['type' => 'text_delta', 'content' => 'Hi'], + ]); + + $response = $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/chat-test", $this->createChatTestPayload(['id' => $assistant->id])); + + $response->assertStatus(200); + $this->assertStringStartsWith('text/event-stream', $response->headers->get('Content-Type')); + } + + public function test_chat_test_streams_text_deltas(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => 'private', + 'model' => 'gpt-4', + ]); + Sanctum::actingAs($user); + + $this->mockRunner([ + ['type' => 'text_delta', 'content' => 'Hel'], + ['type' => 'text_delta', 'content' => 'lo'], + ]); + + [$response, $body] = $this->performStreamingRequest( + "/api/assistants/{$assistant->id}/actions/chat-test", + $this->createChatTestPayload(['id' => $assistant->id]), + ); + + $response->assertStatus(200); + $events = $this->parseSseEvents($body); + + $deltas = array_filter($events, fn ($e) => $e['event'] === 'text_delta'); + $this->assertCount(2, $deltas); + $this->assertEquals('Hel', $events[1]['data']); + $this->assertEquals('lo', $events[2]['data']); + } + + public function test_chat_test_sends_message_event_with_full_content(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => 'private', + 'model' => 'gpt-4', + ]); + Sanctum::actingAs($user); + + $this->mockRunner([ + ['type' => 'text_delta', 'content' => 'Hello'], + ['type' => 'text_delta', 'content' => ' world'], + ]); + + [$response, $body] = $this->performStreamingRequest( + "/api/assistants/{$assistant->id}/actions/chat-test", + $this->createChatTestPayload(['id' => $assistant->id]), + ); + + $response->assertStatus(200); + $events = $this->parseSseEvents($body); + + $messageEvents = array_filter($events, fn ($e) => $e['event'] === 'message'); + $this->assertCount(1, $messageEvents); + + $messageEvent = array_values($messageEvents)[0]; + $this->assertEquals('messages', $messageEvent['data']['type']); + $this->assertEquals('completed', $messageEvent['data']['attributes']['status']); + $this->assertEquals('gpt-4', $messageEvent['data']['attributes']['model']); + $this->assertEquals('Hello world', $messageEvent['data']['attributes']['content']); + } + + public function test_chat_test_sends_stream_start_and_end(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => 'private', + 'model' => 'gpt-4', + ]); + Sanctum::actingAs($user); + + $this->mockRunner([ + ['type' => 'text_delta', 'content' => 'test'], + ]); + + [$response, $body] = $this->performStreamingRequest( + "/api/assistants/{$assistant->id}/actions/chat-test", + $this->createChatTestPayload(['id' => $assistant->id]), + ); + + $response->assertStatus(200); + $events = $this->parseSseEvents($body); + + $eventTypes = array_map(fn ($e) => $e['event'], $events); + $this->assertContains('stream_start', $eventTypes); + $this->assertContains('stream_end', $eventTypes); + $this->assertContains('message', $eventTypes); + + $startEvent = array_values(array_filter($events, fn ($e) => $e['event'] === 'stream_start'))[0]; + $this->assertEquals('gpt-4', $startEvent['data']['model']); + + $endEvent = array_values(array_filter($events, fn ($e) => $e['event'] === 'stream_end'))[0]; + $this->assertEquals('stop', $endEvent['data']['reason']); + } + + public function test_chat_test_handles_error(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => 'private', + 'model' => 'gpt-4', + ]); + Sanctum::actingAs($user); + + $runner = $this->createStub(AssistantChatRunnerInterface::class); + $runner->method('stream')->willThrowException(new \RuntimeException('AI provider error')); + $this->app->instance(AssistantChatRunnerInterface::class, $runner); + + [$response, $body] = $this->performStreamingRequest( + "/api/assistants/{$assistant->id}/actions/chat-test", + $this->createChatTestPayload(['id' => $assistant->id]), + ); + + $response->assertStatus(200); + $events = $this->parseSseEvents($body); + + $failedEvents = array_filter($events, fn ($e) => $e['event'] === 'stream_failed'); + $this->assertCount(1, $failedEvents); + + $failedEvent = array_values($failedEvents)[0]; + $this->assertStringContainsString('AI provider error', $failedEvent['data']['message']); + } + + public function test_chat_test_streams_tool_call_and_result(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => 'private', + 'model' => 'gpt-4', + ]); + Sanctum::actingAs($user); + + $this->mockRunner([ + ['type' => 'tool_call', 'content' => ['tool_id' => 't1', 'tool_name' => 'search', 'arguments' => ['q' => 'test']]], + ['type' => 'tool_result', 'content' => ['tool_id' => 't1', 'tool_name' => 'search', 'result' => 'found']], + ['type' => 'text_delta', 'content' => 'Based on the results...'], + ]); + + [$response, $body] = $this->performStreamingRequest( + "/api/assistants/{$assistant->id}/actions/chat-test", + $this->createChatTestPayload(['id' => $assistant->id]), + ); + + $response->assertStatus(200); + $events = $this->parseSseEvents($body); + $eventTypes = array_map(fn ($e) => $e['event'], $events); + + $this->assertContains('tool_call', $eventTypes); + $this->assertContains('tool_result', $eventTypes); + $this->assertContains('text_delta', $eventTypes); + + $toolCallEvents = array_values(array_filter($events, fn ($e) => $e['event'] === 'tool_call')); + $this->assertEquals('t1', $toolCallEvents[0]['data']['tool_id']); + $this->assertEquals('search', $toolCallEvents[0]['data']['tool_name']); + $this->assertEquals(['q' => 'test'], $toolCallEvents[0]['data']['arguments']); + + $toolResultEvents = array_values(array_filter($events, fn ($e) => $e['event'] === 'tool_result')); + $this->assertEquals('t1', $toolResultEvents[0]['data']['tool_id']); + $this->assertEquals('found', $toolResultEvents[0]['data']['result']); + } + + public function test_chat_test_validates_required_messages(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create(['creator_id' => $user->id]); + Sanctum::actingAs($user); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/chat-test", [ + 'data' => [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => [], + ], + ]) + ->assertStatus(422); + } + + public function test_chat_test_passes_system_prompt_to_runner(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'model' => 'gpt-4', + 'system_prompt' => 'You are a helpful test assistant.', + ]); + Sanctum::actingAs($user); + + $runner = $this->createMock(AssistantChatRunnerInterface::class); + $runner->expects($this->once())->method('stream')->with( + 'You are a helpful test assistant.', + $this->callback(fn ($v) => is_array($v)), + 'gpt-4', + $this->callback(fn ($v) => is_array($v)), + $this->callback(fn ($v) => is_array($v)), + )->willReturn((function () { + yield from []; + })()); + $this->app->instance(AssistantChatRunnerInterface::class, $runner); + + $this->performStreamingRequest( + "/api/assistants/{$assistant->id}/actions/chat-test", + $this->createChatTestPayload([ + 'id' => $assistant->id, + 'attributes' => [ + 'tools' => [['type' => 'function']], + 'params' => ['temperature' => 0.7], + ], + ]), + ); + } +} diff --git a/tests/Feature/Api/Assistant/DestroyTest.php b/tests/Feature/Api/Assistant/DestroyTest.php new file mode 100644 index 000000000..5aa771a3c --- /dev/null +++ b/tests/Feature/Api/Assistant/DestroyTest.php @@ -0,0 +1,42 @@ +create(); + $assistant = Assistant::factory()->create(['creator_id' => $user->id]); + + Sanctum::actingAs($user); + + $this->jsonApi('delete',"/api/assistants/{$assistant->id}") + ->assertNoContent(); + + $this->assertDatabaseMissing('assistants', ['id' => $assistant->id]); + } + + public function test_cannot_delete_other_user_assistant(): void + { + $owner = User::factory()->create(); + $other = User::factory()->create(); + $assistant = Assistant::factory()->create(['creator_id' => $owner->id]); + + Sanctum::actingAs($other); + + $this->jsonApi('delete',"/api/assistants/{$assistant->id}") + ->assertForbidden() + ->assertJson(['errors' => [['detail' => 'This action is unauthorized.']]]); + + $this->assertDatabaseHas('assistants', ['id' => $assistant->id]); + } +} diff --git a/tests/Feature/Api/Assistant/FavoriteTest.php b/tests/Feature/Api/Assistant/FavoriteTest.php new file mode 100644 index 000000000..efb60307b --- /dev/null +++ b/tests/Feature/Api/Assistant/FavoriteTest.php @@ -0,0 +1,295 @@ +create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'public', + ]); + + $viewer = User::factory()->create(); + Sanctum::actingAs($viewer); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/favorite", [ + 'data' => [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => [ + 'is_favorite' => true, + ], + ], + ]) + ->assertSuccessful(); + + $this->assertDatabaseHas('assistant_favorite_users', [ + 'assistant_id' => $assistant->id, + 'user_id' => $viewer->id, + ]); + } + + public function test_can_unfavorite_assistant(): void + { + $owner = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'public', + ]); + + $viewer = User::factory()->create(); + $viewer->favoriteAssistants()->attach($assistant->id); + + Sanctum::actingAs($viewer); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/favorite", [ + 'data' => [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => [ + 'is_favorite' => false, + ], + ], + ]) + ->assertSuccessful(); + + $this->assertDatabaseMissing('assistant_favorite_users', [ + 'assistant_id' => $assistant->id, + 'user_id' => $viewer->id, + ]); + } + + public function test_mark_favorite_is_unique_for_specific_user(): void + { + $owner = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'public', + ]); + + $viewer = User::factory()->create(); + Sanctum::actingAs($viewer); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/favorite", [ + 'data' => [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => [ + 'is_favorite' => true, + ], + ], + ]) + ->assertSuccessful(); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/favorite", [ + 'data' => [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => [ + 'is_favorite' => true, + ], + ], + ]) + ->assertSuccessful(); + + $this->assertEquals(1, $viewer->favoriteAssistants()->count()); + } + + public function test_creator_can_favorite_own_assistant(): void + { + $owner = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'private', + ]); + + Sanctum::actingAs($owner); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/favorite", [ + 'data' => [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => [ + 'is_favorite' => true, + ], + ], + ]) + ->assertSuccessful(); + + $this->assertDatabaseHas('assistant_favorite_users', [ + 'assistant_id' => $assistant->id, + 'user_id' => $owner->id, + ]); + } + + public function test_cannot_favorite_private_assistant_of_other_user(): void + { + $owner = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'private', + ]); + + $otherUser = User::factory()->create(); + Sanctum::actingAs($otherUser); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/favorite", [ + 'data' => [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => [ + 'is_favorite' => true, + ], + ], + ]) + ->assertForbidden(); + + $this->assertEquals(0, $otherUser->favoriteAssistants()->count()); + } + + public function test_cannot_unfavorite_private_assistant_of_other_user(): void + { + $owner = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'private', + ]); + + $otherUser = User::factory()->create(); + Sanctum::actingAs($otherUser); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/favorite", [ + 'data' => [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => [ + 'is_favorite' => false, + ], + ], + ]) + ->assertForbidden(); + } + + public function test_guest_cannot_favorite(): void + { + $owner = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'public', + ]); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/favorite", [ + 'data' => [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => [ + 'is_favorite' => true, + ], + ], + ]) + ->assertUnauthorized(); + } + + public function test_favorite_requires_is_favorite_attribute(): void + { + $owner = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'public', + ]); + + Sanctum::actingAs($owner); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/favorite", [ + 'data' => [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => [], + ], + ]) + ->assertStatus(422); + } + + public function test_favorite_validates_is_favorite_is_boolean(): void + { + $owner = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'public', + ]); + + Sanctum::actingAs($owner); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/favorite", [ + 'data' => [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => [ + 'is_favorite' => 'not-a-boolean', + ], + ], + ]) + ->assertStatus(422); + } + + public function test_favorite_response_includes_is_favorite_true(): void + { + $owner = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'public', + ]); + + Sanctum::actingAs($owner); + + $response = $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/favorite", [ + 'data' => [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => [ + 'is_favorite' => true, + ], + ], + ]); + + $response->assertSuccessful(); + $response->assertJsonPath('data.attributes.is_favorite', true); + } + + public function test_unfavorite_response_includes_is_favorite_false(): void + { + $owner = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'public', + ]); + + $owner->favoriteAssistants()->attach($assistant->id); + + Sanctum::actingAs($owner); + + $response = $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/favorite", [ + 'data' => [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => [ + 'is_favorite' => false, + ], + ], + ]); + + $response->assertSuccessful(); + $response->assertJsonPath('data.attributes.is_favorite', false); + } +} diff --git a/tests/Feature/Api/Assistant/FeedbackTest.php b/tests/Feature/Api/Assistant/FeedbackTest.php new file mode 100644 index 000000000..a24d3e247 --- /dev/null +++ b/tests/Feature/Api/Assistant/FeedbackTest.php @@ -0,0 +1,136 @@ +create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'public', + ]); + + $viewer = User::factory()->create(); + Sanctum::actingAs($viewer); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/feedback", [ + 'data' => [ + 'type' => 'assistants', + 'attributes' => [ + 'text' => 'Great assistant!', + ], + ], + ]) + ->assertSuccessful(); + + $this->assertDatabaseHas('feedback', [ + 'assistant_id' => $assistant->id, + 'user_id' => $viewer->id, + 'text' => 'Great assistant!', + ]); + + $this->assertEquals(1, $assistant->feedback()->count()); + } + + public function test_creator_can_add_feedback_to_own_assistant(): void + { + $owner = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'private', + ]); + + Sanctum::actingAs($owner); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/feedback", [ + 'data' => [ + 'type' => 'assistants', + 'attributes' => [ + 'text' => 'My own feedback', + ], + ], + ]) + ->assertSuccessful(); + + $this->assertDatabaseHas('feedback', [ + 'assistant_id' => $assistant->id, + 'user_id' => $owner->id, + 'text' => 'My own feedback', + ]); + } + + public function test_cannot_add_feedback_to_private_assistant_of_other_user(): void + { + $owner = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'private', + ]); + + $otherUser = User::factory()->create(); + Sanctum::actingAs($otherUser); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/feedback", [ + 'data' => [ + 'type' => 'assistants', + 'attributes' => [ + 'text' => 'Should fail', + ], + ], + ]) + ->assertForbidden(); + + $this->assertEquals(0, Feedback::count()); + } + + public function test_feedback_requires_text(): void + { + $owner = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'public', + ]); + + Sanctum::actingAs($owner); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/feedback", [ + 'data' => [ + 'type' => 'assistants', + 'attributes' => [], + ], + ]) + ->assertStatus(422); + + $this->assertEquals(0, Feedback::count()); + } + + public function test_guest_cannot_add_feedback(): void + { + $owner = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'public', + ]); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/feedback", [ + 'data' => [ + 'type' => 'assistants', + 'attributes' => [ + 'text' => 'Guest feedback', + ], + ], + ]) + ->assertUnauthorized(); + } +} diff --git a/tests/Feature/Api/Assistant/Fixtures/Assistant.php b/tests/Feature/Api/Assistant/Fixtures/Assistant.php new file mode 100644 index 000000000..ce9608d45 --- /dev/null +++ b/tests/Feature/Api/Assistant/Fixtures/Assistant.php @@ -0,0 +1,85 @@ + 'Test Assistant', + 'system_prompt' => 'You are a helpful assistant.', + 'greeting' => 'Hello!', + 'description' => 'A test assistant.', + 'detail_description' => 'Detailed description here.', + 'allow_remix' => true, + 'allow_model_select' => false, + 'release_stage' => 'private', + 'formality' => 'neutral', + 'model' => 'gpt-4', + 'model_length' => 2048, + 'model_temp' => 0.7, + 'model_top_p' => 0.9, + ], $overrides); + } + + private function createRelationships(array $rels = []): array + { + $defaults = []; + if (isset($rels['language'])) { + $defaults['language'] = ['data' => ['type' => 'assistant-languages', 'id' => (string) $rels['language']]]; + } + if (isset($rels['category'])) { + $defaults['category'] = ['data' => ['type' => 'assistant-categories', 'id' => (string) $rels['category']]]; + } + if (isset($rels['tags'])) { + $defaults['tags'] = ['data' => array_map(fn ($id) => ['type' => 'tags', 'id' => (string) $id], $rels['tags'])]; + } + if (isset($rels['ai_tools'])) { + $defaults['ai_tools'] = ['data' => array_map(fn ($id) => ['type' => 'ai-tools', 'id' => (string) $id], $rels['ai_tools'])]; + } + if (isset($rels['user_prompts'])) { + $defaults['user_prompts'] = ['data' => array_map(fn ($id) => ['type' => 'user-prompts', 'id' => (string) $id], $rels['user_prompts'])]; + } + return $defaults; + } + + private function createJsonApiPayload(array $attrOverrides = [], array $relOverrides = []): array + { + $doc = [ + 'data' => [ + 'type' => 'assistants', + 'attributes' => $this->createPayload($attrOverrides), + ], + ]; + $rels = $this->createRelationships($relOverrides); + if ($rels) { + $doc['data']['relationships'] = $rels; + } + return $doc; + } + + private function createAiTool(): AiTool + { + $serverId = DB::table('mcp_servers')->insertGetId([ + 'url' => 'https://example.com/mcp/' . uniqid(), + 'server_label' => 'Test Server ' . uniqid(), + 'timeout' => '10', + 'discovery_timeout' => '10', + 'api_key' => 'test-key', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + return AiTool::create([ + 'type' => 'function', + 'name' => 'test_tool_' . uniqid(), + 'description' => 'A test tool', + 'status' => 'active', + 'server_id' => $serverId, + ]); + } +} diff --git a/tests/Feature/Api/Assistant/IndexTest.php b/tests/Feature/Api/Assistant/IndexTest.php new file mode 100644 index 000000000..9b9632d0e --- /dev/null +++ b/tests/Feature/Api/Assistant/IndexTest.php @@ -0,0 +1,546 @@ +jsonApi('get', '/api/assistants') + ->assertUnauthorized() + ->assertJson(['errors' => [['detail' => 'Unauthenticated.']]]); + } + + public function test_can_list_assistants(): void + { + $user = User::factory()->create(); + $assistants = Assistant::factory()->count(3)->create(['creator_id' => $user->id, 'allow_remix' => true]); + + Sanctum::actingAs($user); + + $response = $this->jsonApi('get', '/api/assistants') + ->assertOk() + ->assertJsonCount(3, 'data'); + + foreach ($assistants as $i => $assistant) { + $response + ->assertJson([ + 'data' => [ + $i => [ + 'id' => (string) $assistant->id, + 'type' => 'assistants', + 'attributes' => [ + 'name' => $assistant->name, + 'handle' => $assistant->handle, + 'system_prompt' => $assistant->system_prompt, + 'greeting' => $assistant->greeting, + 'description' => $assistant->description, + 'detail_description' => $assistant->detail_description, + 'allow_remix' => (int) $assistant->allow_remix, + 'allow_model_select' => (int) $assistant->allow_model_select, + 'release_stage' => $assistant->release_stage, + 'formality' => $assistant->formality, + 'model' => $assistant->model, + 'model_length' => $assistant->model_length, + 'model_temp' => $assistant->model_temp, + 'model_top_p' => $assistant->model_top_p, + 'created_at' => $assistant->created_at->toJson(), + 'updated_at' => $assistant->updated_at->toJson(), + ], + 'links' => [ + 'self' => config('app.url') . "/api/assistants/{$assistant->id}", + 'remix' => [ + 'href' => config('app.url') . "/api/assistants/{$assistant->id}/actions/remix", + 'meta' => ['message' => 'ALLOWED'], + ], + 'release' => [ + 'href' => config('app.url') . "/api/assistants/{$assistant->id}/actions/release", + 'meta' => ['message' => 'ALLOWED'], + ], + 'feedback' => [ + 'href' => config('app.url') . "/api/assistants/{$assistant->id}/actions/feedback", + 'meta' => ['message' => 'ALLOWED'], + ], + ], + ], + ], + ]); + } + } + + public function test_can_list_assistants_with_relations(): void + { + $user = User::factory()->create(); + Assistant::factory()->create(['creator_id' => $user->id]); + + Sanctum::actingAs($user); + + $response = $this->jsonApi('get', '/api/assistants?include=creator,user_prompts') + ->assertOk(); + + $response->assertJsonPath('data.0.relationships.creator.data.id', (string) $user->id); + $response->assertJsonPath('data.0.relationships.creator.data.type', 'users'); + $response->assertJsonPath('data.0.relationships.user_prompts.data', []); + + $included = collect($response->json('included')); + $creatorResource = $included->first(fn($item) => $item['type'] === 'users'); + $this->assertEquals($user->name, $creatorResource['attributes']['name']); + } + + public function test_can_list_assistants_with_versions(): void + { + $user = User::factory()->create(); + Assistant::factory()->create(['creator_id' => $user->id]); + + Sanctum::actingAs($user); + + $response = $this->jsonApi('get', '/api/assistants?include=versions') + ->assertOk(); + + $included = collect($response->json('included')); + $versionResource = $included->first(fn($item) => $item['type'] === 'versions'); + $this->assertEquals('Initial version', $versionResource['attributes']['text']); + $this->assertEquals('1.0', $versionResource['attributes']['version']); + } + + public function test_can_filter_assistants_by_category(): void + { + $user = User::factory()->create(); + $education = Category::factory()->create(['text' => 'education']); + $general = Category::factory()->create(['text' => 'general']); + Assistant::factory()->create(['creator_id' => $user->id, 'category_id' => $education->id]); + Assistant::factory()->create(['creator_id' => $user->id, 'category_id' => $general->id]); + Assistant::factory()->create(['creator_id' => $user->id, 'category_id' => $education->id]); + + Sanctum::actingAs($user); + + $response = $this->jsonApi('get', '/api/assistants?filter[category][text]=education&include=category') + ->assertOk() + ->assertJsonCount(2, 'data'); + + $included = collect($response->json('included')); + $catResources = $included->filter(fn($item) => $item['type'] === 'assistant-categories'); + foreach ($catResources as $catResource) { + $this->assertEquals('education', $catResource['attributes']['text']); + } + } + + public function test_can_filter_assistants_by_multiple_categories(): void + { + $user = User::factory()->create(); + $education = Category::factory()->create(['text' => 'education']); + $general = Category::factory()->create(['text' => 'general']); + $science = Category::factory()->create(['text' => 'science']); + Assistant::factory()->create(['creator_id' => $user->id, 'category_id' => $education->id]); + Assistant::factory()->create(['creator_id' => $user->id, 'category_id' => $general->id]); + Assistant::factory()->create(['creator_id' => $user->id, 'category_id' => $science->id]); + + Sanctum::actingAs($user); + + $response = $this->jsonApi('get', '/api/assistants?filter[category][text]=education,general&include=category') + ->assertOk() + ->assertJsonCount(2, 'data'); + + $included = collect($response->json('included')); + $catResources = $included->filter(fn($item) => $item['type'] === 'assistant-categories'); + foreach ($catResources as $catResource) { + $this->assertContains($catResource['attributes']['text'], ['education', 'general']); + } + } + + public function test_filter_by_category_returns_empty_when_no_match(): void + { + $user = User::factory()->create(); + $general = Category::factory()->create(['text' => 'general']); + Assistant::factory()->create(['creator_id' => $user->id, 'category_id' => $general->id]); + + Sanctum::actingAs($user); + + $this->jsonApi('get', '/api/assistants?filter[category][text]=nonexistent') + ->assertOk() + ->assertJsonCount(0, 'data'); + } + + public function test_list_without_category_filter_returns_all(): void + { + $user = User::factory()->create(); + Assistant::factory()->count(3)->create(['creator_id' => $user->id]); + + Sanctum::actingAs($user); + + $this->jsonApi('get', '/api/assistants') + ->assertOk() + ->assertJsonCount(3, 'data'); + } + + public function test_can_list_assistants_with_organization(): void + { + $org = Organization::first(); + + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'organization_id' => $org->id, + ]); + + Sanctum::actingAs($user); + + $response = $this->jsonApi('get', '/api/assistants?include=organization') + ->assertOk(); + + $response->assertJson([ + 'data' => [ + [ + 'id' => (string) $assistant->id, + 'relationships' => [ + 'organization' => [ + 'data' => [ + 'id' => (string) $org->id, + 'type' => 'organizations', + ], + ], + ], + ], + ], + ]); + + $included = collect($response->json('included')); + $orgResource = $included->first(fn($item) => $item['type'] === 'organizations'); + $this->assertEquals($org->name, $orgResource['attributes']['name']); + } + + public function test_can_list_assistants_with_language_and_category(): void + { + $language = Language::factory()->create(['text' => 'de']); + $category = Category::factory()->create(['text' => 'education']); + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'language_id' => $language->id, + 'category_id' => $category->id, + ]); + + Sanctum::actingAs($user); + + $response = $this->jsonApi('get', '/api/assistants?include=language,category') + ->assertOk(); + + $response->assertJson([ + 'data' => [ + [ + 'id' => (string) $assistant->id, + 'relationships' => [ + 'language' => [ + 'data' => [ + 'id' => (string) $language->id, + 'type' => 'assistant-languages', + ], + ], + 'category' => [ + 'data' => [ + 'id' => (string) $category->id, + 'type' => 'assistant-categories', + ], + ], + ], + ], + ], + ]); + + $included = collect($response->json('included')); + $langResource = $included->first(fn($item) => $item['type'] === 'assistant-languages'); + $this->assertEquals('de', $langResource['attributes']['text']); + $catResource = $included->first(fn($item) => $item['type'] === 'assistant-categories'); + $this->assertEquals('education', $catResource['attributes']['text']); + } + + public function test_user_cannot_see_other_users_private_assistant_in_list(): void + { + $owner = User::factory()->create(); + $other = User::factory()->create(); + + Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'private', + ]); + + Sanctum::actingAs($other); + + $this->jsonApi('get', '/api/assistants') + ->assertOk() + ->assertJsonCount(0, 'data'); + } + + public function test_user_can_see_own_private_assistant_in_list(): void + { + $user = User::factory()->create(); + + Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => 'private', + ]); + + Sanctum::actingAs($user); + + $this->jsonApi('get', '/api/assistants') + ->assertOk() + ->assertJsonCount(1, 'data'); + } + + public function test_pagination_links_preserve_query_params(): void + { + $user = User::factory()->create(); + $general = Category::factory()->create(['text' => 'general']); + Category::factory()->create(['text' => 'education']); + Assistant::factory()->count(20)->create([ + 'creator_id' => $user->id, + 'category_id' => $general->id, + ]); + + Sanctum::actingAs($user); + + $query = http_build_query([ + 'include' => 'tags,category', + 'fields' => ['tags' => 'text'], + 'filter' => ['category' => ['text' => 'general']], + 'page' => ['size' => 5], + ]); + + $response = $this->jsonApi('get', "/api/assistants?{$query}") + ->assertOk() + ->assertJsonStructure([ + 'links' => ['first', 'last', 'next'], + ]); + + $links = $response->json('links'); + + $this->assertStringContainsString('include=tags%2Ccategory', $links['first']); + $this->assertStringContainsString('fields%5Btags%5D=text', $links['first']); + $this->assertStringContainsString('filter%5Bcategory%5D%5Btext%5D=general', $links['first']); + $this->assertStringContainsString('page%5Bsize%5D=5', $links['first']); + $this->assertStringContainsString('page%5Bnumber%5D=1', $links['first']); + $this->assertStringContainsString('page%5Bnumber%5D=2', $links['next']); + } + + public function test_pagination_advances_pages_correctly(): void + { + $user = User::factory()->create(); + Assistant::factory()->count(2)->create([ + 'creator_id' => $user->id, + 'release_stage' => 'federated', + ]); + + Sanctum::actingAs($user); + + $page1Query = http_build_query(['page' => ['size' => 1]]); + + $page1 = $this->jsonApi('get', "/api/assistants?{$page1Query}") + ->assertOk() + ->assertJsonCount(1, 'data'); + + $this->assertStringContainsString('page%5Bnumber%5D=2', $page1->json('links.next')); + $this->assertNull($page1->json('links.prev')); + + $page2Query = http_build_query(['page' => ['size' => 1, 'number' => 2]]); + + $page2 = $this->jsonApi('get', "/api/assistants?{$page2Query}") + ->assertOk() + ->assertJsonCount(1, 'data'); + + $this->assertStringContainsString('page%5Bnumber%5D=1', $page2->json('links.prev')); + $this->assertNull($page2->json('links.next')); + } + + public function test_user_can_see_federated_assistant_in_list(): void + { + $owner = User::factory()->create(); + $other = User::factory()->create(); + + Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'federated', + ]); + + Sanctum::actingAs($other); + + $this->jsonApi('get', '/api/assistants') + ->assertOk() + ->assertJsonCount(1, 'data'); + } + + public function test_list_shows_denied_action_links_for_non_owner(): void + { + $owner = User::factory()->create(); + $other = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'federated', + 'allow_remix' => false, + ]); + + Sanctum::actingAs($other); + + $response = $this->jsonApi('get', '/api/assistants') + ->assertOk() + ->assertJsonCount(1, 'data'); + + $response->assertJson([ + 'data' => [ + [ + 'id' => (string) $assistant->id, + 'links' => [ + 'remix' => [ + 'href' => config('app.url') . "/api/assistants/{$assistant->id}/actions/remix", + 'meta' => ['message' => 'DENIED'], + ], + 'release' => [ + 'href' => config('app.url') . "/api/assistants/{$assistant->id}/actions/release", + 'meta' => ['message' => 'DENIED'], + ], + 'feedback' => [ + 'href' => config('app.url') . "/api/assistants/{$assistant->id}/actions/feedback", + 'meta' => ['message' => 'ALLOWED'], + ], + ], + ], + ], + ]); + } + + public function test_can_filter_assistants_by_name_case_insensitive(): void + { + $user = User::factory()->create(); + Assistant::factory()->create(['creator_id' => $user->id, 'name' => 'Code Helper']); + Assistant::factory()->create(['creator_id' => $user->id, 'name' => 'Writing Assistant']); + Assistant::factory()->create(['creator_id' => $user->id, 'name' => 'Code Reviewer']); + + Sanctum::actingAs($user); + + $this->jsonApi('get', '/api/assistants?' . http_build_query(['filter' => ['name' => 'code']])) + ->assertOk() + ->assertJsonCount(2, 'data'); + } + + public function test_filter_by_name_is_case_insensitive(): void + { + $user = User::factory()->create(); + Assistant::factory()->create(['creator_id' => $user->id, 'name' => 'Code Helper']); + + Sanctum::actingAs($user); + + $this->jsonApi('get', '/api/assistants?' . http_build_query(['filter' => ['name' => 'CODE']])) + ->assertOk() + ->assertJsonCount(1, 'data'); + } + + public function test_filter_by_name_returns_empty_when_no_match(): void + { + $user = User::factory()->create(); + Assistant::factory()->create(['creator_id' => $user->id, 'name' => 'Code Helper']); + + Sanctum::actingAs($user); + + $this->jsonApi('get', '/api/assistants?' . http_build_query(['filter' => ['name' => 'nonexistent']])) + ->assertOk() + ->assertJsonCount(0, 'data'); + } + + public function test_is_favorite_attribute_returned_in_list(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create(['creator_id' => $user->id]); + $user->favoriteAssistants()->attach($assistant->id); + + Sanctum::actingAs($user); + + $this->jsonApi('get', '/api/assistants') + ->assertOk() + ->assertJsonPath('data.0.attributes.is_favorite', true); + } + + public function test_is_favorite_is_false_when_not_favorited(): void + { + $user = User::factory()->create(); + Assistant::factory()->create(['creator_id' => $user->id]); + + Sanctum::actingAs($user); + + $this->jsonApi('get', '/api/assistants') + ->assertOk() + ->assertJsonPath('data.0.attributes.is_favorite', false); + } + + public function test_can_filter_assistants_by_is_favorite_true(): void + { + $user = User::factory()->create(); + $favorited = Assistant::factory()->create(['creator_id' => $user->id, 'name' => 'Favorited']); + Assistant::factory()->create(['creator_id' => $user->id, 'name' => 'Not Favorited']); + $user->favoriteAssistants()->attach($favorited->id); + + Sanctum::actingAs($user); + + $this->jsonApi('get', '/api/assistants?' . http_build_query(['filter' => ['is_favorite' => 'true']])) + ->assertOk() + ->assertJsonCount(1, 'data') + ->assertJsonPath('data.0.attributes.name', 'Favorited'); + } + + public function test_can_filter_assistants_by_is_favorite_false(): void + { + $user = User::factory()->create(); + $favorited = Assistant::factory()->create(['creator_id' => $user->id, 'name' => 'Favorited']); + Assistant::factory()->create(['creator_id' => $user->id, 'name' => 'Not Favorited A']); + Assistant::factory()->create(['creator_id' => $user->id, 'name' => 'Not Favorited B']); + $user->favoriteAssistants()->attach($favorited->id); + + Sanctum::actingAs($user); + + $this->jsonApi('get', '/api/assistants?' . http_build_query(['filter' => ['is_favorite' => 'false']])) + ->assertOk() + ->assertJsonCount(2, 'data'); + } + + public function test_can_filter_assistant_release_status(): void + { + $user = User::factory()->create(); + $draftAssistant = Assistant::factory()->create(['creator_id' => $user->id, 'name' => 'Release draft', 'release_stage' => ReleaseStage::DRAFT]); + Assistant::factory()->create(['creator_id' => $user->id, 'name' => 'Release organizational', 'release_stage' => ReleaseStage::ORGANIZATIONAL]); + Assistant::factory()->create(['creator_id' => $user->id, 'name' => 'Release private', 'release_stage' => ReleaseStage::PRIVATE]); + + Sanctum::actingAs($user); + + $this->jsonApi('get', '/api/assistants?' . http_build_query(['filter' => ['release_stage' => ReleaseStage::DRAFT]])) + ->assertOk() + ->assertJsonCount(1, 'data') + ->assertJsonPath('data.0.id', (string) $draftAssistant -> id); + } + + public function test_is_favorite_filter_only_scopes_to_authenticated_user(): void + { + $userA = User::factory()->create(); + $userB = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $userA->id, + 'release_stage' => 'public', + ]); + $userA->favoriteAssistants()->attach($assistant->id); + + Sanctum::actingAs($userB); + + $this->jsonApi('get', '/api/assistants?' . http_build_query(['filter' => ['is_favorite' => 'true']])) + ->assertOk() + ->assertJsonCount(0, 'data'); + } +} diff --git a/tests/Feature/Api/Assistant/ReleaseTest.php b/tests/Feature/Api/Assistant/ReleaseTest.php new file mode 100644 index 000000000..f19689afc --- /dev/null +++ b/tests/Feature/Api/Assistant/ReleaseTest.php @@ -0,0 +1,144 @@ +create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => ReleaseStage::PRIVATE->value, + ]); + + Sanctum::actingAs($user); + Event::fake(AssistantTriggerReleaseStatus::class); + + $response = $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/release", [ + 'data' => [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => [ + 'release_stage' => ReleaseStage::ORGANIZATIONAL->value, + ], + ], + ]); + + $response->assertOk(); + + $assistant->refresh(); + $this->assertEquals(ReleaseStage::ORGANIZATIONAL->value, $assistant->release_stage); + + Event::assertDispatched(AssistantTriggerReleaseStatus::class, function ($event) { + return $event->oldStage === ReleaseStage::PRIVATE + && $event->newStage === ReleaseStage::ORGANIZATIONAL; + }); + } + + public function test_cannot_release_others_assistant(): void + { + $owner = User::factory()->create(); + $other = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => ReleaseStage::PRIVATE->value, + ]); + + Sanctum::actingAs($other); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/release", [ + 'data' => [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => [ + 'release_stage' => ReleaseStage::ORGANIZATIONAL->value, + ], + ], + ]) + ->assertForbidden() + ->assertJson(['errors' => [['detail' => 'This action is unauthorized.']]]); + } + + public function test_guest_cannot_release_assistant(): void + { + $owner = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => ReleaseStage::PRIVATE->value, + ]); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/release", [ + 'data' => [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => [ + 'release_stage' => ReleaseStage::ORGANIZATIONAL->value, + ], + ], + ]) + ->assertUnauthorized(); + } + + public function test_release_with_same_stage_does_not_dispatch_event(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => ReleaseStage::PRIVATE->value, + ]); + + Sanctum::actingAs($user); + Event::fake(AssistantTriggerReleaseStatus::class); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/release", [ + 'data' => [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => [ + 'release_stage' => ReleaseStage::PRIVATE->value, + ], + ], + ]) + ->assertOk(); + + Event::assertNotDispatched(AssistantTriggerReleaseStatus::class); + } + + public function test_release_with_invalid_stage_returns_validation_error(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => ReleaseStage::PRIVATE->value, + ]); + + Sanctum::actingAs($user); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/release", [ + 'data' => [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => [ + 'release_stage' => 'invalid', + ], + ], + ]) + ->assertUnprocessable(); + } +} diff --git a/tests/Feature/Api/Assistant/RemixTest.php b/tests/Feature/Api/Assistant/RemixTest.php new file mode 100644 index 000000000..597ca58ee --- /dev/null +++ b/tests/Feature/Api/Assistant/RemixTest.php @@ -0,0 +1,300 @@ +create(); + $remixUser = User::factory()->create(); + $originalCreator = User::factory()->create(); + + $tool = $this->createAiTool(); + $tag = Tag::create(['text' => 'remix-tag']); + + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'remixed_creator_id' => $originalCreator->id, + 'allow_remix' => true, + ]); + $assistant->user_prompts()->createMany([ + ['text' => 'Prompt one'], + ['text' => 'Prompt two'], + ]); + $assistant->ai_tools()->attach($tool->id); + $assistant->tags()->attach($tag->id); + $assistant->attachments()->create([ + 'uuid' => 'test-uuid', + 'name' => 'test.png', + 'category' => 'avatar', + 'type' => 'image', + 'mime' => 'image/png', + 'user_id' => $owner->id, + ]); + + Sanctum::actingAs($remixUser); + + $response = $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/remix") + ->assertCreated(); + + $clone = Assistant::where('creator_id', $remixUser->id)->first(); + $this->assertNotNull($clone); + $this->assertEquals($remixUser->id, $clone->creator_id); + $this->assertEquals($owner->id, $clone->remixed_creator_id); + $this->assertNotEquals($assistant->id, $clone->id); + $this->assertEquals($assistant->id, $clone->remixed_assistant_id); + $this->assertEquals('private', $clone->release_stage); + + $this->assertEquals($assistant->name, $clone->name); + $this->assertEquals($assistant->system_prompt, $clone->system_prompt); + $this->assertEquals($assistant->description, $clone->description); + $this->assertEquals($assistant->greeting, $clone->greeting); + $this->assertEquals($assistant->allow_remix, $clone->allow_remix); + $this->assertEquals($assistant->allow_model_select, $clone->allow_model_select); + $this->assertEquals($assistant->model_length, $clone->model_length); + $this->assertEquals($assistant->model_temp, $clone->model_temp); + $this->assertEquals($assistant->model_top_p, $clone->model_top_p); + $this->assertEquals($assistant->model, $clone->model); + $this->assertEquals($assistant->formality, $clone->formality); + $this->assertEquals($assistant->detail_description, $clone->detail_description); + $this->assertEquals($assistant->language_id, $clone->language_id); + $this->assertEquals($assistant->category_id, $clone->category_id); + + $this->assertNull($clone->handle); + + $this->assertEquals(2, $clone->user_prompts()->count()); + $this->assertTrue($clone->tags()->where('tag_id', $tag->id)->exists()); + $this->assertEquals(1, $clone->attachments()->count()); + $this->assertEquals('test-uuid', $clone->attachments()->first()->uuid); + + $response->assertJson([ + 'data' => [ + 'id' => (string) $clone->id, + ], + ]); + } + + public function test_remix_copies_latest_version_only(): void + { + $owner = User::factory()->create(); + $remixUser = User::factory()->create(); + + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'allow_remix' => true, + ]); + $assistant->versions()->createMany([ + ['text' => 'Version 1', 'version' => 1.0], + ['text' => 'Version 2', 'version' => 2.0], + ['text' => 'Version 3', 'version' => 3.0], + ]); + + Sanctum::actingAs($remixUser); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/remix") + ->assertCreated(); + + $clone = Assistant::where('creator_id', $remixUser->id)->first(); + + $this->assertEquals(1, $clone->versions()->count()); + $latestVersion = $clone->versions()->first(); + $this->assertEquals('Version 3', $latestVersion->text); + $this->assertEquals(3.0, (float) $latestVersion->version); + } + + public function test_remix_copies_ai_tools_when_users_share_organization(): void + { + $org = Organization::create(['name' => 'Test Org']); + $owner = User::factory()->create(); + $remixUser = User::factory()->create(); + $org->users()->attach([$owner->id, $remixUser->id]); + + $tool = $this->createAiTool(); + + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'allow_remix' => true, + ]); + $assistant->ai_tools()->attach($tool->id); + + Sanctum::actingAs($remixUser); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/remix") + ->assertCreated(); + + $clone = Assistant::where('creator_id', $remixUser->id)->first(); + $this->assertTrue($clone->ai_tools()->where('ai_tool_id', $tool->id)->exists()); + } + + public function test_remix_does_not_copy_ai_tools_when_users_differ_orgs(): void + { + $org1 = Organization::create(['name' => 'Org 1']); + $org2 = Organization::create(['name' => 'Org 2']); + $owner = User::factory()->create(); + $remixUser = User::factory()->create(); + $org1->users()->attach($owner->id); + $org2->users()->attach($remixUser->id); + + $tool = $this->createAiTool(); + + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'allow_remix' => true, + ]); + $assistant->ai_tools()->attach($tool->id); + + Sanctum::actingAs($remixUser); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/remix") + ->assertCreated(); + + $clone = Assistant::where('creator_id', $remixUser->id)->first(); + $this->assertFalse($clone->ai_tools()->exists()); + } + + public function test_remix_copies_attachments(): void + { + $owner = User::factory()->create(); + $remixUser = User::factory()->create(); + + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'allow_remix' => true, + ]); + $assistant->attachments()->createMany([ + [ + 'uuid' => 'file-1', + 'name' => 'doc.pdf', + 'category' => 'document', + 'type' => 'document', + 'mime' => 'application/pdf', + 'user_id' => $owner->id, + ], + [ + 'uuid' => 'file-2', + 'name' => 'img.png', + 'category' => 'avatar', + 'type' => 'image', + 'mime' => 'image/png', + 'user_id' => $owner->id, + ], + ]); + + Sanctum::actingAs($remixUser); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/remix") + ->assertCreated(); + + $clone = Assistant::where('creator_id', $remixUser->id)->first(); + $this->assertEquals(2, $clone->attachments()->count()); + $this->assertTrue($clone->attachments()->where('uuid', 'file-1')->exists()); + $this->assertTrue($clone->attachments()->where('uuid', 'file-2')->exists()); + } + + public function test_remix_does_not_copy_reviews(): void + { + $owner = User::factory()->create(); + $remixUser = User::factory()->create(); + + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'allow_remix' => true, + ]); + $assistant->review()->create([ + 'status' => 'approved', + 'reason' => 'Looks good', + ]); + + Sanctum::actingAs($remixUser); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/remix") + ->assertCreated(); + + $clone = Assistant::where('creator_id', $remixUser->id)->first(); + $this->assertNull($clone->review); + } + + public function test_remix_set_release_stage_to_private(): void + { + $owner = User::factory()->create(); + $remixUser = User::factory()->create(); + + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'allow_remix' => true, + 'release_stage' => 'federated', + ]); + + Sanctum::actingAs($remixUser); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/remix") + ->assertCreated(); + + $clone = Assistant::where('creator_id', $remixUser->id)->first(); + $this->assertEquals('private', $clone->release_stage); + } + + public function test_remixed_creator_id_is_source_creator_id(): void + { + $originalCreator = User::factory()->create(); + $owner = User::factory()->create(); + $remixUser = User::factory()->create(); + + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'remixed_creator_id' => $originalCreator->id, + 'allow_remix' => true, + ]); + + Sanctum::actingAs($remixUser); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/remix") + ->assertCreated(); + + $clone = Assistant::where('creator_id', $remixUser->id)->first(); + $this->assertEquals($owner->id, $clone->remixed_creator_id); + } + + public function test_cannot_remix_when_not_allowed(): void + { + $owner = User::factory()->create(); + $remixer = User::factory()->create(); + + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'allow_remix' => false, + ]); + + Sanctum::actingAs($remixer); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/remix") + ->assertForbidden() + ->assertJson(['errors' => [['detail' => 'This action is unauthorized.']]]); + + $this->assertEquals(1, Assistant::count()); + } + + public function test_guest_cannot_remix_assistant(): void + { + $owner = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'allow_remix' => true, + ]); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/remix") + ->assertUnauthorized(); + } +} diff --git a/tests/Feature/Api/Assistant/ReviewTest.php b/tests/Feature/Api/Assistant/ReviewTest.php new file mode 100644 index 000000000..7aefed958 --- /dev/null +++ b/tests/Feature/Api/Assistant/ReviewTest.php @@ -0,0 +1,353 @@ +create(); + $org = Organization::first(); + $user->organizations()->attach($org, ['role' => 'admin']); + + return $user; + } + + private function createMember(): User + { + $user = User::factory()->create(); + $org = Organization::first(); + $user->organizations()->attach($org, ['role' => 'member']); + + return $user; + } + + public function test_release_creates_pending_review(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => ReleaseStage::PRIVATE->value, + ]); + + Sanctum::actingAs($user); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/release", [ + 'data' => [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => [ + 'release_stage' => ReleaseStage::ORGANIZATIONAL->value, + ], + ], + ]) + ->assertOk(); + + $this->assertDatabaseHas('reviews', [ + 'assistant_id' => $assistant->id, + 'status' => ReviewStatus::PENDING->value, + ]); + } + + public function test_release_reuses_existing_review(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => ReleaseStage::ORGANIZATIONAL->value, + ]); + Review::create([ + 'assistant_id' => $assistant->id, + 'status' => ReviewStatus::APPROVED->value, + ]); + + Sanctum::actingAs($user); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/release", [ + 'data' => [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => [ + 'release_stage' => ReleaseStage::FEDERATED->value, + ], + ], + ]) + ->assertOk(); + + $this->assertEquals(1, Review::where('assistant_id', $assistant->id)->count()); + $this->assertDatabaseHas('reviews', [ + 'assistant_id' => $assistant->id, + 'status' => ReviewStatus::PENDING->value, + ]); + } + + public function test_release_to_private_does_not_create_review(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => ReleaseStage::PRIVATE->value, + ]); + + Sanctum::actingAs($user); + Event::fake(AssistantTriggerReleaseStatus::class); + + $this->jsonApi('post', "/api/assistants/{$assistant->id}/actions/release", [ + 'data' => [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => [ + 'release_stage' => ReleaseStage::PRIVATE->value, + ], + ], + ]) + ->assertOk(); + + $this->assertDatabaseMissing('reviews', [ + 'assistant_id' => $assistant->id, + ]); + } + + public function test_admin_can_list_reviews_with_assistant(): void + { + $admin = $this->createAdmin(); + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => ReleaseStage::ORGANIZATIONAL->value, + ]); + Review::create([ + 'assistant_id' => $assistant->id, + 'status' => ReviewStatus::PENDING->value, + ]); + + Sanctum::actingAs($admin); + + $response = $this->jsonApi('get', '/api/assistant-reviews?include=assistant') + ->assertOk() + ->assertJsonCount(1, 'data'); + + $response->assertJson([ + 'data' => [ + [ + 'type' => 'assistant-reviews', + 'attributes' => [ + 'status' => ReviewStatus::PENDING->value, + ], + 'relationships' => [ + 'assistant' => [ + 'data' => [ + 'id' => (string) $assistant->id, + 'type' => 'assistants', + ], + ], + ], + ], + ], + ]); + + $included = collect($response->json('included')); + $assistantResource = $included->first(fn ($item) => $item['type'] === 'assistants'); + $this->assertEquals($assistant->name, $assistantResource['attributes']['name']); + } + + public function test_admin_can_list_reviews_without_assistant(): void + { + $admin = $this->createAdmin(); + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => ReleaseStage::ORGANIZATIONAL->value, + ]); + Review::create([ + 'assistant_id' => $assistant->id, + 'status' => ReviewStatus::PENDING->value, + ]); + + Sanctum::actingAs($admin); + + $response = $this->jsonApi('get', '/api/assistant-reviews') + ->assertOk() + ->assertJsonCount(1, 'data'); + + $response->assertJsonMissingPath('included'); + } + + public function test_non_admin_cannot_list_reviews(): void + { + $member = $this->createMember(); + + Sanctum::actingAs($member); + + $this->jsonApi('get', '/api/assistant-reviews') + ->assertForbidden(); + } + + public function test_admin_can_approve_review(): void + { + $admin = $this->createAdmin(); + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => ReleaseStage::ORGANIZATIONAL->value, + ]); + $review = Review::create([ + 'assistant_id' => $assistant->id, + 'status' => ReviewStatus::PENDING->value, + ]); + + Sanctum::actingAs($admin); + + $response = $this->jsonApi('patch', "/api/assistant-reviews/{$review->id}", [ + 'data' => [ + 'type' => 'assistant-reviews', + 'id' => (string) $review->id, + 'attributes' => [ + 'status' => ReviewStatus::APPROVED->value, + ], + ], + ]) + ->assertOk(); + + $response->assertJson([ + 'data' => [ + 'id' => (string) $review->id, + 'type' => 'assistant-reviews', + 'attributes' => [ + 'status' => ReviewStatus::APPROVED->value, + ], + ], + ]); + + $assistant->refresh(); + $this->assertEquals(ReleaseStage::ORGANIZATIONAL->value, $assistant->release_stage); + } + + public function test_admin_can_deny_review(): void + { + $admin = $this->createAdmin(); + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => ReleaseStage::ORGANIZATIONAL->value, + ]); + $review = Review::create([ + 'assistant_id' => $assistant->id, + 'status' => ReviewStatus::PENDING->value, + ]); + + Sanctum::actingAs($admin); + + $response = $this->jsonApi('patch', "/api/assistant-reviews/{$review->id}", [ + 'data' => [ + 'type' => 'assistant-reviews', + 'id' => (string) $review->id, + 'attributes' => [ + 'status' => ReviewStatus::DENIED->value, + 'reason' => 'Not ready for release', + ], + ], + ]) + ->assertOk(); + + $response->assertJson([ + 'data' => [ + 'id' => (string) $review->id, + 'type' => 'assistant-reviews', + 'attributes' => [ + 'status' => ReviewStatus::DENIED->value, + 'reason' => 'Not ready for release', + ], + ], + ]); + + $assistant->refresh(); + $this->assertEquals(ReleaseStage::PRIVATE->value, $assistant->release_stage); + } + + public function test_deny_without_reason_returns_validation_error(): void + { + $admin = $this->createAdmin(); + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => ReleaseStage::ORGANIZATIONAL->value, + ]); + $review = Review::create([ + 'assistant_id' => $assistant->id, + 'status' => ReviewStatus::PENDING->value, + ]); + + Sanctum::actingAs($admin); + + $this->jsonApi('patch', "/api/assistant-reviews/{$review->id}", [ + 'data' => [ + 'type' => 'assistant-reviews', + 'id' => (string) $review->id, + 'attributes' => [ + 'status' => ReviewStatus::DENIED->value, + ], + ], + ]) + ->assertUnprocessable() + ->assertJsonPath('errors.0.source.pointer', '/data/attributes/reason'); + } + + public function test_non_admin_cannot_update_review(): void + { + $member = $this->createMember(); + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => ReleaseStage::ORGANIZATIONAL->value, + ]); + $review = Review::create([ + 'assistant_id' => $assistant->id, + 'status' => ReviewStatus::PENDING->value, + ]); + + Sanctum::actingAs($member); + + $this->jsonApi('patch', "/api/assistant-reviews/{$review->id}", [ + 'data' => [ + 'type' => 'assistant-reviews', + 'id' => (string) $review->id, + 'attributes' => [ + 'status' => ReviewStatus::APPROVED->value, + ], + ], + ]) + ->assertForbidden(); + } + + public function test_guest_cannot_access_reviews(): void + { + $this->jsonApi('get', '/api/assistant-reviews') + ->assertUnauthorized(); + + $this->jsonApi('patch', '/api/assistant-reviews/1', [ + 'data' => [ + 'type' => 'assistant-reviews', + 'id' => '1', + 'attributes' => [ + 'status' => ReviewStatus::APPROVED->value, + ], + ], + ]) + ->assertUnauthorized(); + } +} diff --git a/tests/Feature/Api/Assistant/ShowTest.php b/tests/Feature/Api/Assistant/ShowTest.php new file mode 100644 index 000000000..f04307232 --- /dev/null +++ b/tests/Feature/Api/Assistant/ShowTest.php @@ -0,0 +1,420 @@ +create(); + $assistant = Assistant::factory()->create(['creator_id' => $user->id, 'allow_remix' => true]); + + Sanctum::actingAs($user); + + $this->jsonApi('get',"/api/assistants/{$assistant->id}") + ->assertOk() + ->assertJson([ + 'data' => [ + 'id' => (string) $assistant->id, + 'type' => 'assistants', + 'attributes' => [ + 'name' => $assistant->name, + 'handle' => $assistant->handle, + 'system_prompt' => $assistant->system_prompt, + 'greeting' => $assistant->greeting, + 'description' => $assistant->description, + 'detail_description' => $assistant->detail_description, + 'allow_remix' => (int) $assistant->allow_remix, + 'allow_model_select' => (int) $assistant->allow_model_select, + 'release_stage' => $assistant->release_stage, + 'formality' => $assistant->formality, + 'model' => $assistant->model, + 'model_length' => $assistant->model_length, + 'model_temp' => $assistant->model_temp, + 'model_top_p' => $assistant->model_top_p, + 'created_at' => $assistant->created_at->toJson(), + 'updated_at' => $assistant->updated_at->toJson(), + ], + 'links' => [ + 'self' => config('app.url') . "/api/assistants/{$assistant->id}", + 'remix' => [ + 'href' => config('app.url') . "/api/assistants/{$assistant->id}/actions/remix", + 'meta' => ['message' => 'ALLOWED'], + ], + 'release' => [ + 'href' => config('app.url') . "/api/assistants/{$assistant->id}/actions/release", + 'meta' => ['message' => 'ALLOWED'], + ], + ], + ], + ]); + } + + public function test_can_show_assistant_with_relations(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create(['creator_id' => $user->id]); + + Sanctum::actingAs($user); + + $response = $this->jsonApi('get',"/api/assistants/{$assistant->id}?include=creator,user_prompts,ai_tools,tags") + ->assertOk() + ->assertJson([ + 'data' => [ + 'id' => (string) $assistant->id, + 'type' => 'assistants', + 'relationships' => [ + 'creator' => [ + 'data' => [ + 'id' => (string) $user->id, + 'type' => 'users', + ], + ], + 'user_prompts' => [ + 'data' => [], + ], + 'ai_tools' => [ + 'data' => [], + ], + 'tags' => [ + 'data' => [], + ], + ], + ], + ]); + + $included = collect($response->json('included')); + $creatorResource = $included->first(fn ($item) => $item['type'] === 'users'); + $this->assertEquals((string) $user->id, $creatorResource['id']); + $this->assertEquals($user->name, $creatorResource['attributes']['name']); + } + + public function test_can_show_assistant_with_organization(): void + { + $org = Organization::first(); + + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'organization_id' => $org->id, + ]); + + Sanctum::actingAs($user); + + $response = $this->jsonApi('get',"/api/assistants/{$assistant->id}?include=organization") + ->assertOk(); + + $response->assertJson([ + 'data' => [ + 'id' => (string) $assistant->id, + 'relationships' => [ + 'organization' => [ + 'data' => [ + 'id' => (string) $org->id, + 'type' => 'organizations', + ], + ], + ], + ], + ]); + + $included = collect($response->json('included')); + $orgResource = $included->first(fn ($item) => $item['type'] === 'organizations'); + $this->assertEquals($org->name, $orgResource['attributes']['name']); + } + + public function test_can_show_assistant_with_versions(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create(['creator_id' => $user->id]); + + Sanctum::actingAs($user); + + $response = $this->jsonApi('get',"/api/assistants/{$assistant->id}?include=versions") + ->assertOk(); + + $included = collect($response->json('included')); + $versionResource = $included->first(fn ($item) => $item['type'] === 'versions'); + $this->assertEquals('Initial version', $versionResource['attributes']['text']); + $this->assertEquals('1.0', $versionResource['attributes']['version']); + } + + public function test_can_show_assistant_with_language(): void + { + $user = User::factory()->create(); + $language = Language::factory()->create(['text' => 'en']); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'language_id' => $language->id, + ]); + + Sanctum::actingAs($user); + + $response = $this->jsonApi('get',"/api/assistants/{$assistant->id}?include=language") + ->assertOk(); + + $included = collect($response->json('included')); + $langResource = $included->first(fn ($item) => $item['type'] === 'assistant-languages'); + $this->assertEquals('en', $langResource['attributes']['text']); + } + + public function test_can_show_assistant_with_category(): void + { + $user = User::factory()->create(); + $category = Category::factory()->create(['text' => 'general']); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'category_id' => $category->id, + ]); + + Sanctum::actingAs($user); + + $response = $this->jsonApi('get',"/api/assistants/{$assistant->id}?include=category") + ->assertOk(); + + $included = collect($response->json('included')); + $catResource = $included->first(fn ($item) => $item['type'] === 'assistant-categories'); + $this->assertEquals('general', $catResource['attributes']['text']); + } + + public function test_can_show_assistant_with_creator(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create(['creator_id' => $user->id]); + + Sanctum::actingAs($user); + + $response = $this->jsonApi('get',"/api/assistants/{$assistant->id}?include=creator") + ->assertOk(); + + $response->assertJson([ + 'data' => [ + 'id' => (string) $assistant->id, + 'relationships' => [ + 'creator' => [ + 'data' => [ + 'id' => (string) $user->id, + 'type' => 'users', + ], + ], + ], + ], + ]); + + $included = collect($response->json('included')); + $creatorResource = $included->first(fn ($item) => $item['type'] === 'users'); + $this->assertEquals($user->name, $creatorResource['attributes']['name']); + } + + public function test_can_show_assistant_with_remixed_creator(): void + { + $originalCreator = User::factory()->create(); + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'remixed_creator_id' => $originalCreator->id, + ]); + + Sanctum::actingAs($user); + + $response = $this->jsonApi('get',"/api/assistants/{$assistant->id}?include=remix_creator") + ->assertOk(); + + $response->assertJson([ + 'data' => [ + 'id' => (string) $assistant->id, + 'relationships' => [ + 'remix_creator' => [ + 'data' => [ + 'id' => (string) $originalCreator->id, + 'type' => 'users', + ], + ], + ], + ], + ]); + + $included = collect($response->json('included')); + $creatorResource = $included->first(fn ($item) => $item['type'] === 'users'); + $this->assertEquals($originalCreator->name, $creatorResource['attributes']['name']); + } + + public function test_can_show_assistant_with_remixed_assistant(): void + { + $user = User::factory()->create(); + $original = Assistant::factory()->create(['creator_id' => $user->id]); + $remix = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'remixed_assistant_id' => $original->id, + ]); + + Sanctum::actingAs($user); + + $response = $this->jsonApi('get',"/api/assistants/{$remix->id}?include=remixed_assistant") + ->assertOk(); + + $response->assertJson([ + 'data' => [ + 'id' => (string) $remix->id, + 'relationships' => [ + 'remixed_assistant' => [ + 'data' => [ + 'id' => (string) $original->id, + 'type' => 'assistants', + ], + ], + ], + ], + ]); + + $included = collect($response->json('included')); + $originalResource = $included->first(fn ($item) => $item['type'] === 'assistants'); + $this->assertEquals($original->name, $originalResource['attributes']['name']); + } + + public function test_relationship_data_absent_when_not_included(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create(['creator_id' => $user->id]); + + Sanctum::actingAs($user); + + $this->jsonApi('get',"/api/assistants/{$assistant->id}") + ->assertOk() + ->assertJsonMissingPath('included'); + } + + public function test_user_cannot_show_other_users_private_assistant(): void + { + $owner = User::factory()->create(); + $other = User::factory()->create(); + + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'private', + ]); + + Sanctum::actingAs($other); + + $this->jsonApi('get',"/api/assistants/{$assistant->id}") + ->assertForbidden(); + } + + public function test_show_assistant_shows_denied_release_link_for_non_owner(): void + { + $owner = User::factory()->create(); + $other = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'federated', + 'allow_remix' => true, + ]); + + Sanctum::actingAs($other); + + $response = $this->jsonApi('get', "/api/assistants/{$assistant->id}") + ->assertOk(); + + $response->assertJson([ + 'data' => [ + 'id' => (string) $assistant->id, + 'links' => [ + 'remix' => [ + 'href' => config('app.url') . "/api/assistants/{$assistant->id}/actions/remix", + 'meta' => ['message' => 'ALLOWED'], + ], + 'release' => [ + 'href' => config('app.url') . "/api/assistants/{$assistant->id}/actions/release", + 'meta' => ['message' => 'DENIED'], + ], + ], + ], + ]); + } + + public function test_show_assistant_shows_denied_remix_link_when_not_allowed(): void + { + $owner = User::factory()->create(); + $other = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'federated', + 'allow_remix' => false, + ]); + + Sanctum::actingAs($other); + + $response = $this->jsonApi('get', "/api/assistants/{$assistant->id}") + ->assertOk(); + + $response->assertJson([ + 'data' => [ + 'id' => (string) $assistant->id, + 'links' => [ + 'remix' => [ + 'href' => config('app.url') . "/api/assistants/{$assistant->id}/actions/remix", + 'meta' => ['message' => 'DENIED'], + ], + 'release' => [ + 'href' => config('app.url') . "/api/assistants/{$assistant->id}/actions/release", + 'meta' => ['message' => 'DENIED'], + ], + ], + ], + ]); + } + + public function test_show_assistant_has_no_action_links_when_unauthenticated(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => 'federated', + ]); + + $this->jsonApi('get', "/api/assistants/{$assistant->id}") + ->assertUnauthorized(); + } + + public function test_is_favorite_attribute_on_show_when_favorited(): void + { + $owner = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'public', + ]); + $owner->favoriteAssistants()->attach($assistant->id); + + Sanctum::actingAs($owner); + + $this->jsonApi('get', "/api/assistants/{$assistant->id}") + ->assertOk() + ->assertJsonPath('data.attributes.is_favorite', true); + } + + public function test_is_favorite_attribute_on_show_when_not_favorited(): void + { + $owner = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $owner->id, + 'release_stage' => 'public', + ]); + + Sanctum::actingAs($owner); + + $this->jsonApi('get', "/api/assistants/{$assistant->id}") + ->assertOk() + ->assertJsonPath('data.attributes.is_favorite', false); + } +} diff --git a/tests/Feature/Api/Assistant/StoreTest.php b/tests/Feature/Api/Assistant/StoreTest.php new file mode 100644 index 000000000..3526b6a5c --- /dev/null +++ b/tests/Feature/Api/Assistant/StoreTest.php @@ -0,0 +1,270 @@ +jsonApi('post', '/api/assistants', $this->createJsonApiPayload()) + ->assertUnauthorized() + ->assertJson(['errors' => [['detail' => 'Unauthenticated.']]]); + } + + public function test_can_create_assistant(): void + { + Event::fake(AssistantCreated::class); + Event::assertListening(AssistantCreated::class, AssistantCreateInitialVersion::class); + + $language = Language::factory()->create(['text' => 'en']); + $category = Category::factory()->create(['text' => 'general']); + + $user = User::factory()->create(); + $org = Organization::create(['name' => 'Test Org']); + $org->users()->attach($user); + Sanctum::actingAs($user); + + $response = $this->jsonApi('post', '/api/assistants', $this->createJsonApiPayload([ + 'name' => 'Test Assistant', + ], [ + 'language' => $language->id, + 'category' => $category->id, + ])) + ->assertCreated(); + + $this->assertDatabaseHas('assistants', [ + 'name' => 'Test Assistant', + 'creator_id' => $user->id, + 'remixed_creator_id' => null, + ]); + + $this->assertNotNull(Assistant::first()->organization_id); + + $assistant = Assistant::first(); + $response + ->assertStatus(201) + ->assertJson([ + 'data' => [ + 'id' => (string) $assistant->id, + 'type' => 'assistants', + 'attributes' => [ + 'name' => 'Test Assistant', + 'handle' => null, + 'system_prompt' => 'You are a helpful assistant.', + 'greeting' => 'Hello!', + 'description' => 'A test assistant.', + 'detail_description' => 'Detailed description here.', + 'allow_remix' => true, + 'allow_model_select' => false, + 'release_stage' => 'private', + 'formality' => 'neutral', + 'model' => 'gpt-4', + 'model_length' => 2048, + 'model_temp' => 0.7, + 'model_top_p' => 0.9, + 'created_at' => $assistant->created_at->toJson(), + 'updated_at' => $assistant->updated_at->toJson(), + ], + ], + ]); + } + + public function test_can_create_assistant_with_user_prompts(): void + { + $language = Language::factory()->create(['text' => 'en']); + $category = Category::factory()->create(['text' => 'general']); + $user = User::factory()->create(); + + $temp = Assistant::factory()->create(['creator_id' => $user->id]); + $prompt1 = $temp->user_prompts()->create(['text' => 'First prompt']); + $prompt2 = $temp->user_prompts()->create(['text' => 'Second prompt']); + + Sanctum::actingAs($user); + + $this->jsonApi('post', '/api/assistants?include=user_prompts', $this->createJsonApiPayload([], [ + 'language' => $language->id, + 'category' => $category->id, + 'user_prompts' => [$prompt1->id, $prompt2->id], + ])) + ->assertCreated(); + + $assistant = Assistant::where('creator_id', $user->id)->where('id', '!=', $temp->id)->first(); + $this->assertNotNull($assistant); + $this->assertEquals(2, $assistant->user_prompts()->count()); + } + + public function test_can_create_assistant_with_ai_tools(): void + { + $language = Language::factory()->create(['text' => 'en']); + $category = Category::factory()->create(['text' => 'general']); + $tool = $this->createAiTool(); + + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $this->jsonApi('post', '/api/assistants?include=ai_tools', $this->createJsonApiPayload([], [ + 'language' => $language->id, + 'category' => $category->id, + 'ai_tools' => [$tool->id], + ])) + ->assertCreated(); + + $this->assertDatabaseHas('assistant_tools', [ + 'ai_tool_id' => $tool->id, + ]); + } + + public function test_can_create_empty_assistant(): void + { + Event::fake(AssistantCreated::class); + + $user = User::factory()->create(); + $org = Organization::create(['name' => 'Test Org']); + $org->users()->attach($user); + Sanctum::actingAs($user); + + $response = $this->jsonApi('post', '/api/assistants', [ + 'data' => ['type' => 'assistants'], + ]) + ->assertCreated(); + + $assistant = Assistant::first(); + $this->assertEquals($user->id, $assistant->creator_id); + $this->assertNotNull($assistant->organization_id); + $this->assertNull($assistant->language_id); + $this->assertNull($assistant->category_id); + $this->assertEquals(0, $assistant->tags()->count()); + $this->assertEquals(0, $assistant->ai_tools()->count()); + $this->assertEquals(0, $assistant->user_prompts()->count()); + + $response->assertJson([ + 'data' => [ + 'id' => (string) $assistant->id, + 'type' => 'assistants', + 'attributes' => [ + 'name' => '', + 'handle' => null, + 'system_prompt' => '', + 'greeting' => '', + 'description' => '', + 'detail_description' => '', + 'allow_remix' => false, + 'allow_model_select' => false, + 'release_stage' => 'draft', + 'formality' => 'neutral', + 'model' => '', + 'model_length' => 0, + 'model_temp' => 0, + 'model_top_p' => 0, + ], + ], + ]); + } + + public function test_create_assistant_fails_validation(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $this->jsonApi('post', '/api/assistants', [ + 'data' => [ + 'type' => 'assistants', + 'attributes' => [ + 'name' => 12345, + 'release_stage' => 'invalid-stage', + 'model_temp' => 5.0, + ], + ], + ]) + ->assertStatus(422); + } + + public function test_create_fails_for_duplicate_handle(): void + { + $language = Language::factory()->create(['text' => 'en']); + $category = Category::factory()->create(['text' => 'general']); + $user = User::factory()->create(); + Assistant::factory()->create(['handle' => 'unique-handle']); + + Sanctum::actingAs($user); + + $this->jsonApi('post', '/api/assistants', $this->createJsonApiPayload( + ['handle' => 'unique-handle'], + ['language' => $language->id, 'category' => $category->id] + )) + ->assertUnprocessable() + ->assertJsonPath('errors.0.source.pointer', '/data/attributes/handle'); + } + + public function test_create_fails_for_nonexistent_ai_tool(): void + { + $language = Language::factory()->create(['text' => 'en']); + $category = Category::factory()->create(['text' => 'general']); + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $this->jsonApi('post', '/api/assistants', $this->createJsonApiPayload([], [ + 'language' => $language->id, + 'category' => $category->id, + 'ai_tools' => [999999], + ])) + ->assertStatus(404); + } + + public function test_can_create_assistant_with_tags(): void + { + $language = Language::factory()->create(['text' => 'en']); + $category = Category::factory()->create(['text' => 'general']); + $tag1 = Tag::create(['text' => 'existing-tag']); + $tag2 = Tag::create(['text' => 'new-tag']); + + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $this->jsonApi('post', '/api/assistants?include=tags', $this->createJsonApiPayload([], [ + 'language' => $language->id, + 'category' => $category->id, + 'tags' => [$tag1->id, $tag2->id], + ])) + ->assertCreated(); + + $this->assertDatabaseHas('tags', ['text' => 'existing-tag']); + $this->assertDatabaseHas('tags', ['text' => 'new-tag']); + + $assistant = Assistant::first(); + $this->assertEquals(2, $assistant->tags()->count()); + $this->assertTrue($assistant->tags->pluck('text')->contains('existing-tag')); + $this->assertTrue($assistant->tags->pluck('text')->contains('new-tag')); + } + + public function test_create_fails_for_nonexistent_tag(): void + { + $language = Language::factory()->create(['text' => 'en']); + $category = Category::factory()->create(['text' => 'general']); + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $this->jsonApi('post', '/api/assistants', $this->createJsonApiPayload([], [ + 'language' => $language->id, + 'category' => $category->id, + 'tags' => [999999], + ])) + ->assertStatus(404); + } +} diff --git a/tests/Feature/Api/Assistant/UpdateTest.php b/tests/Feature/Api/Assistant/UpdateTest.php new file mode 100644 index 000000000..6ea396a83 --- /dev/null +++ b/tests/Feature/Api/Assistant/UpdateTest.php @@ -0,0 +1,296 @@ + [ + 'type' => 'assistants', + 'id' => (string) $assistant->id, + 'attributes' => $attributes, + ], + ]; + $rels = $this->createRelationships($relationships); + if ($rels) { + $data['data']['relationships'] = $rels; + } + return $data; + } + + public function test_can_update_assistant(): void + { + Event::fake(AssistantUpdated::class); + Event::assertListening(AssistantUpdated::class, AssistantUpdatedVersion::class); + Event::assertListening(AssistantUpdated::class, ResetReviewOnUpdate::class); + + $user = User::factory()->create(); + $assistant = Assistant::factory()->create(['creator_id' => $user->id, 'remixed_creator_id' => null]); + + Sanctum::actingAs($user); + + $response = $this->jsonApi('patch', "/api/assistants/{$assistant->id}", $this->updatePayload($assistant, [ + 'name' => 'Updated Name', + 'description' => 'Updated description.', + ])) + ->assertOk(); + + $assistant->refresh(); + $response->assertJson([ + 'data' => [ + 'id' => (string) $assistant->id, + 'type' => 'assistants', + 'attributes' => [ + 'name' => 'Updated Name', + 'description' => 'Updated description.', + 'handle' => $assistant->handle, + 'system_prompt' => $assistant->system_prompt, + 'greeting' => $assistant->greeting, + 'detail_description' => $assistant->detail_description, + 'allow_remix' => (int) $assistant->allow_remix, + 'allow_model_select' => (int) $assistant->allow_model_select, + 'release_stage' => $assistant->release_stage, + 'formality' => $assistant->formality, + 'model' => $assistant->model, + 'model_length' => $assistant->model_length, + 'model_temp' => $assistant->model_temp, + 'model_top_p' => $assistant->model_top_p, + 'created_at' => $assistant->created_at->toJson(), + 'updated_at' => $assistant->updated_at->toJson(), + ], + ], + ]); + + $this->assertDatabaseHas('assistants', [ + 'id' => $assistant->id, + 'name' => 'Updated Name', + ]); + } + + public function test_cannot_update_others_assistant(): void + { + $owner = User::factory()->create(); + $other = User::factory()->create(); + $assistant = Assistant::factory()->create(['creator_id' => $owner->id]); + + Sanctum::actingAs($other); + + $this->jsonApi('patch', "/api/assistants/{$assistant->id}", $this->updatePayload($assistant, [ + 'name' => 'Hacked', + ])) + ->assertForbidden() + ->assertJson(['errors' => [['detail' => 'This action is unauthorized.']]]); + } + + public function test_update_assistant_increments_version(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create(['creator_id' => $user->id]); + + Sanctum::actingAs($user); + + $this->jsonApi('patch', "/api/assistants/{$assistant->id}", $this->updatePayload($assistant, [ + 'name' => 'Updated Name', + ])) + ->assertOk(); + + $version = $assistant->fresh()->versions()->where('version', 2.0)->first(); + $this->assertEquals(['name'], $version->changed_keys); + } + + public function test_update_assistant_with_version_text(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create(['creator_id' => $user->id]); + + Sanctum::actingAs($user); + + $this->jsonApi('patch', "/api/assistants/{$assistant->id}", $this->updatePayload($assistant, [ + 'name' => 'Updated Name', + 'version_text' => 'Changed the name', + ])) + ->assertOk(); + + $version = $assistant->fresh()->versions()->where('version', 2.0)->first(); + $this->assertEquals('Changed the name', $version->text); + $this->assertEquals(['name'], $version->changed_keys); + } + + public function test_multiple_updates_increment_version(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create(['creator_id' => $user->id]); + + Sanctum::actingAs($user); + + $this->jsonApi('patch', "/api/assistants/{$assistant->id}", $this->updatePayload($assistant, [ + 'name' => 'v2', + ])) + ->assertOk(); + + $this->jsonApi('patch', "/api/assistants/{$assistant->id}", $this->updatePayload($assistant, [ + 'name' => 'v3', + ])) + ->assertOk(); + + $versions = $assistant->fresh()->versions()->orderBy('version')->get(); + + $this->assertCount(3, $versions); + + $this->assertEquals(1.0, (float) $versions[0]->version); + $this->assertNull($versions[0]->changed_keys); + + $this->assertEquals(2.0, (float) $versions[1]->version); + $this->assertEquals(['name'], $versions[1]->changed_keys); + + $this->assertEquals(3.0, (float) $versions[2]->version); + $this->assertEquals(['name'], $versions[2]->changed_keys); + } + + public function test_can_update_assistant_with_tags(): void + { + $tag1 = Tag::create(['text' => 'tag-one']); + $tag2 = Tag::create(['text' => 'tag-two']); + + $user = User::factory()->create(); + $assistant = Assistant::factory()->create(['creator_id' => $user->id]); + $assistant->tags()->attach([$tag1->id, $tag2->id]); + + Sanctum::actingAs($user); + + $response = $this->jsonApi('patch', "/api/assistants/{$assistant->id}?include=tags", $this->updatePayload($assistant, [ + 'name' => $assistant->name, + ], [ + 'tags' => [$tag2->id], + ])) + ->assertOk(); + + $this->assertDatabaseMissing('assistant_tag', [ + 'assistant_id' => $assistant->id, + 'tag_id' => $tag1->id, + ]); + + $assistant->refresh(); + $this->assertEquals(1, $assistant->tags()->count()); + $this->assertTrue($assistant->tags->pluck('text')->contains('tag-two')); + + $version = $assistant->fresh()->versions()->where('version', 2.0)->first(); + $this->assertEquals(['tags'], $version->changed_keys); + } + + public function test_update_resets_review_to_pending(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => ReleaseStage::ORGANIZATIONAL->value, + ]); + Review::create([ + 'assistant_id' => $assistant->id, + 'status' => ReviewStatus::APPROVED->value, + 'reason' => 'Looks good', + ]); + + Sanctum::actingAs($user); + + $this->jsonApi('patch', "/api/assistants/{$assistant->id}", $this->updatePayload($assistant, [ + 'name' => 'Updated Name', + ])) + ->assertOk(); + + $this->assertDatabaseHas('reviews', [ + 'assistant_id' => $assistant->id, + 'status' => ReviewStatus::PENDING->value, + 'reason' => null, + ]); + } + + public function test_update_does_not_create_review_when_none_exists(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => ReleaseStage::ORGANIZATIONAL->value, + ]); + + Sanctum::actingAs($user); + + $this->jsonApi('patch', "/api/assistants/{$assistant->id}", $this->updatePayload($assistant, [ + 'name' => 'Updated Name', + ])) + ->assertOk(); + + $this->assertDatabaseMissing('reviews', [ + 'assistant_id' => $assistant->id, + ]); + } + + public function test_update_does_not_increment_version_when_draft(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => ReleaseStage::DRAFT->value, + ]); + + Sanctum::actingAs($user); + + $this->jsonApi('patch', "/api/assistants/{$assistant->id}", $this->updatePayload($assistant, [ + 'name' => 'Updated Name', + ])) + ->assertOk(); + + $this->assertDatabaseHas('assistants', [ + 'id' => $assistant->id, + 'name' => 'Updated Name', + ]); + + $this->assertCount(1, $assistant->fresh()->versions); + } + + public function test_update_does_not_reset_review_when_draft(): void + { + $user = User::factory()->create(); + $assistant = Assistant::factory()->create([ + 'creator_id' => $user->id, + 'release_stage' => ReleaseStage::DRAFT->value, + ]); + Review::create([ + 'assistant_id' => $assistant->id, + 'status' => ReviewStatus::APPROVED->value, + 'reason' => 'Looks good', + ]); + + Sanctum::actingAs($user); + + $this->jsonApi('patch', "/api/assistants/{$assistant->id}", $this->updatePayload($assistant, [ + 'name' => 'Updated Name', + ])) + ->assertOk(); + + $this->assertDatabaseHas('reviews', [ + 'assistant_id' => $assistant->id, + 'status' => ReviewStatus::APPROVED->value, + 'reason' => 'Looks good', + ]); + } +} diff --git a/tests/Feature/Api/CategoryTest.php b/tests/Feature/Api/CategoryTest.php new file mode 100644 index 000000000..a52eb8f47 --- /dev/null +++ b/tests/Feature/Api/CategoryTest.php @@ -0,0 +1,99 @@ +jsonApi('get', '/api/assistant-categories') + ->assertUnauthorized() + ->assertJson(['errors' => [['detail' => 'Unauthenticated.']]]); + } + + public function test_can_list_categories(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $categories = Category::factory()->count(3)->create(); + + $response = $this->jsonApi('get', '/api/assistant-categories') + ->assertOk() + ->assertJsonCount(3, 'data'); + + foreach ($categories->sortBy('text')->values() as $i => $category) { + $response->assertJson([ + 'data' => [ + $i => [ + 'id' => (string) $category->id, + 'type' => 'assistant-categories', + 'attributes' => [ + 'text' => $category->text, + 'created_at' => $category->created_at->toJson(), + 'updated_at' => $category->updated_at->toJson(), + ], + ], + ], + ]); + } + } + + public function test_empty_categories_returns_empty_data(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $this->jsonApi('get', '/api/assistant-categories') + ->assertOk() + ->assertJsonCount(0, 'data'); + } + + public function test_categories_are_ordered_alphabetically(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + Category::factory()->create(['text' => 'programming']); + Category::factory()->create(['text' => 'art']); + Category::factory()->create(['text' => 'education']); + + $this->jsonApi('get', '/api/assistant-categories') + ->assertOk() + ->assertJsonCount(3, 'data') + ->assertJson([ + 'data' => [ + ['attributes' => ['text' => 'art']], + ['attributes' => ['text' => 'education']], + ['attributes' => ['text' => 'programming']], + ], + ]); + } + + public function test_categories_pagination(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + Category::factory()->count(20)->create(); + + $response = $this->jsonApi('get', '/api/assistant-categories?' . http_build_query(['page' => ['size' => 5]])) + ->assertOk() + ->assertJsonCount(5, 'data'); + + $response->assertJsonStructure([ + 'meta' => [ + 'page' => ['currentPage', 'from', 'to', 'perPage', 'lastPage', 'total'], + ], + 'links' => ['first', 'last', 'next'], + ]); + } +} diff --git a/tests/Feature/Api/LanguageTest.php b/tests/Feature/Api/LanguageTest.php new file mode 100644 index 000000000..9cfe78860 --- /dev/null +++ b/tests/Feature/Api/LanguageTest.php @@ -0,0 +1,99 @@ +jsonApi('get', '/api/assistant-languages') + ->assertUnauthorized() + ->assertJson(['errors' => [['detail' => 'Unauthenticated.']]]); + } + + public function test_can_list_languages(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $languages = Language::factory()->count(3)->create(); + + $response = $this->jsonApi('get', '/api/assistant-languages') + ->assertOk() + ->assertJsonCount(3, 'data'); + + foreach ($languages->sortBy('text')->values() as $i => $language) { + $response->assertJson([ + 'data' => [ + $i => [ + 'id' => (string) $language->id, + 'type' => 'assistant-languages', + 'attributes' => [ + 'text' => $language->text, + 'created_at' => $language->created_at->toJson(), + 'updated_at' => $language->updated_at->toJson(), + ], + ], + ], + ]); + } + } + + public function test_empty_languages_returns_empty_data(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $this->jsonApi('get', '/api/assistant-languages') + ->assertOk() + ->assertJsonCount(0, 'data'); + } + + public function test_languages_are_ordered_alphabetically(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + Language::factory()->create(['text' => 'es']); + Language::factory()->create(['text' => 'de']); + Language::factory()->create(['text' => 'en']); + + $this->jsonApi('get', '/api/assistant-languages') + ->assertOk() + ->assertJsonCount(3, 'data') + ->assertJson([ + 'data' => [ + ['attributes' => ['text' => 'de']], + ['attributes' => ['text' => 'en']], + ['attributes' => ['text' => 'es']], + ], + ]); + } + + public function test_languages_pagination(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + Language::factory()->count(20)->create(); + + $response = $this->jsonApi('get', '/api/assistant-languages?' . http_build_query(['page' => ['size' => 5]])) + ->assertOk() + ->assertJsonCount(5, 'data'); + + $response->assertJsonStructure([ + 'meta' => [ + 'page' => ['currentPage', 'from', 'to', 'perPage', 'lastPage', 'total'], + ], + 'links' => ['first', 'last', 'next'], + ]); + } +} diff --git a/tests/Feature/Api/McpServerTest.php b/tests/Feature/Api/McpServerTest.php new file mode 100644 index 000000000..b26a137e2 --- /dev/null +++ b/tests/Feature/Api/McpServerTest.php @@ -0,0 +1,210 @@ +jsonApi('get', '/api/mcp-servers') + ->assertUnauthorized() + ->assertJson(['errors' => [['detail' => 'Unauthenticated.']]]); + } + + public function test_can_list_mcp_servers(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $server = McpServer::create([ + 'url' => 'https://example.com/mcp', + 'server_label' => 'Test Server', + 'version' => '1.0', + 'protocolVersion' => '2024-11-05', + 'description' => 'A test MCP server', + 'require_approval' => 'never', + 'timeout' => '15', + 'discovery_timeout' => '20', + ]); + + $response = $this->jsonApi('get', '/api/mcp-servers') + ->assertOk(); + + $data = collect($response->json('data')); + $serverResource = $data->first(fn($item) => $item['id'] === (string) $server->id); + + $response->assertJson([ + 'data' => [ + array_search($serverResource, $data->all()) => [ + 'id' => (string) $server->id, + 'type' => 'mcp-servers', + 'attributes' => [ + 'url' => 'https://example.com/mcp', + 'server_label' => 'Test Server', + 'version' => '1.0', + 'protocolVersion' => '2024-11-05', + 'description' => 'A test MCP server', + 'require_approval' => 'never', + 'timeout' => '15', + 'discovery_timeout' => '20', + 'created_at' => $server->created_at->toJson(), + 'updated_at' => $server->updated_at->toJson(), + ], + ], + ], + ]); + } + + public function test_mcp_server_does_not_expose_api_key(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + McpServer::create([ + 'url' => 'https://example.com/mcp', + 'server_label' => 'Secure Server', + 'timeout' => '10', + 'discovery_timeout' => '10', + 'api_key' => 'super-secret-key', + ]); + + $response = $this->jsonApi('get', '/api/mcp-servers') + ->assertOk(); + + foreach ($response->json('data') as $resource) { + $this->assertArrayNotHasKey('api_key', $resource['attributes']); + } + } + + public function test_can_list_mcp_servers_with_tools(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $server = McpServer::create([ + 'url' => 'https://example.com/mcp', + 'server_label' => 'Test Server', + 'timeout' => '10', + 'discovery_timeout' => '10', + ]); + + $tool = AiTool::create([ + 'type' => 'mcp', + 'name' => 'server-tool', + 'status' => 'active', + 'server_id' => $server->id, + ]); + + $response = $this->jsonApi('get', '/api/mcp-servers?include=tools') + ->assertOk(); + + $data = collect($response->json('data')); + $serverResource = $data->first(fn($item) => $item['id'] === (string) $server->id); + $idx = array_search($serverResource, $data->all()); + + $response->assertJson([ + 'data' => [ + $idx => [ + 'id' => (string) $server->id, + 'type' => 'mcp-servers', + 'relationships' => [ + 'tools' => [ + 'data' => [ + [ + 'id' => (string) $tool->id, + 'type' => 'ai-tools', + ], + ], + ], + ], + ], + ], + ]); + + $included = collect($response->json('included')); + $toolResource = $included->first(fn($item) => $item['type'] === 'ai-tools' && $item['id'] === (string) $tool->id); + + $response->assertJson([ + 'included' => [ + array_search($toolResource, $included->all()) => [ + 'id' => (string) $tool->id, + 'type' => 'ai-tools', + 'attributes' => [ + 'type' => 'mcp', + 'name' => 'server-tool', + 'status' => 'active', + ], + ], + ], + ]); + } + + public function test_can_show_single_mcp_server(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $server = McpServer::create([ + 'url' => 'https://example.com/mcp', + 'server_label' => 'Single Server', + 'timeout' => '10', + 'discovery_timeout' => '10', + ]); + + $this->jsonApi('get', "/api/mcp-servers/{$server->id}") + ->assertOk() + ->assertJson([ + 'data' => [ + 'id' => (string) $server->id, + 'type' => 'mcp-servers', + 'attributes' => [ + 'url' => 'https://example.com/mcp', + 'server_label' => 'Single Server', + 'version' => null, + 'protocolVersion' => null, + 'description' => null, + 'require_approval' => 'never', + 'timeout' => '10', + 'discovery_timeout' => '10', + 'created_at' => $server->created_at->toJson(), + 'updated_at' => $server->updated_at->toJson(), + ], + ], + ]); + } + + public function test_mcp_servers_pagination(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + foreach (range(1, 15) as $i) { + McpServer::create([ + 'url' => "https://example.com/mcp/{$i}", + 'server_label' => "Server {$i}", + 'timeout' => '10', + 'discovery_timeout' => '10', + ]); + } + + $response = $this->jsonApi('get', '/api/mcp-servers?' . http_build_query(['page' => ['size' => 5]])) + ->assertOk() + ->assertJsonCount(5, 'data'); + + $response->assertJsonStructure([ + 'meta' => [ + 'page' => ['currentPage', 'from', 'to', 'perPage', 'lastPage', 'total'], + ], + 'links' => ['first', 'last', 'next'], + ]); + } +} diff --git a/tests/Feature/Api/TagTest.php b/tests/Feature/Api/TagTest.php new file mode 100644 index 000000000..c3b2c65b6 --- /dev/null +++ b/tests/Feature/Api/TagTest.php @@ -0,0 +1,126 @@ +jsonApi('get', '/api/tags') + ->assertUnauthorized(); + } + + public function test_can_list_tags(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + Tag::create(['text' => 'php']); + Tag::create(['text' => 'laravel']); + + $this->jsonApi('get', '/api/tags') + ->assertOk() + ->assertJsonCount(2, 'data'); + } + + public function test_can_show_tag(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $tag = Tag::create(['text' => 'php']); + + $this->jsonApi('get', "/api/tags/{$tag->id}") + ->assertOk() + ->assertJson([ + 'data' => [ + 'id' => (string) $tag->id, + 'type' => 'tags', + 'attributes' => [ + 'text' => 'php', + ], + ], + ]); + } + + public function test_can_create_tag(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $this->jsonApi('post', '/api/tags', [ + 'data' => [ + 'type' => 'tags', + 'attributes' => [ + 'text' => 'new-tag', + ], + ], + ]) + ->assertCreated() + ->assertJson([ + 'data' => [ + 'type' => 'tags', + 'attributes' => [ + 'text' => 'new-tag', + ], + ], + ]); + + $this->assertDatabaseHas('tags', ['text' => 'new-tag']); + } + + public function test_cannot_create_duplicate_tag(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + Tag::create(['text' => 'existing-tag']); + + $this->jsonApi('post', '/api/tags', [ + 'data' => [ + 'type' => 'tags', + 'attributes' => [ + 'text' => 'existing-tag', + ], + ], + ]) + ->assertUnprocessable(); + } + + public function test_cannot_create_tag_without_text(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $this->jsonApi('post', '/api/tags', [ + 'data' => [ + 'type' => 'tags', + 'attributes' => [ + 'text' => '', + ], + ], + ]) + ->assertUnprocessable(); + } + + public function test_can_delete_tag(): void + { + $user = User::factory()->create(); + Sanctum::actingAs($user); + + $tag = Tag::create(['text' => 'to-delete']); + + $this->jsonApi('delete', "/api/tags/{$tag->id}") + ->assertNoContent(); + + $this->assertDatabaseMissing('tags', ['id' => $tag->id]); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index fe1ffc2ff..66bb53a79 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,9 +2,20 @@ namespace Tests; +use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase { - // + use RefreshDatabase; + + protected function jsonApi(string $method, string $uri, array $data = [], array $headers = []): \Illuminate\Testing\TestResponse + { + $headers = array_merge([ + 'Accept' => 'application/vnd.api+json', + 'Content-Type' => 'application/vnd.api+json', + ], $headers); + + return $this->json($method, $uri, $data, $headers); + } }