moai-adk 0.34.0__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- moai_adk/__main__.py +136 -5
- moai_adk/astgrep/__init__.py +37 -0
- moai_adk/astgrep/analyzer.py +522 -0
- moai_adk/astgrep/models.py +124 -0
- moai_adk/astgrep/rules.py +179 -0
- moai_adk/cli/commands/analyze.py +11 -2
- moai_adk/cli/commands/doctor.py +7 -1
- moai_adk/cli/commands/init.py +321 -11
- moai_adk/cli/commands/language.py +7 -1
- moai_adk/cli/commands/rank.py +449 -0
- moai_adk/cli/commands/status.py +7 -1
- moai_adk/cli/commands/switch.py +325 -0
- moai_adk/cli/commands/update.py +296 -23
- moai_adk/cli/prompts/init_prompts.py +362 -66
- moai_adk/cli/prompts/translations/__init__.py +573 -0
- moai_adk/cli/ui/prompts.py +61 -2
- moai_adk/cli/worktree/cli.py +106 -1
- moai_adk/cli/worktree/manager.py +155 -0
- moai_adk/core/config/unified.py +244 -63
- moai_adk/core/credentials.py +264 -0
- moai_adk/core/error_recovery_system.py +22 -4
- moai_adk/core/git/conflict_detector.py +10 -1
- moai_adk/core/git/event_detector.py +16 -5
- moai_adk/core/integration/engine.py +2 -2
- moai_adk/core/integration/integration_tester.py +5 -5
- moai_adk/core/language_config_resolver.py +9 -3
- moai_adk/core/merge/analyzer.py +509 -324
- moai_adk/core/migration/alfred_to_moai_migrator.py +7 -1
- moai_adk/core/migration/backup_manager.py +54 -4
- moai_adk/core/migration/file_migrator.py +174 -2
- moai_adk/core/migration/interactive_checkbox_ui.py +42 -31
- moai_adk/core/migration/version_detector.py +123 -19
- moai_adk/core/migration/version_migrator.py +44 -9
- moai_adk/core/model_allocator.py +241 -0
- moai_adk/core/project/backup_utils.py +12 -2
- moai_adk/core/project/initializer.py +44 -87
- moai_adk/core/project/phase_executor.py +95 -33
- moai_adk/core/project/validator.py +16 -1
- moai_adk/core/quality/trust_checker.py +30 -10
- moai_adk/core/rollback_manager.py +60 -25
- moai_adk/core/template/backup.py +88 -6
- moai_adk/core/template/config.py +33 -9
- moai_adk/core/template/merger.py +34 -8
- moai_adk/core/template/processor.py +334 -11
- moai_adk/core/template_engine.py +10 -1
- moai_adk/core/template_variable_synchronizer.py +16 -2
- moai_adk/core/version_sync.py +54 -6
- moai_adk/foundation/__init__.py +1 -20
- moai_adk/foundation/testing.py +1 -1
- moai_adk/loop/__init__.py +54 -0
- moai_adk/loop/controller.py +305 -0
- moai_adk/loop/feedback.py +230 -0
- moai_adk/loop/state.py +209 -0
- moai_adk/loop/storage.py +220 -0
- moai_adk/lsp/__init__.py +70 -0
- moai_adk/lsp/client.py +320 -0
- moai_adk/lsp/models.py +261 -0
- moai_adk/lsp/protocol.py +404 -0
- moai_adk/lsp/server_manager.py +248 -0
- moai_adk/project/configuration.py +8 -1
- moai_adk/py.typed +0 -0
- moai_adk/ralph/__init__.py +37 -0
- moai_adk/ralph/engine.py +307 -0
- moai_adk/rank/__init__.py +21 -0
- moai_adk/rank/auth.py +425 -0
- moai_adk/rank/client.py +557 -0
- moai_adk/rank/config.py +147 -0
- moai_adk/rank/hook.py +1503 -0
- moai_adk/rank/py.typed +0 -0
- moai_adk/statusline/__init__.py +3 -0
- moai_adk/statusline/enhanced_output_style_detector.py +5 -5
- moai_adk/statusline/main.py +20 -1
- moai_adk/statusline/memory_collector.py +268 -0
- moai_adk/statusline/renderer.py +54 -38
- moai_adk/tag_system/__init__.py +48 -0
- moai_adk/tag_system/atomic_ops.py +117 -0
- moai_adk/tag_system/linkage.py +335 -0
- moai_adk/tag_system/parser.py +176 -0
- moai_adk/tag_system/validator.py +200 -0
- moai_adk/templates/.claude/agents/moai/builder-agent.md +19 -3
- moai_adk/templates/.claude/agents/moai/builder-command.md +62 -16
- moai_adk/templates/.claude/agents/moai/builder-plugin.md +763 -0
- moai_adk/templates/.claude/agents/moai/builder-skill.md +21 -5
- moai_adk/templates/.claude/agents/moai/expert-backend.md +103 -39
- moai_adk/templates/.claude/agents/moai/expert-debug.md +9 -3
- moai_adk/templates/.claude/agents/moai/expert-devops.md +16 -14
- moai_adk/templates/.claude/agents/moai/expert-frontend.md +45 -31
- moai_adk/templates/.claude/agents/moai/expert-performance.md +13 -9
- moai_adk/templates/.claude/agents/moai/expert-refactoring.md +228 -0
- moai_adk/templates/.claude/agents/moai/expert-security.md +19 -3
- moai_adk/templates/.claude/agents/moai/expert-testing.md +13 -9
- moai_adk/templates/.claude/agents/moai/manager-claude-code.md +8 -2
- moai_adk/templates/.claude/agents/moai/manager-docs.md +10 -5
- moai_adk/templates/.claude/agents/moai/manager-git.md +99 -27
- moai_adk/templates/.claude/agents/moai/manager-project.md +87 -7
- moai_adk/templates/.claude/agents/moai/manager-quality.md +22 -5
- moai_adk/templates/.claude/agents/moai/manager-spec.md +8 -2
- moai_adk/templates/.claude/agents/moai/manager-strategy.md +45 -14
- moai_adk/templates/.claude/agents/moai/manager-tdd.md +16 -3
- moai_adk/templates/.claude/commands/moai/0-project.md +239 -1185
- moai_adk/templates/.claude/commands/moai/1-plan.md +383 -363
- moai_adk/templates/.claude/commands/moai/2-run.md +254 -347
- moai_adk/templates/.claude/commands/moai/3-sync.md +174 -100
- moai_adk/templates/.claude/commands/moai/9-feedback.md +49 -33
- moai_adk/templates/.claude/commands/moai/alfred.md +339 -0
- moai_adk/templates/.claude/commands/moai/cancel-loop.md +163 -0
- moai_adk/templates/.claude/commands/moai/fix.md +264 -0
- moai_adk/templates/.claude/commands/moai/loop.md +363 -0
- moai_adk/templates/.claude/hooks/moai/lib/README.md +143 -0
- moai_adk/templates/.claude/hooks/moai/lib/__init__.py +37 -81
- moai_adk/templates/.claude/hooks/moai/lib/alfred_detector.py +105 -0
- moai_adk/templates/.claude/hooks/moai/lib/atomic_write.py +122 -0
- moai_adk/templates/.claude/hooks/moai/lib/checkpoint.py +4 -1
- moai_adk/templates/.claude/hooks/moai/lib/common.py +35 -5
- moai_adk/templates/.claude/hooks/moai/lib/config.py +376 -0
- moai_adk/templates/.claude/hooks/moai/lib/config_manager.py +24 -28
- moai_adk/templates/.claude/hooks/moai/lib/config_validator.py +14 -14
- moai_adk/templates/.claude/hooks/moai/lib/enhanced_output_style_detector.py +372 -0
- moai_adk/templates/.claude/hooks/moai/lib/exceptions.py +171 -0
- moai_adk/templates/.claude/hooks/moai/lib/file_utils.py +95 -0
- moai_adk/templates/.claude/hooks/moai/lib/git_collector.py +190 -0
- moai_adk/templates/.claude/hooks/moai/lib/git_operations_manager.py +15 -13
- moai_adk/templates/.claude/hooks/moai/lib/language_detector.py +298 -0
- moai_adk/templates/.claude/hooks/moai/lib/language_validator.py +125 -25
- moai_adk/templates/.claude/hooks/moai/lib/main.py +341 -0
- moai_adk/templates/.claude/hooks/moai/lib/memory_collector.py +268 -0
- moai_adk/templates/.claude/hooks/moai/lib/metrics_tracker.py +78 -0
- moai_adk/templates/.claude/hooks/moai/lib/models.py +9 -7
- moai_adk/templates/.claude/hooks/moai/lib/path_utils.py +204 -13
- moai_adk/templates/.claude/hooks/moai/lib/project.py +23 -14
- moai_adk/templates/.claude/hooks/moai/lib/renderer.py +359 -0
- moai_adk/templates/.claude/hooks/moai/lib/tag_linkage.py +333 -0
- moai_adk/templates/.claude/hooks/moai/lib/tag_parser.py +176 -0
- moai_adk/templates/.claude/hooks/moai/lib/tag_validator.py +200 -0
- moai_adk/templates/.claude/hooks/moai/lib/timeout.py +5 -5
- moai_adk/templates/.claude/hooks/moai/lib/tool_registry.py +896 -0
- moai_adk/templates/.claude/hooks/moai/lib/unified_timeout_manager.py +30 -18
- moai_adk/templates/.claude/hooks/moai/lib/update_checker.py +129 -0
- moai_adk/templates/.claude/hooks/moai/lib/version_reader.py +741 -0
- moai_adk/templates/.claude/hooks/moai/post_tool__ast_grep_scan.py +276 -0
- moai_adk/templates/.claude/hooks/moai/post_tool__code_formatter.py +255 -0
- moai_adk/templates/.claude/hooks/moai/post_tool__coverage_guard.py +325 -0
- moai_adk/templates/.claude/hooks/moai/post_tool__linter.py +315 -0
- moai_adk/templates/.claude/hooks/moai/post_tool__lsp_diagnostic.py +508 -0
- moai_adk/templates/.claude/hooks/moai/pre_commit__tag_validator.py +287 -0
- moai_adk/templates/.claude/hooks/moai/pre_tool__security_guard.py +268 -0
- moai_adk/templates/.claude/hooks/moai/pre_tool__tdd_enforcer.py +208 -0
- moai_adk/templates/.claude/hooks/moai/session_end__auto_cleanup.py +93 -61
- moai_adk/templates/.claude/hooks/moai/session_end__rank_submit.py +69 -0
- moai_adk/templates/.claude/hooks/moai/session_start__show_project_info.py +165 -70
- moai_adk/templates/.claude/hooks/moai/shared/utils/announcement_translator.py +206 -0
- moai_adk/templates/.claude/hooks/moai/stop__loop_controller.py +621 -0
- moai_adk/templates/.claude/output-styles/moai/alfred.md +758 -0
- moai_adk/templates/.claude/output-styles/moai/r2d2.md +86 -3
- moai_adk/templates/.claude/output-styles/moai/yoda.md +2 -2
- moai_adk/templates/.claude/settings.json +154 -77
- moai_adk/templates/.claude/skills/moai-docs-generation/SKILL.md +252 -198
- moai_adk/templates/.claude/skills/moai-docs-generation/examples.md +169 -323
- moai_adk/templates/.claude/skills/moai-docs-generation/modules/README.md +39 -27
- moai_adk/templates/.claude/skills/moai-docs-generation/modules/api-documentation.md +115 -125
- moai_adk/templates/.claude/skills/moai-docs-generation/modules/code-documentation.md +150 -150
- moai_adk/templates/.claude/skills/moai-docs-generation/modules/multi-format-output.md +182 -175
- moai_adk/templates/.claude/skills/moai-docs-generation/modules/user-guides.md +198 -138
- moai_adk/templates/.claude/skills/moai-docs-generation/reference.md +226 -320
- moai_adk/templates/.claude/skills/moai-domain-backend/SKILL.md +43 -222
- moai_adk/templates/.claude/skills/moai-domain-database/SKILL.md +75 -219
- moai_adk/templates/.claude/skills/moai-domain-frontend/SKILL.md +103 -463
- moai_adk/templates/.claude/skills/moai-domain-frontend/modules/component-architecture.md +723 -0
- moai_adk/templates/.claude/skills/moai-domain-frontend/modules/nextjs16-patterns.md +713 -0
- moai_adk/templates/.claude/skills/moai-domain-frontend/modules/performance-optimization.md +694 -0
- moai_adk/templates/.claude/skills/moai-domain-frontend/modules/react19-patterns.md +591 -0
- moai_adk/templates/.claude/skills/moai-domain-frontend/modules/state-management.md +680 -0
- moai_adk/templates/.claude/skills/moai-domain-frontend/modules/vue35-patterns.md +802 -0
- moai_adk/templates/.claude/skills/moai-domain-uiux/SKILL.md +118 -339
- moai_adk/templates/.claude/skills/moai-formats-data/SKILL.md +74 -377
- moai_adk/templates/.claude/skills/moai-formats-data/modules/README.md +299 -70
- moai_adk/templates/.claude/skills/moai-foundation-claude/SKILL.md +205 -182
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/advanced-agent-patterns.md +370 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-cli-reference-official.md +420 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-custom-slash-commands-official.md +32 -22
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-devcontainers-official.md +381 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-discover-plugins-official.md +379 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-headless-official.md +378 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-hooks-official.md +110 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-plugin-marketplaces-official.md +308 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-plugins-official.md +640 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-sandboxing-official.md +282 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-skills-official.md +425 -71
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-statusline-official.md +293 -0
- moai_adk/templates/.claude/skills/moai-foundation-claude/reference/claude-code-sub-agents-official.md +325 -143
- moai_adk/templates/.claude/skills/moai-foundation-context/SKILL.md +96 -316
- moai_adk/templates/.claude/skills/moai-foundation-core/SKILL.md +116 -294
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/delegation-advanced.md +279 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/delegation-implementation.md +267 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/delegation-patterns.md +121 -650
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/patterns.md +22 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/spec-ears-format.md +200 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/spec-first-tdd.md +37 -730
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/spec-tdd-implementation.md +275 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/trust-5-framework.md +77 -819
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/trust-5-implementation.md +244 -0
- moai_adk/templates/.claude/skills/moai-foundation-core/modules/trust-5-validation.md +219 -0
- moai_adk/templates/.claude/skills/moai-foundation-philosopher/SKILL.md +14 -18
- moai_adk/templates/.claude/skills/moai-foundation-quality/SKILL.md +86 -270
- moai_adk/templates/.claude/skills/moai-framework-electron/SKILL.md +288 -0
- moai_adk/templates/.claude/skills/moai-framework-electron/examples.md +2082 -0
- moai_adk/templates/.claude/skills/moai-framework-electron/reference.md +1649 -0
- moai_adk/templates/.claude/skills/moai-lang-cpp/SKILL.md +76 -582
- moai_adk/templates/.claude/skills/moai-lang-cpp/examples.md +1239 -0
- moai_adk/templates/.claude/skills/moai-lang-cpp/modules/advanced-patterns.md +401 -0
- moai_adk/templates/.claude/skills/moai-lang-cpp/reference.md +1136 -0
- moai_adk/templates/.claude/skills/moai-lang-csharp/SKILL.md +82 -436
- moai_adk/templates/.claude/skills/moai-lang-csharp/examples.md +585 -0
- moai_adk/templates/.claude/skills/moai-lang-csharp/modules/aspnet-core.md +627 -0
- moai_adk/templates/.claude/skills/moai-lang-csharp/modules/blazor-components.md +767 -0
- moai_adk/templates/.claude/skills/moai-lang-csharp/modules/cqrs-validation.md +626 -0
- moai_adk/templates/.claude/skills/moai-lang-csharp/modules/csharp12-features.md +580 -0
- moai_adk/templates/.claude/skills/moai-lang-csharp/modules/efcore-patterns.md +622 -0
- moai_adk/templates/.claude/skills/moai-lang-csharp/reference.md +403 -0
- moai_adk/templates/.claude/skills/moai-lang-elixir/SKILL.md +65 -542
- moai_adk/templates/.claude/skills/moai-lang-elixir/examples.md +1171 -0
- moai_adk/templates/.claude/skills/moai-lang-elixir/modules/advanced-patterns.md +531 -0
- moai_adk/templates/.claude/skills/moai-lang-elixir/reference.md +889 -0
- moai_adk/templates/.claude/skills/moai-lang-flutter/SKILL.md +32 -405
- moai_adk/templates/.claude/skills/moai-lang-go/SKILL.md +114 -293
- moai_adk/templates/.claude/skills/moai-lang-java/SKILL.md +83 -307
- moai_adk/templates/.claude/skills/moai-lang-javascript/SKILL.md +179 -0
- moai_adk/templates/.claude/skills/moai-lang-javascript/examples.md +973 -0
- moai_adk/templates/.claude/skills/moai-lang-javascript/reference.md +1543 -0
- moai_adk/templates/.claude/skills/moai-lang-kotlin/SKILL.md +42 -279
- moai_adk/templates/.claude/skills/moai-lang-php/SKILL.md +94 -556
- moai_adk/templates/.claude/skills/moai-lang-php/examples.md +1608 -0
- moai_adk/templates/.claude/skills/moai-lang-php/modules/advanced-patterns.md +538 -0
- moai_adk/templates/.claude/skills/moai-lang-php/reference.md +1323 -0
- moai_adk/templates/.claude/skills/moai-lang-python/SKILL.md +108 -358
- moai_adk/templates/.claude/skills/moai-lang-r/SKILL.md +84 -482
- moai_adk/templates/.claude/skills/moai-lang-r/examples.md +1154 -0
- moai_adk/templates/.claude/skills/moai-lang-r/modules/advanced-patterns.md +489 -0
- moai_adk/templates/.claude/skills/moai-lang-r/reference.md +1087 -0
- moai_adk/templates/.claude/skills/moai-lang-ruby/SKILL.md +106 -610
- moai_adk/templates/.claude/skills/moai-lang-ruby/examples.md +1106 -0
- moai_adk/templates/.claude/skills/moai-lang-ruby/modules/advanced-patterns.md +309 -0
- moai_adk/templates/.claude/skills/moai-lang-ruby/modules/testing-patterns.md +306 -0
- moai_adk/templates/.claude/skills/moai-lang-ruby/reference.md +1024 -0
- moai_adk/templates/.claude/skills/moai-lang-rust/SKILL.md +51 -265
- moai_adk/templates/.claude/skills/moai-lang-scala/SKILL.md +106 -442
- moai_adk/templates/.claude/skills/moai-lang-scala/modules/akka-actors.md +479 -0
- moai_adk/templates/.claude/skills/moai-lang-scala/modules/cats-effect.md +489 -0
- moai_adk/templates/.claude/skills/moai-lang-scala/modules/functional-programming.md +460 -0
- moai_adk/templates/.claude/skills/moai-lang-scala/modules/spark-data.md +498 -0
- moai_adk/templates/.claude/skills/moai-lang-scala/modules/zio-patterns.md +541 -0
- moai_adk/templates/.claude/skills/moai-lang-swift/SKILL.md +88 -457
- moai_adk/templates/.claude/skills/moai-lang-swift/modules/combine-reactive.md +256 -0
- moai_adk/templates/.claude/skills/moai-lang-swift/modules/concurrency.md +270 -0
- moai_adk/templates/.claude/skills/moai-lang-swift/modules/swift6-features.md +265 -0
- moai_adk/templates/.claude/skills/moai-lang-swift/modules/swiftui-patterns.md +314 -0
- moai_adk/templates/.claude/skills/moai-lang-typescript/SKILL.md +75 -283
- moai_adk/templates/.claude/skills/moai-library-mermaid/SKILL.md +97 -252
- moai_adk/templates/.claude/skills/moai-library-nextra/SKILL.md +64 -240
- moai_adk/templates/.claude/skills/moai-library-nextra/modules/advanced-patterns.md +331 -12
- moai_adk/templates/.claude/skills/moai-library-nextra/modules/configuration.md +330 -37
- moai_adk/templates/.claude/skills/moai-library-shadcn/SKILL.md +90 -287
- moai_adk/templates/.claude/skills/moai-platform-auth0/SKILL.md +200 -206
- moai_adk/templates/.claude/skills/moai-platform-auth0/examples.md +2446 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/adaptive-mfa.md +233 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/akamai-integration.md +214 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/application-credentials.md +280 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/attack-protection-log-events.md +224 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/attack-protection-overview.md +140 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/bot-detection.md +144 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/breached-password-detection.md +187 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/brute-force-protection.md +189 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/certifications.md +282 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/compliance-overview.md +263 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/continuous-session-protection.md +307 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/customize-mfa.md +177 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/dpop-implementation.md +283 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/fapi-implementation.md +259 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/gdpr-compliance.md +313 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/guardian-configuration.md +269 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/highly-regulated-identity.md +272 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/jwt-fundamentals.md +248 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/mdl-verification.md +210 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/mfa-api-management.md +278 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/mfa-factors.md +226 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/mfa-overview.md +174 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/mtls-sender-constraining.md +316 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/ropg-flow-mfa.md +216 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/security-center.md +325 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/security-guidance.md +277 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/state-parameters.md +177 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/step-up-authentication.md +251 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/suspicious-ip-throttling.md +240 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/tenant-access-control.md +179 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/modules/webauthn-fido.md +235 -0
- moai_adk/templates/.claude/skills/moai-platform-auth0/reference.md +224 -0
- moai_adk/templates/.claude/skills/moai-platform-clerk/SKILL.md +75 -330
- moai_adk/templates/.claude/skills/moai-platform-clerk/examples.md +1426 -0
- moai_adk/templates/.claude/skills/moai-platform-clerk/modules/advanced-patterns.md +417 -0
- moai_adk/templates/.claude/skills/moai-platform-clerk/reference.md +273 -0
- moai_adk/templates/.claude/skills/moai-platform-convex/SKILL.md +100 -340
- moai_adk/templates/.claude/skills/moai-platform-convex/examples.md +506 -0
- moai_adk/templates/.claude/skills/moai-platform-convex/modules/auth-integration.md +421 -0
- moai_adk/templates/.claude/skills/moai-platform-convex/modules/file-storage.md +474 -0
- moai_adk/templates/.claude/skills/moai-platform-convex/modules/reactive-queries.md +302 -0
- moai_adk/templates/.claude/skills/moai-platform-convex/modules/server-functions.md +452 -0
- moai_adk/templates/.claude/skills/moai-platform-convex/reference.md +385 -0
- moai_adk/templates/.claude/skills/moai-platform-firebase-auth/SKILL.md +113 -326
- moai_adk/templates/.claude/skills/moai-platform-firebase-auth/examples.md +514 -0
- moai_adk/templates/.claude/skills/moai-platform-firebase-auth/modules/custom-claims.md +374 -0
- moai_adk/templates/.claude/skills/moai-platform-firebase-auth/modules/phone-auth.md +372 -0
- moai_adk/templates/.claude/skills/moai-platform-firebase-auth/modules/social-auth.md +339 -0
- moai_adk/templates/.claude/skills/moai-platform-firebase-auth/reference.md +382 -0
- moai_adk/templates/.claude/skills/moai-platform-firestore/SKILL.md +71 -302
- moai_adk/templates/.claude/skills/moai-platform-firestore/examples.md +445 -0
- moai_adk/templates/.claude/skills/moai-platform-firestore/modules/offline-cache.md +392 -0
- moai_adk/templates/.claude/skills/moai-platform-firestore/modules/realtime-listeners.md +441 -0
- moai_adk/templates/.claude/skills/moai-platform-firestore/modules/security-rules.md +352 -0
- moai_adk/templates/.claude/skills/moai-platform-firestore/modules/transactions.md +452 -0
- moai_adk/templates/.claude/skills/moai-platform-firestore/reference.md +322 -0
- moai_adk/templates/.claude/skills/moai-platform-neon/SKILL.md +101 -412
- moai_adk/templates/.claude/skills/moai-platform-neon/examples.md +470 -0
- moai_adk/templates/.claude/skills/moai-platform-neon/modules/auto-scaling.md +349 -0
- moai_adk/templates/.claude/skills/moai-platform-neon/modules/branching-workflows.md +354 -0
- moai_adk/templates/.claude/skills/moai-platform-neon/modules/connection-pooling.md +412 -0
- moai_adk/templates/.claude/skills/moai-platform-neon/modules/pitr-backups.md +458 -0
- moai_adk/templates/.claude/skills/moai-platform-neon/reference.md +272 -0
- moai_adk/templates/.claude/skills/moai-platform-railway/SKILL.md +96 -327
- moai_adk/templates/.claude/skills/moai-platform-railway/examples.md +539 -0
- moai_adk/templates/.claude/skills/moai-platform-railway/modules/docker-deployment.md +261 -0
- moai_adk/templates/.claude/skills/moai-platform-railway/modules/multi-service.md +291 -0
- moai_adk/templates/.claude/skills/moai-platform-railway/modules/networking-domains.md +338 -0
- moai_adk/templates/.claude/skills/moai-platform-railway/modules/volumes-storage.md +353 -0
- moai_adk/templates/.claude/skills/moai-platform-railway/reference.md +374 -0
- moai_adk/templates/.claude/skills/moai-platform-supabase/SKILL.md +103 -428
- moai_adk/templates/.claude/skills/moai-platform-supabase/examples.md +502 -0
- moai_adk/templates/.claude/skills/moai-platform-supabase/modules/auth-integration.md +384 -0
- moai_adk/templates/.claude/skills/moai-platform-supabase/modules/edge-functions.md +371 -0
- moai_adk/templates/.claude/skills/moai-platform-supabase/modules/postgresql-pgvector.md +231 -0
- moai_adk/templates/.claude/skills/moai-platform-supabase/modules/realtime-presence.md +354 -0
- moai_adk/templates/.claude/skills/moai-platform-supabase/modules/row-level-security.md +286 -0
- moai_adk/templates/.claude/skills/moai-platform-supabase/modules/storage-cdn.md +319 -0
- moai_adk/templates/.claude/skills/moai-platform-supabase/modules/typescript-patterns.md +453 -0
- moai_adk/templates/.claude/skills/moai-platform-supabase/reference.md +284 -0
- moai_adk/templates/.claude/skills/moai-platform-vercel/SKILL.md +96 -446
- moai_adk/templates/.claude/skills/moai-platform-vercel/examples.md +502 -0
- moai_adk/templates/.claude/skills/moai-platform-vercel/modules/analytics-speed.md +348 -0
- moai_adk/templates/.claude/skills/moai-platform-vercel/modules/deployment-config.md +344 -0
- moai_adk/templates/.claude/skills/moai-platform-vercel/modules/edge-functions.md +222 -0
- moai_adk/templates/.claude/skills/moai-platform-vercel/modules/isr-caching.md +306 -0
- moai_adk/templates/.claude/skills/moai-platform-vercel/modules/kv-storage.md +399 -0
- moai_adk/templates/.claude/skills/moai-platform-vercel/reference.md +360 -0
- moai_adk/templates/.claude/skills/moai-tool-ast-grep/SKILL.md +193 -0
- moai_adk/templates/.claude/skills/moai-tool-ast-grep/examples.md +1099 -0
- moai_adk/templates/.claude/skills/moai-tool-ast-grep/modules/language-specific.md +307 -0
- moai_adk/templates/.claude/skills/moai-tool-ast-grep/modules/pattern-syntax.md +237 -0
- moai_adk/templates/.claude/skills/moai-tool-ast-grep/modules/refactoring-patterns.md +260 -0
- moai_adk/templates/.claude/skills/moai-tool-ast-grep/modules/security-rules.md +239 -0
- moai_adk/templates/.claude/skills/moai-tool-ast-grep/reference.md +288 -0
- moai_adk/templates/.claude/skills/moai-tool-ast-grep/rules/languages/go.yml +90 -0
- moai_adk/templates/.claude/skills/moai-tool-ast-grep/rules/languages/python.yml +101 -0
- moai_adk/templates/.claude/skills/moai-tool-ast-grep/rules/languages/typescript.yml +83 -0
- moai_adk/templates/.claude/skills/moai-tool-ast-grep/rules/quality/complexity-check.yml +94 -0
- moai_adk/templates/.claude/skills/moai-tool-ast-grep/rules/quality/deprecated-apis.yml +84 -0
- moai_adk/templates/.claude/skills/moai-tool-ast-grep/rules/security/secrets-detection.yml +89 -0
- moai_adk/templates/.claude/skills/moai-tool-ast-grep/rules/security/sql-injection.yml +45 -0
- moai_adk/templates/.claude/skills/moai-tool-ast-grep/rules/security/xss-prevention.yml +50 -0
- moai_adk/templates/.claude/skills/moai-tool-ast-grep/rules/sgconfig.yml +54 -0
- moai_adk/templates/.claude/skills/moai-workflow-jit-docs/SKILL.md +225 -423
- moai_adk/templates/.claude/skills/moai-workflow-loop/SKILL.md +197 -0
- moai_adk/templates/.claude/skills/moai-workflow-loop/examples.md +1063 -0
- moai_adk/templates/.claude/skills/moai-workflow-loop/reference.md +1414 -0
- moai_adk/templates/.claude/skills/moai-workflow-project/SKILL.md +211 -314
- moai_adk/templates/.claude/skills/moai-workflow-project/schemas/tab_schema.json +15 -43
- moai_adk/templates/.claude/skills/moai-workflow-spec/SKILL.md +119 -316
- moai_adk/templates/.claude/skills/moai-workflow-spec/modules/advanced-patterns.md +237 -0
- moai_adk/templates/.claude/skills/moai-workflow-templates/SKILL.md +96 -203
- moai_adk/templates/.claude/skills/moai-workflow-testing/SKILL.md +201 -388
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/README.md +52 -3
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/ai-debugging.md +263 -806
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/automated-code-review/context7-integration.md +286 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/automated-code-review/review-workflows.md +500 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/automated-code-review/trust5-framework/relevance-analysis.md +154 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/automated-code-review/trust5-framework/safety-analysis.md +148 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/automated-code-review/trust5-framework/scoring-algorithms.md +196 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/automated-code-review/trust5-framework/timeliness-analysis.md +168 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/automated-code-review/trust5-framework/truthfulness-analysis.md +136 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/automated-code-review/trust5-framework/usability-analysis.md +153 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/automated-code-review/trust5-framework.md +257 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/automated-code-review.md +191 -1344
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/code-review/analysis-patterns.md +340 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/code-review/core-classes.md +299 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/code-review/tool-integration.md +380 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/debugging/debugging-workflows.md +451 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/debugging/error-analysis.md +442 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/performance/optimization-patterns.md +473 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/performance/profiling-techniques.md +481 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/performance-optimization/ai-optimization.md +241 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/performance-optimization/bottleneck-detection.md +397 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/performance-optimization/optimization-plan.md +315 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/performance-optimization/profiler-core.md +277 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/performance-optimization/real-time-monitoring.md +187 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/performance-optimization.md +287 -1194
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/quality-metrics.md +415 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/refactoring/ai-workflows.md +620 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/refactoring/patterns.md +692 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/security-analysis.md +429 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/smart-refactoring.md +262 -1192
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/static-analysis.md +438 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/tdd/core-classes.md +397 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/tdd-context7/advanced-features.md +494 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/tdd-context7/red-green-refactor.md +316 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/tdd-context7/test-generation.md +471 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/tdd-context7/test-patterns.md +371 -0
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/tdd-context7.md +227 -1222
- moai_adk/templates/.claude/skills/moai-workflow-testing/modules/trust5-validation.md +428 -0
- moai_adk/templates/.claude/skills/moai-workflow-worktree/SKILL.md +228 -0
- moai_adk/templates/.claude/skills/moai-workflow-worktree/modules/integration-patterns.md +149 -0
- moai_adk/templates/.claude/skills/moai-workflow-worktree/modules/moai-adk-integration.md +245 -0
- moai_adk/templates/.claude/skills/moai-workflow-worktree/modules/parallel-advanced.md +310 -0
- moai_adk/templates/.claude/skills/moai-workflow-worktree/modules/parallel-development.md +202 -0
- moai_adk/templates/.claude/skills/moai-workflow-worktree/modules/parallel-workflows.md +302 -0
- moai_adk/templates/.claude/skills/moai-workflow-worktree/modules/registry-architecture.md +271 -0
- moai_adk/templates/.claude/skills/moai-workflow-worktree/modules/resource-optimization.md +300 -0
- moai_adk/templates/.claude/skills/moai-workflow-worktree/modules/tools-integration.md +280 -0
- moai_adk/templates/.claude/skills/moai-workflow-worktree/modules/troubleshooting.md +397 -0
- moai_adk/templates/.claude/skills/moai-workflow-worktree/modules/worktree-commands.md +296 -0
- moai_adk/templates/.claude/skills/moai-workflow-worktree/modules/worktree-management.md +217 -0
- moai_adk/templates/.git-hooks/pre-push +162 -59
- moai_adk/templates/.github/workflows/ci-universal.yml +934 -133
- moai_adk/templates/.gitignore +65 -107
- moai_adk/templates/.lsp.json +152 -0
- moai_adk/templates/.mcp.json +2 -20
- moai_adk/templates/.moai/announcements/en.json +18 -0
- moai_adk/templates/.moai/announcements/ja.json +18 -0
- moai_adk/templates/.moai/announcements/ko.json +18 -0
- moai_adk/templates/.moai/announcements/zh.json +18 -0
- moai_adk/templates/.moai/config/config.yaml +8 -2
- moai_adk/templates/.moai/config/multilingual-triggers.yaml +213 -0
- moai_adk/templates/.moai/config/sections/language.yaml +2 -2
- moai_adk/templates/.moai/config/sections/llm.yaml +41 -0
- moai_adk/templates/.moai/config/sections/pricing.yaml +30 -0
- moai_adk/templates/.moai/config/sections/project.yaml +2 -2
- moai_adk/templates/.moai/config/sections/quality.yaml +43 -5
- moai_adk/templates/.moai/config/sections/ralph.yaml +55 -0
- moai_adk/templates/.moai/config/sections/system.yaml +46 -1
- moai_adk/templates/.moai/config/sections/user.yaml +1 -1
- moai_adk/templates/.moai/config/statusline-config.yaml +2 -2
- moai_adk/templates/.moai/llm-configs/glm.json +22 -0
- moai_adk/templates/CLAUDE.ja.md +343 -0
- moai_adk/templates/CLAUDE.ko.md +343 -0
- moai_adk/templates/CLAUDE.md +200 -499
- moai_adk/templates/CLAUDE.zh.md +343 -0
- moai_adk/utils/common.py +37 -0
- moai_adk/version.py +1 -1
- moai_adk-1.1.0.dist-info/METADATA +2443 -0
- moai_adk-1.1.0.dist-info/RECORD +701 -0
- {moai_adk-0.34.0.dist-info → moai_adk-1.1.0.dist-info}/entry_points.txt +2 -0
- moai_adk-1.1.0.dist-info/licenses/LICENSE +99 -0
- moai_adk/core/config/auto_spec_config.py +0 -340
- moai_adk/core/hooks/post_tool_auto_spec_completion.py +0 -901
- moai_adk/core/spec/confidence_scoring.py +0 -680
- moai_adk/core/spec/ears_template_engine.py +0 -1247
- moai_adk/core/spec/quality_validator.py +0 -687
- moai_adk/templates/.claude/agents/moai/ai-nano-banana.md +0 -670
- moai_adk/templates/.claude/agents/moai/expert-database.md +0 -777
- moai_adk/templates/.claude/agents/moai/expert-uiux.md +0 -1041
- moai_adk/templates/.claude/agents/moai/mcp-context7.md +0 -458
- moai_adk/templates/.claude/agents/moai/mcp-figma.md +0 -1607
- moai_adk/templates/.claude/agents/moai/mcp-notion.md +0 -789
- moai_adk/templates/.claude/agents/moai/mcp-playwright.md +0 -469
- moai_adk/templates/.claude/agents/moai/mcp-sequential-thinking.md +0 -1032
- moai_adk/templates/.claude/skills/moai-ai-nano-banana/SKILL.md +0 -438
- moai_adk/templates/.claude/skills/moai-ai-nano-banana/examples.md +0 -431
- moai_adk/templates/.claude/skills/moai-domain-uiux/modules/design-system-tokens.md +0 -405
- moai_adk/templates/.claude/skills/moai-library-nextra/advanced-patterns.md +0 -336
- moai_adk/templates/.claude/skills/moai-mcp-figma/SKILL.md +0 -402
- moai_adk/templates/.claude/skills/moai-mcp-figma/advanced-patterns.md +0 -607
- moai_adk/templates/.claude/skills/moai-mcp-notion/SKILL.md +0 -300
- moai_adk/templates/.claude/skills/moai-mcp-notion/advanced-patterns.md +0 -537
- moai_adk/templates/.claude/skills/moai-workflow-project/__init__.py +0 -520
- moai_adk/templates/.claude/skills/moai-workflow-project/complete_workflow_demo_fixed.py +0 -574
- moai_adk/templates/.claude/skills/moai-workflow-project/examples/complete_project_setup.py +0 -317
- moai_adk/templates/.claude/skills/moai-workflow-project/examples/complete_workflow_demo.py +0 -663
- moai_adk/templates/.claude/skills/moai-workflow-project/examples/config-migration-example.json +0 -190
- moai_adk/templates/.claude/skills/moai-workflow-project/examples/question-examples.json +0 -175
- moai_adk/templates/.claude/skills/moai-workflow-project/examples/quick_start.py +0 -196
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/__init__.py +0 -17
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/advanced-patterns.md +0 -158
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/ask_user_integration.py +0 -340
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/batch_questions.py +0 -713
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/config_manager.py +0 -538
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/documentation_manager.py +0 -1336
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/language_initializer.py +0 -730
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/migration_manager.py +0 -608
- moai_adk/templates/.claude/skills/moai-workflow-project/modules/template_optimizer.py +0 -1005
- moai_adk/templates/.claude/skills/moai-workflow-project/test_integration_simple.py +0 -436
- moai_adk/templates/.claude/skills/moai-worktree/SKILL.md +0 -411
- moai_adk/templates/.claude/skills/moai-worktree/modules/integration-patterns.md +0 -982
- moai_adk/templates/.claude/skills/moai-worktree/modules/parallel-development.md +0 -778
- moai_adk/templates/.claude/skills/moai-worktree/modules/worktree-commands.md +0 -646
- moai_adk/templates/.claude/skills/moai-worktree/modules/worktree-management.md +0 -782
- moai_adk/templates/.moai/config/questions/_schema.yaml +0 -151
- moai_adk/templates/.moai/config/questions/tab0-init.yaml +0 -251
- moai_adk/templates/.moai/config/questions/tab1-user.yaml +0 -108
- moai_adk/templates/.moai/config/questions/tab2-project.yaml +0 -81
- moai_adk/templates/.moai/config/questions/tab3-git.yaml +0 -634
- moai_adk/templates/.moai/config/questions/tab4-quality.yaml +0 -170
- moai_adk/templates/.moai/config/questions/tab5-system.yaml +0 -87
- moai_adk/templates/.moai/scripts/setup-glm.py +0 -136
- moai_adk-0.34.0.dist-info/METADATA +0 -2999
- moai_adk-0.34.0.dist-info/RECORD +0 -463
- moai_adk-0.34.0.dist-info/licenses/LICENSE +0 -21
- /moai_adk/foundation/{git.py → git/__init__.py} +0 -0
- /moai_adk/templates/.claude/skills/moai-library-mermaid/{advanced-patterns.md → modules/advanced-patterns.md} +0 -0
- /moai_adk/templates/.claude/skills/moai-library-mermaid/{optimization.md → modules/optimization.md} +0 -0
- /moai_adk/templates/.claude/skills/moai-library-nextra/{optimization.md → modules/optimization.md} +0 -0
- /moai_adk/templates/.claude/skills/moai-workflow-jit-docs/{advanced-patterns.md → modules/advanced-patterns.md} +0 -0
- /moai_adk/templates/.claude/skills/moai-workflow-jit-docs/{optimization.md → modules/optimization.md} +0 -0
- /moai_adk/templates/.claude/skills/moai-workflow-testing/{advanced-patterns.md → modules/advanced-patterns.md} +0 -0
- /moai_adk/templates/.claude/skills/moai-workflow-testing/{optimization.md → modules/optimization.md} +0 -0
- /moai_adk/templates/.claude/skills/{moai-worktree → moai-workflow-worktree}/examples.md +0 -0
- /moai_adk/templates/.claude/skills/{moai-worktree → moai-workflow-worktree}/reference.md +0 -0
- {moai_adk-0.34.0.dist-info → moai_adk-1.1.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,1608 @@
|
|
|
1
|
+
# PHP Production-Ready Code Examples
|
|
2
|
+
|
|
3
|
+
## Complete Laravel 11 Application
|
|
4
|
+
|
|
5
|
+
### Project Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
laravel_app/
|
|
9
|
+
├── app/
|
|
10
|
+
│ ├── Console/
|
|
11
|
+
│ │ └── Commands/
|
|
12
|
+
│ │ └── ProcessOrdersCommand.php
|
|
13
|
+
│ ├── Events/
|
|
14
|
+
│ │ └── UserRegistered.php
|
|
15
|
+
│ ├── Http/
|
|
16
|
+
│ │ ├── Controllers/
|
|
17
|
+
│ │ │ └── Api/
|
|
18
|
+
│ │ │ ├── UserController.php
|
|
19
|
+
│ │ │ └── PostController.php
|
|
20
|
+
│ │ ├── Middleware/
|
|
21
|
+
│ │ │ └── EnsureUserIsVerified.php
|
|
22
|
+
│ │ ├── Requests/
|
|
23
|
+
│ │ │ ├── StoreUserRequest.php
|
|
24
|
+
│ │ │ └── UpdateUserRequest.php
|
|
25
|
+
│ │ └── Resources/
|
|
26
|
+
│ │ ├── UserResource.php
|
|
27
|
+
│ │ └── PostResource.php
|
|
28
|
+
│ ├── Jobs/
|
|
29
|
+
│ │ └── SendWelcomeEmail.php
|
|
30
|
+
│ ├── Listeners/
|
|
31
|
+
│ │ └── SendWelcomeNotification.php
|
|
32
|
+
│ ├── Models/
|
|
33
|
+
│ │ ├── User.php
|
|
34
|
+
│ │ ├── Post.php
|
|
35
|
+
│ │ └── Comment.php
|
|
36
|
+
│ ├── Repositories/
|
|
37
|
+
│ │ ├── UserRepository.php
|
|
38
|
+
│ │ └── PostRepository.php
|
|
39
|
+
│ ├── Services/
|
|
40
|
+
│ │ ├── UserService.php
|
|
41
|
+
│ │ └── AuthService.php
|
|
42
|
+
│ └── DTOs/
|
|
43
|
+
│ ├── UserDTO.php
|
|
44
|
+
│ └── PostDTO.php
|
|
45
|
+
├── database/
|
|
46
|
+
│ ├── factories/
|
|
47
|
+
│ │ └── UserFactory.php
|
|
48
|
+
│ ├── migrations/
|
|
49
|
+
│ │ └── 2024_01_01_create_users_table.php
|
|
50
|
+
│ └── seeders/
|
|
51
|
+
│ └── UserSeeder.php
|
|
52
|
+
├── routes/
|
|
53
|
+
│ ├── api.php
|
|
54
|
+
│ └── web.php
|
|
55
|
+
├── tests/
|
|
56
|
+
│ ├── Feature/
|
|
57
|
+
│ │ └── UserApiTest.php
|
|
58
|
+
│ └── Unit/
|
|
59
|
+
│ └── UserServiceTest.php
|
|
60
|
+
├── composer.json
|
|
61
|
+
└── Dockerfile
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Eloquent Models with Modern Features
|
|
65
|
+
|
|
66
|
+
```php
|
|
67
|
+
<?php
|
|
68
|
+
|
|
69
|
+
namespace App\Models;
|
|
70
|
+
|
|
71
|
+
use App\Enums\UserRole;
|
|
72
|
+
use App\Enums\UserStatus;
|
|
73
|
+
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
|
74
|
+
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
75
|
+
use Illuminate\Database\Eloquent\Model;
|
|
76
|
+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
77
|
+
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
78
|
+
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
79
|
+
|
|
80
|
+
class User extends Model
|
|
81
|
+
{
|
|
82
|
+
use HasFactory, SoftDeletes, HasUuids;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The attributes that are mass assignable.
|
|
86
|
+
*/
|
|
87
|
+
protected $fillable = [
|
|
88
|
+
'name',
|
|
89
|
+
'email',
|
|
90
|
+
'password',
|
|
91
|
+
'role',
|
|
92
|
+
'status',
|
|
93
|
+
'email_verified_at',
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The attributes that should be hidden for serialization.
|
|
98
|
+
*/
|
|
99
|
+
protected $hidden = [
|
|
100
|
+
'password',
|
|
101
|
+
'remember_token',
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get the attributes that should be cast.
|
|
106
|
+
*/
|
|
107
|
+
protected function casts(): array
|
|
108
|
+
{
|
|
109
|
+
return [
|
|
110
|
+
'email_verified_at' => 'datetime',
|
|
111
|
+
'password' => 'hashed',
|
|
112
|
+
'role' => UserRole::class,
|
|
113
|
+
'status' => UserStatus::class,
|
|
114
|
+
'created_at' => 'datetime',
|
|
115
|
+
'updated_at' => 'datetime',
|
|
116
|
+
'deleted_at' => 'datetime',
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get the posts for the user.
|
|
122
|
+
*/
|
|
123
|
+
public function posts(): HasMany
|
|
124
|
+
{
|
|
125
|
+
return $this->hasMany(Post::class);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get the comments for the user.
|
|
130
|
+
*/
|
|
131
|
+
public function comments(): HasMany
|
|
132
|
+
{
|
|
133
|
+
return $this->hasMany(Comment::class);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Scope a query to only include active users.
|
|
138
|
+
*/
|
|
139
|
+
public function scopeActive($query)
|
|
140
|
+
{
|
|
141
|
+
return $query->where('status', UserStatus::Active);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Scope a query to only include verified users.
|
|
146
|
+
*/
|
|
147
|
+
public function scopeVerified($query)
|
|
148
|
+
{
|
|
149
|
+
return $query->whereNotNull('email_verified_at');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get the user's full name.
|
|
154
|
+
*/
|
|
155
|
+
public function getFullNameAttribute(): string
|
|
156
|
+
{
|
|
157
|
+
return "{$this->first_name} {$this->last_name}";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Check if user is admin.
|
|
162
|
+
*/
|
|
163
|
+
public function isAdmin(): bool
|
|
164
|
+
{
|
|
165
|
+
return $this->role === UserRole::Admin;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```php
|
|
171
|
+
<?php
|
|
172
|
+
|
|
173
|
+
namespace App\Models;
|
|
174
|
+
|
|
175
|
+
use App\Enums\PostStatus;
|
|
176
|
+
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
|
177
|
+
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
178
|
+
use Illuminate\Database\Eloquent\Model;
|
|
179
|
+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
180
|
+
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
181
|
+
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
182
|
+
|
|
183
|
+
class Post extends Model
|
|
184
|
+
{
|
|
185
|
+
use HasFactory, SoftDeletes, HasUuids;
|
|
186
|
+
|
|
187
|
+
protected $fillable = [
|
|
188
|
+
'user_id',
|
|
189
|
+
'title',
|
|
190
|
+
'slug',
|
|
191
|
+
'content',
|
|
192
|
+
'excerpt',
|
|
193
|
+
'status',
|
|
194
|
+
'published_at',
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
protected function casts(): array
|
|
198
|
+
{
|
|
199
|
+
return [
|
|
200
|
+
'status' => PostStatus::class,
|
|
201
|
+
'published_at' => 'datetime',
|
|
202
|
+
'created_at' => 'datetime',
|
|
203
|
+
'updated_at' => 'datetime',
|
|
204
|
+
];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get the user that owns the post.
|
|
209
|
+
*/
|
|
210
|
+
public function user(): BelongsTo
|
|
211
|
+
{
|
|
212
|
+
return $this->belongsTo(User::class);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get the comments for the post.
|
|
217
|
+
*/
|
|
218
|
+
public function comments(): HasMany
|
|
219
|
+
{
|
|
220
|
+
return $this->hasMany(Comment::class);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Scope a query to only include published posts.
|
|
225
|
+
*/
|
|
226
|
+
public function scopePublished($query)
|
|
227
|
+
{
|
|
228
|
+
return $query->where('status', PostStatus::Published)
|
|
229
|
+
->whereNotNull('published_at')
|
|
230
|
+
->where('published_at', '<=', now());
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Scope a query to search posts.
|
|
235
|
+
*/
|
|
236
|
+
public function scopeSearch($query, string $search)
|
|
237
|
+
{
|
|
238
|
+
return $query->where(function ($q) use ($search) {
|
|
239
|
+
$q->where('title', 'like', "%{$search}%")
|
|
240
|
+
->orWhere('content', 'like', "%{$search}%");
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get the route key for the model.
|
|
246
|
+
*/
|
|
247
|
+
public function getRouteKeyName(): string
|
|
248
|
+
{
|
|
249
|
+
return 'slug';
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Modern PHP 8.3 Enums
|
|
255
|
+
|
|
256
|
+
```php
|
|
257
|
+
<?php
|
|
258
|
+
|
|
259
|
+
namespace App\Enums;
|
|
260
|
+
|
|
261
|
+
enum UserRole: string
|
|
262
|
+
{
|
|
263
|
+
case Admin = 'admin';
|
|
264
|
+
case Editor = 'editor';
|
|
265
|
+
case Author = 'author';
|
|
266
|
+
case Subscriber = 'subscriber';
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get the label for the role.
|
|
270
|
+
*/
|
|
271
|
+
public function label(): string
|
|
272
|
+
{
|
|
273
|
+
return match($this) {
|
|
274
|
+
self::Admin => 'Administrator',
|
|
275
|
+
self::Editor => 'Editor',
|
|
276
|
+
self::Author => 'Author',
|
|
277
|
+
self::Subscriber => 'Subscriber',
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Get all permissions for the role.
|
|
283
|
+
*/
|
|
284
|
+
public function permissions(): array
|
|
285
|
+
{
|
|
286
|
+
return match($this) {
|
|
287
|
+
self::Admin => ['*'],
|
|
288
|
+
self::Editor => ['posts.create', 'posts.edit', 'posts.delete', 'posts.publish'],
|
|
289
|
+
self::Author => ['posts.create', 'posts.edit', 'posts.delete'],
|
|
290
|
+
self::Subscriber => ['posts.read'],
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Check if role can perform action.
|
|
296
|
+
*/
|
|
297
|
+
public function can(string $permission): bool
|
|
298
|
+
{
|
|
299
|
+
$permissions = $this->permissions();
|
|
300
|
+
|
|
301
|
+
if (in_array('*', $permissions)) {
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return in_array($permission, $permissions);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Get all available roles.
|
|
310
|
+
*/
|
|
311
|
+
public static function values(): array
|
|
312
|
+
{
|
|
313
|
+
return array_column(self::cases(), 'value');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
```php
|
|
319
|
+
<?php
|
|
320
|
+
|
|
321
|
+
namespace App\Enums;
|
|
322
|
+
|
|
323
|
+
enum PostStatus: string
|
|
324
|
+
{
|
|
325
|
+
case Draft = 'draft';
|
|
326
|
+
case Published = 'published';
|
|
327
|
+
case Archived = 'archived';
|
|
328
|
+
|
|
329
|
+
public function label(): string
|
|
330
|
+
{
|
|
331
|
+
return match($this) {
|
|
332
|
+
self::Draft => 'Draft',
|
|
333
|
+
self::Published => 'Published',
|
|
334
|
+
self::Archived => 'Archived',
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
public function color(): string
|
|
339
|
+
{
|
|
340
|
+
return match($this) {
|
|
341
|
+
self::Draft => 'gray',
|
|
342
|
+
self::Published => 'green',
|
|
343
|
+
self::Archived => 'red',
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### DTOs with Readonly Classes
|
|
350
|
+
|
|
351
|
+
```php
|
|
352
|
+
<?php
|
|
353
|
+
|
|
354
|
+
namespace App\DTOs;
|
|
355
|
+
|
|
356
|
+
readonly class UserDTO
|
|
357
|
+
{
|
|
358
|
+
public function __construct(
|
|
359
|
+
public int $id,
|
|
360
|
+
public string $name,
|
|
361
|
+
public string $email,
|
|
362
|
+
public UserRole $role,
|
|
363
|
+
public UserStatus $status,
|
|
364
|
+
public ?\DateTimeImmutable $emailVerifiedAt = null,
|
|
365
|
+
) {}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Create from Eloquent model.
|
|
369
|
+
*/
|
|
370
|
+
public static function fromModel(User $user): self
|
|
371
|
+
{
|
|
372
|
+
return new self(
|
|
373
|
+
id: $user->id,
|
|
374
|
+
name: $user->name,
|
|
375
|
+
email: $user->email,
|
|
376
|
+
role: $user->role,
|
|
377
|
+
status: $user->status,
|
|
378
|
+
emailVerifiedAt: $user->email_verified_at?->toDateTimeImmutable(),
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Create from array.
|
|
384
|
+
*/
|
|
385
|
+
public static function fromArray(array $data): self
|
|
386
|
+
{
|
|
387
|
+
return new self(
|
|
388
|
+
id: $data['id'],
|
|
389
|
+
name: $data['name'],
|
|
390
|
+
email: $data['email'],
|
|
391
|
+
role: UserRole::from($data['role']),
|
|
392
|
+
status: UserStatus::from($data['status']),
|
|
393
|
+
emailVerifiedAt: isset($data['email_verified_at'])
|
|
394
|
+
? new \DateTimeImmutable($data['email_verified_at'])
|
|
395
|
+
: null,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Convert to array.
|
|
401
|
+
*/
|
|
402
|
+
public function toArray(): array
|
|
403
|
+
{
|
|
404
|
+
return [
|
|
405
|
+
'id' => $this->id,
|
|
406
|
+
'name' => $this->name,
|
|
407
|
+
'email' => $this->email,
|
|
408
|
+
'role' => $this->role->value,
|
|
409
|
+
'status' => $this->status->value,
|
|
410
|
+
'email_verified_at' => $this->emailVerifiedAt?->format('Y-m-d H:i:s'),
|
|
411
|
+
];
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Repository Pattern
|
|
417
|
+
|
|
418
|
+
```php
|
|
419
|
+
<?php
|
|
420
|
+
|
|
421
|
+
namespace App\Repositories;
|
|
422
|
+
|
|
423
|
+
use App\Models\User;
|
|
424
|
+
use App\DTOs\UserDTO;
|
|
425
|
+
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
426
|
+
use Illuminate\Database\Eloquent\Collection;
|
|
427
|
+
|
|
428
|
+
class UserRepository
|
|
429
|
+
{
|
|
430
|
+
/**
|
|
431
|
+
* Find user by ID.
|
|
432
|
+
*/
|
|
433
|
+
public function find(int $id): ?User
|
|
434
|
+
{
|
|
435
|
+
return User::find($id);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Find user by email.
|
|
440
|
+
*/
|
|
441
|
+
public function findByEmail(string $email): ?User
|
|
442
|
+
{
|
|
443
|
+
return User::where('email', $email)->first();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Get all users with pagination.
|
|
448
|
+
*/
|
|
449
|
+
public function paginate(int $perPage = 15): LengthAwarePaginator
|
|
450
|
+
{
|
|
451
|
+
return User::with(['posts'])
|
|
452
|
+
->latest()
|
|
453
|
+
->paginate($perPage);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Get active users.
|
|
458
|
+
*/
|
|
459
|
+
public function getActive(): Collection
|
|
460
|
+
{
|
|
461
|
+
return User::active()
|
|
462
|
+
->verified()
|
|
463
|
+
->orderBy('name')
|
|
464
|
+
->get();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Create a new user.
|
|
469
|
+
*/
|
|
470
|
+
public function create(array $data): User
|
|
471
|
+
{
|
|
472
|
+
return User::create([
|
|
473
|
+
'name' => $data['name'],
|
|
474
|
+
'email' => $data['email'],
|
|
475
|
+
'password' => $data['password'], // Will be hashed by cast
|
|
476
|
+
'role' => $data['role'] ?? UserRole::Subscriber,
|
|
477
|
+
'status' => UserStatus::Active,
|
|
478
|
+
]);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Update user.
|
|
483
|
+
*/
|
|
484
|
+
public function update(User $user, array $data): User
|
|
485
|
+
{
|
|
486
|
+
$user->update($data);
|
|
487
|
+
return $user->fresh();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Delete user (soft delete).
|
|
492
|
+
*/
|
|
493
|
+
public function delete(User $user): bool
|
|
494
|
+
{
|
|
495
|
+
return $user->delete();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Force delete user.
|
|
500
|
+
*/
|
|
501
|
+
public function forceDelete(User $user): bool
|
|
502
|
+
{
|
|
503
|
+
return $user->forceDelete();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Search users.
|
|
508
|
+
*/
|
|
509
|
+
public function search(string $query, int $perPage = 15): LengthAwarePaginator
|
|
510
|
+
{
|
|
511
|
+
return User::where('name', 'like', "%{$query}%")
|
|
512
|
+
->orWhere('email', 'like', "%{$query}%")
|
|
513
|
+
->paginate($perPage);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Get users by role.
|
|
518
|
+
*/
|
|
519
|
+
public function getByRole(UserRole $role): Collection
|
|
520
|
+
{
|
|
521
|
+
return User::where('role', $role)->get();
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Service Layer with Business Logic
|
|
527
|
+
|
|
528
|
+
```php
|
|
529
|
+
<?php
|
|
530
|
+
|
|
531
|
+
namespace App\Services;
|
|
532
|
+
|
|
533
|
+
use App\DTOs\UserDTO;
|
|
534
|
+
use App\Events\UserRegistered;
|
|
535
|
+
use App\Models\User;
|
|
536
|
+
use App\Repositories\UserRepository;
|
|
537
|
+
use Illuminate\Support\Facades\DB;
|
|
538
|
+
use Illuminate\Support\Facades\Hash;
|
|
539
|
+
|
|
540
|
+
class UserService
|
|
541
|
+
{
|
|
542
|
+
public function __construct(
|
|
543
|
+
private readonly UserRepository $repository,
|
|
544
|
+
) {}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Register a new user.
|
|
548
|
+
*/
|
|
549
|
+
public function register(array $data): UserDTO
|
|
550
|
+
{
|
|
551
|
+
return DB::transaction(function () use ($data) {
|
|
552
|
+
$user = $this->repository->create([
|
|
553
|
+
'name' => $data['name'],
|
|
554
|
+
'email' => $data['email'],
|
|
555
|
+
'password' => Hash::make($data['password']),
|
|
556
|
+
'role' => UserRole::Subscriber,
|
|
557
|
+
]);
|
|
558
|
+
|
|
559
|
+
event(new UserRegistered($user));
|
|
560
|
+
|
|
561
|
+
return UserDTO::fromModel($user);
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Update user profile.
|
|
567
|
+
*/
|
|
568
|
+
public function updateProfile(User $user, array $data): UserDTO
|
|
569
|
+
{
|
|
570
|
+
$updateData = [];
|
|
571
|
+
|
|
572
|
+
if (isset($data['name'])) {
|
|
573
|
+
$updateData['name'] = $data['name'];
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (isset($data['email']) && $data['email'] !== $user->email) {
|
|
577
|
+
$updateData['email'] = $data['email'];
|
|
578
|
+
$updateData['email_verified_at'] = null;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (isset($data['password'])) {
|
|
582
|
+
$updateData['password'] = Hash::make($data['password']);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
$updatedUser = $this->repository->update($user, $updateData);
|
|
586
|
+
|
|
587
|
+
return UserDTO::fromModel($updatedUser);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Verify user email.
|
|
592
|
+
*/
|
|
593
|
+
public function verifyEmail(User $user): void
|
|
594
|
+
{
|
|
595
|
+
if ($user->email_verified_at !== null) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
$user->update(['email_verified_at' => now()]);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Change user role.
|
|
604
|
+
*/
|
|
605
|
+
public function changeRole(User $user, UserRole $newRole): UserDTO
|
|
606
|
+
{
|
|
607
|
+
$user->update(['role' => $newRole]);
|
|
608
|
+
return UserDTO::fromModel($user->fresh());
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Get user statistics.
|
|
613
|
+
*/
|
|
614
|
+
public function getUserStatistics(User $user): array
|
|
615
|
+
{
|
|
616
|
+
return [
|
|
617
|
+
'total_posts' => $user->posts()->count(),
|
|
618
|
+
'published_posts' => $user->posts()->published()->count(),
|
|
619
|
+
'total_comments' => $user->comments()->count(),
|
|
620
|
+
'member_since' => $user->created_at->diffForHumans(),
|
|
621
|
+
];
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### Form Requests with Validation
|
|
627
|
+
|
|
628
|
+
```php
|
|
629
|
+
<?php
|
|
630
|
+
|
|
631
|
+
namespace App\Http\Requests;
|
|
632
|
+
|
|
633
|
+
use App\Enums\UserRole;
|
|
634
|
+
use Illuminate\Foundation\Http\FormRequest;
|
|
635
|
+
use Illuminate\Validation\Rules\Enum;
|
|
636
|
+
use Illuminate\Validation\Rules\Password;
|
|
637
|
+
|
|
638
|
+
class StoreUserRequest extends FormRequest
|
|
639
|
+
{
|
|
640
|
+
/**
|
|
641
|
+
* Determine if the user is authorized to make this request.
|
|
642
|
+
*/
|
|
643
|
+
public function authorize(): bool
|
|
644
|
+
{
|
|
645
|
+
return $this->user()?->isAdmin() ?? false;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Get the validation rules that apply to the request.
|
|
650
|
+
*/
|
|
651
|
+
public function rules(): array
|
|
652
|
+
{
|
|
653
|
+
return [
|
|
654
|
+
'name' => ['required', 'string', 'max:255'],
|
|
655
|
+
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
|
|
656
|
+
'password' => [
|
|
657
|
+
'required',
|
|
658
|
+
'confirmed',
|
|
659
|
+
Password::min(8)
|
|
660
|
+
->letters()
|
|
661
|
+
->mixedCase()
|
|
662
|
+
->numbers()
|
|
663
|
+
->symbols()
|
|
664
|
+
->uncompromised(),
|
|
665
|
+
],
|
|
666
|
+
'role' => ['required', new Enum(UserRole::class)],
|
|
667
|
+
];
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Get custom messages for validator errors.
|
|
672
|
+
*/
|
|
673
|
+
public function messages(): array
|
|
674
|
+
{
|
|
675
|
+
return [
|
|
676
|
+
'name.required' => 'A name is required',
|
|
677
|
+
'email.required' => 'An email address is required',
|
|
678
|
+
'email.unique' => 'This email is already registered',
|
|
679
|
+
'password.confirmed' => 'Password confirmation does not match',
|
|
680
|
+
];
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Get custom attributes for validator errors.
|
|
685
|
+
*/
|
|
686
|
+
public function attributes(): array
|
|
687
|
+
{
|
|
688
|
+
return [
|
|
689
|
+
'email' => 'email address',
|
|
690
|
+
];
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Prepare the data for validation.
|
|
695
|
+
*/
|
|
696
|
+
protected function prepareForValidation(): void
|
|
697
|
+
{
|
|
698
|
+
$this->merge([
|
|
699
|
+
'email' => strtolower($this->email),
|
|
700
|
+
]);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
```php
|
|
706
|
+
<?php
|
|
707
|
+
|
|
708
|
+
namespace App\Http\Requests;
|
|
709
|
+
|
|
710
|
+
use Illuminate\Foundation\Http\FormRequest;
|
|
711
|
+
use Illuminate\Validation\Rule;
|
|
712
|
+
use Illuminate\Validation\Rules\Password;
|
|
713
|
+
|
|
714
|
+
class UpdateUserRequest extends FormRequest
|
|
715
|
+
{
|
|
716
|
+
public function authorize(): bool
|
|
717
|
+
{
|
|
718
|
+
$user = $this->route('user');
|
|
719
|
+
|
|
720
|
+
// Users can update their own profile, admins can update anyone
|
|
721
|
+
return $this->user()?->id === $user->id
|
|
722
|
+
|| $this->user()?->isAdmin();
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
public function rules(): array
|
|
726
|
+
{
|
|
727
|
+
$userId = $this->route('user')->id;
|
|
728
|
+
|
|
729
|
+
return [
|
|
730
|
+
'name' => ['sometimes', 'string', 'max:255'],
|
|
731
|
+
'email' => [
|
|
732
|
+
'sometimes',
|
|
733
|
+
'email',
|
|
734
|
+
'max:255',
|
|
735
|
+
Rule::unique('users', 'email')->ignore($userId),
|
|
736
|
+
],
|
|
737
|
+
'password' => [
|
|
738
|
+
'sometimes',
|
|
739
|
+
'confirmed',
|
|
740
|
+
Password::min(8)->letters()->numbers(),
|
|
741
|
+
],
|
|
742
|
+
];
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
### API Resources for Response Transformation
|
|
748
|
+
|
|
749
|
+
```php
|
|
750
|
+
<?php
|
|
751
|
+
|
|
752
|
+
namespace App\Http\Resources;
|
|
753
|
+
|
|
754
|
+
use Illuminate\Http\Request;
|
|
755
|
+
use Illuminate\Http\Resources\Json\JsonResource;
|
|
756
|
+
|
|
757
|
+
class UserResource extends JsonResource
|
|
758
|
+
{
|
|
759
|
+
/**
|
|
760
|
+
* Transform the resource into an array.
|
|
761
|
+
*/
|
|
762
|
+
public function toArray(Request $request): array
|
|
763
|
+
{
|
|
764
|
+
return [
|
|
765
|
+
'id' => $this->id,
|
|
766
|
+
'name' => $this->name,
|
|
767
|
+
'email' => $this->email,
|
|
768
|
+
'role' => [
|
|
769
|
+
'value' => $this->role->value,
|
|
770
|
+
'label' => $this->role->label(),
|
|
771
|
+
],
|
|
772
|
+
'status' => [
|
|
773
|
+
'value' => $this->status->value,
|
|
774
|
+
'label' => $this->status->label(),
|
|
775
|
+
],
|
|
776
|
+
'email_verified_at' => $this->email_verified_at?->toIso8601String(),
|
|
777
|
+
'posts_count' => $this->whenCounted('posts'),
|
|
778
|
+
'posts' => PostResource::collection($this->whenLoaded('posts')),
|
|
779
|
+
'created_at' => $this->created_at->toIso8601String(),
|
|
780
|
+
'updated_at' => $this->updated_at->toIso8601String(),
|
|
781
|
+
];
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Get additional data that should be returned with the resource array.
|
|
786
|
+
*/
|
|
787
|
+
public function with(Request $request): array
|
|
788
|
+
{
|
|
789
|
+
return [
|
|
790
|
+
'meta' => [
|
|
791
|
+
'version' => '1.0.0',
|
|
792
|
+
],
|
|
793
|
+
];
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
```php
|
|
799
|
+
<?php
|
|
800
|
+
|
|
801
|
+
namespace App\Http\Resources;
|
|
802
|
+
|
|
803
|
+
use Illuminate\Http\Request;
|
|
804
|
+
use Illuminate\Http\Resources\Json\ResourceCollection;
|
|
805
|
+
|
|
806
|
+
class UserCollection extends ResourceCollection
|
|
807
|
+
{
|
|
808
|
+
/**
|
|
809
|
+
* Transform the resource collection into an array.
|
|
810
|
+
*/
|
|
811
|
+
public function toArray(Request $request): array
|
|
812
|
+
{
|
|
813
|
+
return [
|
|
814
|
+
'data' => $this->collection,
|
|
815
|
+
'links' => [
|
|
816
|
+
'self' => url()->current(),
|
|
817
|
+
],
|
|
818
|
+
'meta' => [
|
|
819
|
+
'total' => $this->total(),
|
|
820
|
+
'per_page' => $this->perPage(),
|
|
821
|
+
'current_page' => $this->currentPage(),
|
|
822
|
+
'last_page' => $this->lastPage(),
|
|
823
|
+
],
|
|
824
|
+
];
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
### API Controllers
|
|
830
|
+
|
|
831
|
+
```php
|
|
832
|
+
<?php
|
|
833
|
+
|
|
834
|
+
namespace App\Http\Controllers\Api;
|
|
835
|
+
|
|
836
|
+
use App\Http\Controllers\Controller;
|
|
837
|
+
use App\Http\Requests\StoreUserRequest;
|
|
838
|
+
use App\Http\Requests\UpdateUserRequest;
|
|
839
|
+
use App\Http\Resources\UserResource;
|
|
840
|
+
use App\Http\Resources\UserCollection;
|
|
841
|
+
use App\Models\User;
|
|
842
|
+
use App\Services\UserService;
|
|
843
|
+
use Illuminate\Http\JsonResponse;
|
|
844
|
+
use Illuminate\Http\Request;
|
|
845
|
+
|
|
846
|
+
class UserController extends Controller
|
|
847
|
+
{
|
|
848
|
+
public function __construct(
|
|
849
|
+
private readonly UserService $userService,
|
|
850
|
+
) {}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Display a listing of the resource.
|
|
854
|
+
*/
|
|
855
|
+
public function index(Request $request): UserCollection
|
|
856
|
+
{
|
|
857
|
+
$perPage = $request->input('per_page', 15);
|
|
858
|
+
$users = User::with(['posts'])
|
|
859
|
+
->latest()
|
|
860
|
+
->paginate($perPage);
|
|
861
|
+
|
|
862
|
+
return new UserCollection($users);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Store a newly created resource in storage.
|
|
867
|
+
*/
|
|
868
|
+
public function store(StoreUserRequest $request): JsonResponse
|
|
869
|
+
{
|
|
870
|
+
$userDTO = $this->userService->register($request->validated());
|
|
871
|
+
|
|
872
|
+
return response()->json(
|
|
873
|
+
new UserResource(User::find($userDTO->id)),
|
|
874
|
+
201
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Display the specified resource.
|
|
880
|
+
*/
|
|
881
|
+
public function show(User $user): UserResource
|
|
882
|
+
{
|
|
883
|
+
$user->loadMissing(['posts', 'comments']);
|
|
884
|
+
return new UserResource($user);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Update the specified resource in storage.
|
|
889
|
+
*/
|
|
890
|
+
public function update(UpdateUserRequest $request, User $user): UserResource
|
|
891
|
+
{
|
|
892
|
+
$userDTO = $this->userService->updateProfile($user, $request->validated());
|
|
893
|
+
return new UserResource(User::find($userDTO->id));
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Remove the specified resource from storage.
|
|
898
|
+
*/
|
|
899
|
+
public function destroy(User $user): JsonResponse
|
|
900
|
+
{
|
|
901
|
+
$user->delete();
|
|
902
|
+
|
|
903
|
+
return response()->json(null, 204);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Get current authenticated user.
|
|
908
|
+
*/
|
|
909
|
+
public function me(Request $request): UserResource
|
|
910
|
+
{
|
|
911
|
+
return new UserResource($request->user());
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Get user statistics.
|
|
916
|
+
*/
|
|
917
|
+
public function statistics(User $user): JsonResponse
|
|
918
|
+
{
|
|
919
|
+
$stats = $this->userService->getUserStatistics($user);
|
|
920
|
+
|
|
921
|
+
return response()->json([
|
|
922
|
+
'data' => $stats,
|
|
923
|
+
]);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
---
|
|
929
|
+
|
|
930
|
+
## Queue Jobs and Events
|
|
931
|
+
|
|
932
|
+
### Job Classes
|
|
933
|
+
|
|
934
|
+
```php
|
|
935
|
+
<?php
|
|
936
|
+
|
|
937
|
+
namespace App\Jobs;
|
|
938
|
+
|
|
939
|
+
use App\Models\User;
|
|
940
|
+
use App\Notifications\WelcomeNotification;
|
|
941
|
+
use Illuminate\Bus\Queueable;
|
|
942
|
+
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
943
|
+
use Illuminate\Foundation\Bus\Dispatchable;
|
|
944
|
+
use Illuminate\Queue\InteractsWithQueue;
|
|
945
|
+
use Illuminate\Queue\SerializesModels;
|
|
946
|
+
|
|
947
|
+
class SendWelcomeEmail implements ShouldQueue
|
|
948
|
+
{
|
|
949
|
+
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* The number of times the job may be attempted.
|
|
953
|
+
*/
|
|
954
|
+
public int $tries = 3;
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* The maximum number of unhandled exceptions to allow before failing.
|
|
958
|
+
*/
|
|
959
|
+
public int $maxExceptions = 3;
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* The number of seconds the job can run before timing out.
|
|
963
|
+
*/
|
|
964
|
+
public int $timeout = 60;
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Create a new job instance.
|
|
968
|
+
*/
|
|
969
|
+
public function __construct(
|
|
970
|
+
public User $user,
|
|
971
|
+
) {}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Execute the job.
|
|
975
|
+
*/
|
|
976
|
+
public function handle(): void
|
|
977
|
+
{
|
|
978
|
+
$this->user->notify(new WelcomeNotification());
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Handle a job failure.
|
|
983
|
+
*/
|
|
984
|
+
public function failed(\Throwable $exception): void
|
|
985
|
+
{
|
|
986
|
+
// Log failure or send notification
|
|
987
|
+
logger()->error('Welcome email failed', [
|
|
988
|
+
'user_id' => $this->user->id,
|
|
989
|
+
'exception' => $exception->getMessage(),
|
|
990
|
+
]);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
### Event Classes
|
|
996
|
+
|
|
997
|
+
```php
|
|
998
|
+
<?php
|
|
999
|
+
|
|
1000
|
+
namespace App\Events;
|
|
1001
|
+
|
|
1002
|
+
use App\Models\User;
|
|
1003
|
+
use Illuminate\Broadcasting\Channel;
|
|
1004
|
+
use Illuminate\Broadcasting\InteractsWithSockets;
|
|
1005
|
+
use Illuminate\Broadcasting\PresenceChannel;
|
|
1006
|
+
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
|
1007
|
+
use Illuminate\Foundation\Events\Dispatchable;
|
|
1008
|
+
use Illuminate\Queue\SerializesModels;
|
|
1009
|
+
|
|
1010
|
+
class UserRegistered implements ShouldBroadcast
|
|
1011
|
+
{
|
|
1012
|
+
use Dispatchable, InteractsWithSockets, SerializesModels;
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Create a new event instance.
|
|
1016
|
+
*/
|
|
1017
|
+
public function __construct(
|
|
1018
|
+
public User $user,
|
|
1019
|
+
) {}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Get the channels the event should broadcast on.
|
|
1023
|
+
*/
|
|
1024
|
+
public function broadcastOn(): array
|
|
1025
|
+
{
|
|
1026
|
+
return [
|
|
1027
|
+
new PresenceChannel('admin'),
|
|
1028
|
+
new Channel('users'),
|
|
1029
|
+
];
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* The event's broadcast name.
|
|
1034
|
+
*/
|
|
1035
|
+
public function broadcastAs(): string
|
|
1036
|
+
{
|
|
1037
|
+
return 'user.registered';
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Get the data to broadcast.
|
|
1042
|
+
*/
|
|
1043
|
+
public function broadcastWith(): array
|
|
1044
|
+
{
|
|
1045
|
+
return [
|
|
1046
|
+
'id' => $this->user->id,
|
|
1047
|
+
'name' => $this->user->name,
|
|
1048
|
+
'email' => $this->user->email,
|
|
1049
|
+
'registered_at' => now()->toIso8601String(),
|
|
1050
|
+
];
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
### Event Listeners
|
|
1056
|
+
|
|
1057
|
+
```php
|
|
1058
|
+
<?php
|
|
1059
|
+
|
|
1060
|
+
namespace App\Listeners;
|
|
1061
|
+
|
|
1062
|
+
use App\Events\UserRegistered;
|
|
1063
|
+
use App\Jobs\SendWelcomeEmail;
|
|
1064
|
+
|
|
1065
|
+
class SendWelcomeNotification
|
|
1066
|
+
{
|
|
1067
|
+
/**
|
|
1068
|
+
* Handle the event.
|
|
1069
|
+
*/
|
|
1070
|
+
public function handle(UserRegistered $event): void
|
|
1071
|
+
{
|
|
1072
|
+
// Dispatch job to queue
|
|
1073
|
+
SendWelcomeEmail::dispatch($event->user)
|
|
1074
|
+
->onQueue('emails')
|
|
1075
|
+
->delay(now()->addMinutes(5));
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
---
|
|
1081
|
+
|
|
1082
|
+
## Testing with PHPUnit and Pest
|
|
1083
|
+
|
|
1084
|
+
### PHPUnit Feature Tests
|
|
1085
|
+
|
|
1086
|
+
```php
|
|
1087
|
+
<?php
|
|
1088
|
+
|
|
1089
|
+
namespace Tests\Feature;
|
|
1090
|
+
|
|
1091
|
+
use App\Models\User;
|
|
1092
|
+
use App\Enums\UserRole;
|
|
1093
|
+
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
1094
|
+
use Tests\TestCase;
|
|
1095
|
+
|
|
1096
|
+
class UserApiTest extends TestCase
|
|
1097
|
+
{
|
|
1098
|
+
use RefreshDatabase;
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Test user registration.
|
|
1102
|
+
*/
|
|
1103
|
+
public function test_can_register_user(): void
|
|
1104
|
+
{
|
|
1105
|
+
$response = $this->postJson('/api/users', [
|
|
1106
|
+
'name' => 'John Doe',
|
|
1107
|
+
'email' => 'john@example.com',
|
|
1108
|
+
'password' => 'Password123!',
|
|
1109
|
+
'password_confirmation' => 'Password123!',
|
|
1110
|
+
'role' => UserRole::Subscriber->value,
|
|
1111
|
+
]);
|
|
1112
|
+
|
|
1113
|
+
$response->assertStatus(201)
|
|
1114
|
+
->assertJsonStructure([
|
|
1115
|
+
'data' => [
|
|
1116
|
+
'id',
|
|
1117
|
+
'name',
|
|
1118
|
+
'email',
|
|
1119
|
+
'role',
|
|
1120
|
+
'created_at',
|
|
1121
|
+
],
|
|
1122
|
+
]);
|
|
1123
|
+
|
|
1124
|
+
$this->assertDatabaseHas('users', [
|
|
1125
|
+
'email' => 'john@example.com',
|
|
1126
|
+
]);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* Test duplicate email validation.
|
|
1131
|
+
*/
|
|
1132
|
+
public function test_cannot_register_with_duplicate_email(): void
|
|
1133
|
+
{
|
|
1134
|
+
User::factory()->create(['email' => 'john@example.com']);
|
|
1135
|
+
|
|
1136
|
+
$response = $this->postJson('/api/users', [
|
|
1137
|
+
'name' => 'Jane Doe',
|
|
1138
|
+
'email' => 'john@example.com',
|
|
1139
|
+
'password' => 'Password123!',
|
|
1140
|
+
'password_confirmation' => 'Password123!',
|
|
1141
|
+
'role' => UserRole::Subscriber->value,
|
|
1142
|
+
]);
|
|
1143
|
+
|
|
1144
|
+
$response->assertStatus(422)
|
|
1145
|
+
->assertJsonValidationErrors(['email']);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* Test authentication required.
|
|
1150
|
+
*/
|
|
1151
|
+
public function test_authentication_required_for_user_list(): void
|
|
1152
|
+
{
|
|
1153
|
+
$response = $this->getJson('/api/users');
|
|
1154
|
+
|
|
1155
|
+
$response->assertStatus(401);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* Test authenticated user can fetch users.
|
|
1160
|
+
*/
|
|
1161
|
+
public function test_authenticated_user_can_fetch_users(): void
|
|
1162
|
+
{
|
|
1163
|
+
$user = User::factory()->create();
|
|
1164
|
+
User::factory()->count(5)->create();
|
|
1165
|
+
|
|
1166
|
+
$response = $this->actingAs($user)
|
|
1167
|
+
->getJson('/api/users');
|
|
1168
|
+
|
|
1169
|
+
$response->assertStatus(200)
|
|
1170
|
+
->assertJsonStructure([
|
|
1171
|
+
'data' => [
|
|
1172
|
+
'*' => ['id', 'name', 'email'],
|
|
1173
|
+
],
|
|
1174
|
+
'meta' => ['total', 'per_page'],
|
|
1175
|
+
]);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Test user can update own profile.
|
|
1180
|
+
*/
|
|
1181
|
+
public function test_user_can_update_own_profile(): void
|
|
1182
|
+
{
|
|
1183
|
+
$user = User::factory()->create();
|
|
1184
|
+
|
|
1185
|
+
$response = $this->actingAs($user)
|
|
1186
|
+
->patchJson("/api/users/{$user->id}", [
|
|
1187
|
+
'name' => 'Updated Name',
|
|
1188
|
+
]);
|
|
1189
|
+
|
|
1190
|
+
$response->assertStatus(200);
|
|
1191
|
+
|
|
1192
|
+
$this->assertDatabaseHas('users', [
|
|
1193
|
+
'id' => $user->id,
|
|
1194
|
+
'name' => 'Updated Name',
|
|
1195
|
+
]);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* Test user cannot update other users.
|
|
1200
|
+
*/
|
|
1201
|
+
public function test_user_cannot_update_other_users(): void
|
|
1202
|
+
{
|
|
1203
|
+
$user = User::factory()->create();
|
|
1204
|
+
$otherUser = User::factory()->create();
|
|
1205
|
+
|
|
1206
|
+
$response = $this->actingAs($user)
|
|
1207
|
+
->patchJson("/api/users/{$otherUser->id}", [
|
|
1208
|
+
'name' => 'Hacked Name',
|
|
1209
|
+
]);
|
|
1210
|
+
|
|
1211
|
+
$response->assertStatus(403);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
```
|
|
1215
|
+
|
|
1216
|
+
### Pest Tests
|
|
1217
|
+
|
|
1218
|
+
```php
|
|
1219
|
+
<?php
|
|
1220
|
+
|
|
1221
|
+
use App\Models\User;
|
|
1222
|
+
use App\Models\Post;
|
|
1223
|
+
use App\Enums\PostStatus;
|
|
1224
|
+
|
|
1225
|
+
it('can create a post', function () {
|
|
1226
|
+
$user = User::factory()->create();
|
|
1227
|
+
|
|
1228
|
+
$response = $this->actingAs($user)
|
|
1229
|
+
->postJson('/api/posts', [
|
|
1230
|
+
'title' => 'My First Post',
|
|
1231
|
+
'content' => 'This is the content of my first post.',
|
|
1232
|
+
'status' => PostStatus::Draft->value,
|
|
1233
|
+
]);
|
|
1234
|
+
|
|
1235
|
+
$response->assertStatus(201);
|
|
1236
|
+
|
|
1237
|
+
expect(Post::count())->toBe(1);
|
|
1238
|
+
expect(Post::first()->user_id)->toBe($user->id);
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
it('requires authentication to create post', function () {
|
|
1242
|
+
$response = $this->postJson('/api/posts', [
|
|
1243
|
+
'title' => 'My Post',
|
|
1244
|
+
'content' => 'Content here.',
|
|
1245
|
+
'status' => PostStatus::Draft->value,
|
|
1246
|
+
]);
|
|
1247
|
+
|
|
1248
|
+
$response->assertStatus(401);
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
it('validates post data', function () {
|
|
1252
|
+
$user = User::factory()->create();
|
|
1253
|
+
|
|
1254
|
+
$response = $this->actingAs($user)
|
|
1255
|
+
->postJson('/api/posts', [
|
|
1256
|
+
'title' => '', // Empty title
|
|
1257
|
+
'content' => 'Content',
|
|
1258
|
+
]);
|
|
1259
|
+
|
|
1260
|
+
$response->assertStatus(422)
|
|
1261
|
+
->assertJsonValidationErrors(['title']);
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
it('can publish a post', function () {
|
|
1265
|
+
$user = User::factory()->create();
|
|
1266
|
+
$post = Post::factory()->for($user)->create([
|
|
1267
|
+
'status' => PostStatus::Draft,
|
|
1268
|
+
]);
|
|
1269
|
+
|
|
1270
|
+
$response = $this->actingAs($user)
|
|
1271
|
+
->patchJson("/api/posts/{$post->id}", [
|
|
1272
|
+
'status' => PostStatus::Published->value,
|
|
1273
|
+
]);
|
|
1274
|
+
|
|
1275
|
+
$response->assertStatus(200);
|
|
1276
|
+
|
|
1277
|
+
expect($post->fresh()->status)->toBe(PostStatus::Published);
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
it('filters published posts only', function () {
|
|
1281
|
+
$user = User::factory()->create();
|
|
1282
|
+
|
|
1283
|
+
Post::factory()->for($user)->count(3)->create([
|
|
1284
|
+
'status' => PostStatus::Published,
|
|
1285
|
+
'published_at' => now()->subDay(),
|
|
1286
|
+
]);
|
|
1287
|
+
|
|
1288
|
+
Post::factory()->for($user)->count(2)->create([
|
|
1289
|
+
'status' => PostStatus::Draft,
|
|
1290
|
+
]);
|
|
1291
|
+
|
|
1292
|
+
$response = $this->getJson('/api/posts?status=published');
|
|
1293
|
+
|
|
1294
|
+
$response->assertStatus(200)
|
|
1295
|
+
->assertJsonCount(3, 'data');
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
test('post belongs to user', function () {
|
|
1299
|
+
$user = User::factory()->create();
|
|
1300
|
+
$post = Post::factory()->for($user)->create();
|
|
1301
|
+
|
|
1302
|
+
expect($post->user->id)->toBe($user->id);
|
|
1303
|
+
expect($user->posts)->toHaveCount(1);
|
|
1304
|
+
});
|
|
1305
|
+
```
|
|
1306
|
+
|
|
1307
|
+
### Unit Tests
|
|
1308
|
+
|
|
1309
|
+
```php
|
|
1310
|
+
<?php
|
|
1311
|
+
|
|
1312
|
+
namespace Tests\Unit;
|
|
1313
|
+
|
|
1314
|
+
use App\DTOs\UserDTO;
|
|
1315
|
+
use App\Enums\UserRole;
|
|
1316
|
+
use App\Enums\UserStatus;
|
|
1317
|
+
use App\Models\User;
|
|
1318
|
+
use App\Repositories\UserRepository;
|
|
1319
|
+
use App\Services\UserService;
|
|
1320
|
+
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
1321
|
+
use Tests\TestCase;
|
|
1322
|
+
|
|
1323
|
+
class UserServiceTest extends TestCase
|
|
1324
|
+
{
|
|
1325
|
+
use RefreshDatabase;
|
|
1326
|
+
|
|
1327
|
+
private UserService $userService;
|
|
1328
|
+
private UserRepository $userRepository;
|
|
1329
|
+
|
|
1330
|
+
protected function setUp(): void
|
|
1331
|
+
{
|
|
1332
|
+
parent::setUp();
|
|
1333
|
+
|
|
1334
|
+
$this->userRepository = new UserRepository();
|
|
1335
|
+
$this->userService = new UserService($this->userRepository);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
public function test_can_register_user(): void
|
|
1339
|
+
{
|
|
1340
|
+
$data = [
|
|
1341
|
+
'name' => 'John Doe',
|
|
1342
|
+
'email' => 'john@example.com',
|
|
1343
|
+
'password' => 'password123',
|
|
1344
|
+
];
|
|
1345
|
+
|
|
1346
|
+
$userDTO = $this->userService->register($data);
|
|
1347
|
+
|
|
1348
|
+
$this->assertInstanceOf(UserDTO::class, $userDTO);
|
|
1349
|
+
$this->assertEquals('John Doe', $userDTO->name);
|
|
1350
|
+
$this->assertEquals('john@example.com', $userDTO->email);
|
|
1351
|
+
$this->assertDatabaseHas('users', ['email' => 'john@example.com']);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
public function test_can_update_user_profile(): void
|
|
1355
|
+
{
|
|
1356
|
+
$user = User::factory()->create();
|
|
1357
|
+
|
|
1358
|
+
$updateData = [
|
|
1359
|
+
'name' => 'Updated Name',
|
|
1360
|
+
];
|
|
1361
|
+
|
|
1362
|
+
$userDTO = $this->userService->updateProfile($user, $updateData);
|
|
1363
|
+
|
|
1364
|
+
$this->assertEquals('Updated Name', $userDTO->name);
|
|
1365
|
+
$this->assertDatabaseHas('users', [
|
|
1366
|
+
'id' => $user->id,
|
|
1367
|
+
'name' => 'Updated Name',
|
|
1368
|
+
]);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
public function test_can_verify_email(): void
|
|
1372
|
+
{
|
|
1373
|
+
$user = User::factory()->create(['email_verified_at' => null]);
|
|
1374
|
+
|
|
1375
|
+
$this->assertNull($user->email_verified_at);
|
|
1376
|
+
|
|
1377
|
+
$this->userService->verifyEmail($user);
|
|
1378
|
+
|
|
1379
|
+
$this->assertNotNull($user->fresh()->email_verified_at);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
public function test_can_change_user_role(): void
|
|
1383
|
+
{
|
|
1384
|
+
$user = User::factory()->create(['role' => UserRole::Subscriber]);
|
|
1385
|
+
|
|
1386
|
+
$userDTO = $this->userService->changeRole($user, UserRole::Admin);
|
|
1387
|
+
|
|
1388
|
+
$this->assertEquals(UserRole::Admin, $userDTO->role);
|
|
1389
|
+
$this->assertDatabaseHas('users', [
|
|
1390
|
+
'id' => $user->id,
|
|
1391
|
+
'role' => UserRole::Admin->value,
|
|
1392
|
+
]);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
```
|
|
1396
|
+
|
|
1397
|
+
---
|
|
1398
|
+
|
|
1399
|
+
## Artisan Commands
|
|
1400
|
+
|
|
1401
|
+
```php
|
|
1402
|
+
<?php
|
|
1403
|
+
|
|
1404
|
+
namespace App\Console\Commands;
|
|
1405
|
+
|
|
1406
|
+
use App\Models\User;
|
|
1407
|
+
use App\Services\UserService;
|
|
1408
|
+
use Illuminate\Console\Command;
|
|
1409
|
+
|
|
1410
|
+
class ProcessInactiveUsersCommand extends Command
|
|
1411
|
+
{
|
|
1412
|
+
/**
|
|
1413
|
+
* The name and signature of the console command.
|
|
1414
|
+
*/
|
|
1415
|
+
protected $signature = 'users:process-inactive
|
|
1416
|
+
{--days=30 : Number of days of inactivity}
|
|
1417
|
+
{--notify : Send notification to users}
|
|
1418
|
+
{--delete : Delete inactive users}';
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* The console command description.
|
|
1422
|
+
*/
|
|
1423
|
+
protected $description = 'Process inactive users';
|
|
1424
|
+
|
|
1425
|
+
/**
|
|
1426
|
+
* Execute the console command.
|
|
1427
|
+
*/
|
|
1428
|
+
public function handle(UserService $userService): int
|
|
1429
|
+
{
|
|
1430
|
+
$days = $this->option('days');
|
|
1431
|
+
$notify = $this->option('notify');
|
|
1432
|
+
$delete = $this->option('delete');
|
|
1433
|
+
|
|
1434
|
+
$this->info("Processing users inactive for {$days} days...");
|
|
1435
|
+
|
|
1436
|
+
$inactiveUsers = User::where('last_login_at', '<', now()->subDays($days))
|
|
1437
|
+
->whereNull('deleted_at')
|
|
1438
|
+
->get();
|
|
1439
|
+
|
|
1440
|
+
$this->info("Found {$inactiveUsers->count()} inactive users.");
|
|
1441
|
+
|
|
1442
|
+
$bar = $this->output->createProgressBar($inactiveUsers->count());
|
|
1443
|
+
$bar->start();
|
|
1444
|
+
|
|
1445
|
+
foreach ($inactiveUsers as $user) {
|
|
1446
|
+
if ($notify) {
|
|
1447
|
+
// Send notification
|
|
1448
|
+
$user->notify(new InactiveUserNotification());
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
if ($delete) {
|
|
1452
|
+
$user->delete();
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
$bar->advance();
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
$bar->finish();
|
|
1459
|
+
$this->newLine();
|
|
1460
|
+
|
|
1461
|
+
$this->info('Processing complete!');
|
|
1462
|
+
|
|
1463
|
+
return Command::SUCCESS;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
```
|
|
1467
|
+
|
|
1468
|
+
---
|
|
1469
|
+
|
|
1470
|
+
## Database Migrations
|
|
1471
|
+
|
|
1472
|
+
```php
|
|
1473
|
+
<?php
|
|
1474
|
+
|
|
1475
|
+
use Illuminate\Database\Migrations\Migration;
|
|
1476
|
+
use Illuminate\Database\Schema\Blueprint;
|
|
1477
|
+
use Illuminate\Support\Facades\Schema;
|
|
1478
|
+
|
|
1479
|
+
return new class extends Migration
|
|
1480
|
+
{
|
|
1481
|
+
/**
|
|
1482
|
+
* Run the migrations.
|
|
1483
|
+
*/
|
|
1484
|
+
public function up(): void
|
|
1485
|
+
{
|
|
1486
|
+
Schema::create('users', function (Blueprint $table) {
|
|
1487
|
+
$table->id();
|
|
1488
|
+
$table->uuid('uuid')->unique();
|
|
1489
|
+
$table->string('name');
|
|
1490
|
+
$table->string('email')->unique();
|
|
1491
|
+
$table->timestamp('email_verified_at')->nullable();
|
|
1492
|
+
$table->string('password');
|
|
1493
|
+
$table->string('role')->default('subscriber');
|
|
1494
|
+
$table->string('status')->default('active');
|
|
1495
|
+
$table->rememberToken();
|
|
1496
|
+
$table->timestamps();
|
|
1497
|
+
$table->softDeletes();
|
|
1498
|
+
|
|
1499
|
+
$table->index(['email', 'status']);
|
|
1500
|
+
$table->index('created_at');
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
Schema::create('posts', function (Blueprint $table) {
|
|
1504
|
+
$table->id();
|
|
1505
|
+
$table->uuid('uuid')->unique();
|
|
1506
|
+
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
|
1507
|
+
$table->string('title');
|
|
1508
|
+
$table->string('slug')->unique();
|
|
1509
|
+
$table->text('excerpt')->nullable();
|
|
1510
|
+
$table->longText('content');
|
|
1511
|
+
$table->string('status')->default('draft');
|
|
1512
|
+
$table->timestamp('published_at')->nullable();
|
|
1513
|
+
$table->timestamps();
|
|
1514
|
+
$table->softDeletes();
|
|
1515
|
+
|
|
1516
|
+
$table->index(['user_id', 'status']);
|
|
1517
|
+
$table->index('published_at');
|
|
1518
|
+
$table->fullText(['title', 'content']);
|
|
1519
|
+
});
|
|
1520
|
+
|
|
1521
|
+
Schema::create('comments', function (Blueprint $table) {
|
|
1522
|
+
$table->id();
|
|
1523
|
+
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
|
|
1524
|
+
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
|
1525
|
+
$table->text('content');
|
|
1526
|
+
$table->boolean('is_approved')->default(false);
|
|
1527
|
+
$table->timestamps();
|
|
1528
|
+
$table->softDeletes();
|
|
1529
|
+
|
|
1530
|
+
$table->index(['post_id', 'is_approved']);
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
/**
|
|
1535
|
+
* Reverse the migrations.
|
|
1536
|
+
*/
|
|
1537
|
+
public function down(): void
|
|
1538
|
+
{
|
|
1539
|
+
Schema::dropIfExists('comments');
|
|
1540
|
+
Schema::dropIfExists('posts');
|
|
1541
|
+
Schema::dropIfExists('users');
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
```
|
|
1545
|
+
|
|
1546
|
+
---
|
|
1547
|
+
|
|
1548
|
+
## Docker Production Setup
|
|
1549
|
+
|
|
1550
|
+
```dockerfile
|
|
1551
|
+
# Dockerfile
|
|
1552
|
+
FROM php:8.3-fpm-alpine AS base
|
|
1553
|
+
|
|
1554
|
+
# Install system dependencies
|
|
1555
|
+
RUN apk add --no-cache \
|
|
1556
|
+
nginx \
|
|
1557
|
+
supervisor \
|
|
1558
|
+
postgresql-dev \
|
|
1559
|
+
zip \
|
|
1560
|
+
unzip \
|
|
1561
|
+
git \
|
|
1562
|
+
curl
|
|
1563
|
+
|
|
1564
|
+
# Install PHP extensions
|
|
1565
|
+
RUN docker-php-ext-install \
|
|
1566
|
+
pdo \
|
|
1567
|
+
pdo_pgsql \
|
|
1568
|
+
pgsql \
|
|
1569
|
+
opcache \
|
|
1570
|
+
pcntl
|
|
1571
|
+
|
|
1572
|
+
# Install Composer
|
|
1573
|
+
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
|
1574
|
+
|
|
1575
|
+
WORKDIR /var/www/html
|
|
1576
|
+
|
|
1577
|
+
# Copy composer files
|
|
1578
|
+
COPY composer.json composer.lock ./
|
|
1579
|
+
|
|
1580
|
+
# Install dependencies
|
|
1581
|
+
RUN composer install --no-dev --optimize-autoloader --no-scripts
|
|
1582
|
+
|
|
1583
|
+
# Copy application code
|
|
1584
|
+
COPY . .
|
|
1585
|
+
|
|
1586
|
+
# Set permissions
|
|
1587
|
+
RUN chown -R www-data:www-data /var/www/html \
|
|
1588
|
+
&& chmod -R 755 /var/www/html/storage
|
|
1589
|
+
|
|
1590
|
+
# Production optimizations
|
|
1591
|
+
RUN php artisan config:cache \
|
|
1592
|
+
&& php artisan route:cache \
|
|
1593
|
+
&& php artisan view:cache
|
|
1594
|
+
|
|
1595
|
+
# Expose port
|
|
1596
|
+
EXPOSE 8000
|
|
1597
|
+
|
|
1598
|
+
# Health check
|
|
1599
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
1600
|
+
CMD php artisan schedule:test || exit 1
|
|
1601
|
+
|
|
1602
|
+
CMD ["php-fpm"]
|
|
1603
|
+
```
|
|
1604
|
+
|
|
1605
|
+
---
|
|
1606
|
+
|
|
1607
|
+
Last Updated: 2026-01-10
|
|
1608
|
+
Version: 1.0.0
|