toga-ai 1.0.0
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.
- package/.claude/settings.json +119 -0
- package/.claude-plugin/marketplace.json +87 -0
- package/.claude-plugin/plugin.json +22 -0
- package/CLAUDE.md +161 -0
- package/README.md +72 -0
- package/agents/framework-pattern-checker.md +67 -0
- package/agents/harness-optimizer.md +102 -0
- package/agents/knowledge-writer.md +62 -0
- package/agents/php-build-resolver.md +51 -0
- package/agents/php-reviewer.md +51 -0
- package/agents/planner.md +88 -0
- package/agents/session-capture.md +101 -0
- package/agents/sql-reviewer.md +67 -0
- package/contexts/dev.md +43 -0
- package/contexts/research.md +49 -0
- package/contexts/review.md +37 -0
- package/knowledge/1.0/apps/library/INDEX.md +5 -0
- package/knowledge/1.0/apps/library/architecture.md +105 -0
- package/knowledge/1.0/apps/worker/INDEX.md +5 -0
- package/knowledge/1.0/apps/worker/architecture.md +223 -0
- package/knowledge/1.0/standards/backend-php.md +450 -0
- package/knowledge/2.0/apps/_underscore/INDEX.md +6 -0
- package/knowledge/2.0/apps/_underscore/architecture.md +183 -0
- package/knowledge/2.0/apps/_underscore/features/recursive-item-fulfillments.md +111 -0
- package/knowledge/2.0/apps/api2/INDEX.md +5 -0
- package/knowledge/2.0/apps/api2/architecture.md +162 -0
- package/knowledge/2.0/apps/worker2/INDEX.md +6 -0
- package/knowledge/2.0/apps/worker2/architecture.md +127 -0
- package/knowledge/2.0/apps/worker2/features/creating-worker-actions.md +135 -0
- package/knowledge/2.0/standards/backend-php.md +710 -0
- package/knowledge/CONVENTIONS.md +117 -0
- package/knowledge/INDEX.md +19 -0
- package/knowledge/clients/.gitkeep +0 -0
- package/knowledge/registry.json +7 -0
- package/knowledge.js +384 -0
- package/mcp-configs/README.md +72 -0
- package/mcp-configs/mcp-servers.json +23 -0
- package/package.json +50 -0
- package/rules/README.md +53 -0
- package/rules/common/coding-style.md +123 -0
- package/rules/common/git-workflow.md +72 -0
- package/rules/common/security.md +118 -0
- package/rules/common/testing.md +74 -0
- package/rules/php/app-framework.md +104 -0
- package/rules/php/underscore-framework.md +111 -0
- package/scripts/harness.js +605 -0
- package/scripts/hooks/evaluate-session.js +55 -0
- package/scripts/hooks/post-edit-validate.js +102 -0
- package/scripts/hooks/session-end.js +13 -0
- package/scripts/hooks/session-start.js +57 -0
- package/scripts/install.js +611 -0
- package/scripts/pre-commit +46 -0
- package/skills/capture/SKILL.md +294 -0
- package/skills/code-review/SKILL.md +140 -0
- package/skills/create-elastic-beanstalk/SKILL.md +217 -0
- package/skills/harness-audit/SKILL.md +152 -0
- package/skills/kickoff/SKILL.md +151 -0
- package/skills/php-patterns/SKILL.md +296 -0
- package/skills/session-resume/SKILL.md +156 -0
- package/skills/session-save/SKILL.md +158 -0
- package/skills/sync-team-skills/SKILL.md +87 -0
- package/sync-skills.js +71 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: harness-audit
|
|
3
|
+
description: Audits the health of the TOGA Technology team Claude knowledge repo. Runs validation, checks completeness, scores 0-100, and provides top action items. Trigger when the user says "harness-audit", "audit the knowledge base", "check the harness", "how healthy is the knowledge base", or "score the repo".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Harness Audit — check the health of the team knowledge repo
|
|
7
|
+
|
|
8
|
+
Perform a full health check of this repo and provide a scored report with concrete
|
|
9
|
+
action items. The goal is to catch drift, missing content, and integrity failures
|
|
10
|
+
before they cause problems in coding sessions.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Step 1 — Locate the team repo
|
|
15
|
+
|
|
16
|
+
Use the same resolution order as `kickoff`:
|
|
17
|
+
1. Claude memory `team-repo-path`
|
|
18
|
+
2. Env var `CLAUDE_TEAM_REPO`
|
|
19
|
+
3. Auto-discover (`./claude`, `../claude`, `../../claude`, walk up for `knowledge/INDEX.md`)
|
|
20
|
+
4. Ask
|
|
21
|
+
|
|
22
|
+
From here, all commands use `node "<TEAM_REPO>/knowledge.js" …` and
|
|
23
|
+
`node "<TEAM_REPO>/scripts/harness.js" …`.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Step 2 — Run automated checks
|
|
28
|
+
|
|
29
|
+
### Check 1: Validation
|
|
30
|
+
```sh
|
|
31
|
+
node "<TEAM_REPO>/knowledge.js" validate
|
|
32
|
+
```
|
|
33
|
+
- **PASS:** output contains "validate: OK" and exit code 0
|
|
34
|
+
- **FAIL:** any `ERROR:` lines in output or non-zero exit code
|
|
35
|
+
- **Report:** count of errors, list first 5 error lines
|
|
36
|
+
|
|
37
|
+
### Check 2: Harness status
|
|
38
|
+
```sh
|
|
39
|
+
node "<TEAM_REPO>/scripts/harness.js" status
|
|
40
|
+
```
|
|
41
|
+
Collect and display:
|
|
42
|
+
- Number of registered repos
|
|
43
|
+
- Doc counts per framework (broken down by type)
|
|
44
|
+
- Client count and doc counts
|
|
45
|
+
- Last git commit date on `knowledge/`
|
|
46
|
+
|
|
47
|
+
### Check 3: Architecture coverage
|
|
48
|
+
For every repo in `registry.json`, check that
|
|
49
|
+
`knowledge/<framework>/apps/<repo>/architecture.md` exists.
|
|
50
|
+
- **PASS:** all repos have an architecture.md
|
|
51
|
+
- **FAIL:** list the specific missing paths
|
|
52
|
+
|
|
53
|
+
### Check 4: INDEX.md integrity
|
|
54
|
+
Check `knowledge/INDEX.md` for the "Auto-generated" notice. Check each
|
|
55
|
+
`<fw>/apps/<repo>/INDEX.md` for the `| Doc |` table header (generated by `knowledge.js index`).
|
|
56
|
+
- **PASS:** all INDEX files appear auto-generated
|
|
57
|
+
- **FAIL:** list any INDEX.md files that look hand-edited (missing table structure)
|
|
58
|
+
|
|
59
|
+
Additionally, run `git -C "<TEAM_REPO>" log --oneline -- "*/INDEX.md"` and check
|
|
60
|
+
whether any INDEX.md commits were made by a human (non-`knowledge.js` author). Flag
|
|
61
|
+
these as warnings.
|
|
62
|
+
|
|
63
|
+
### Check 5: clients/ directory
|
|
64
|
+
Check that `knowledge/clients/` exists.
|
|
65
|
+
- **PASS:** directory exists (even if empty)
|
|
66
|
+
- **FAIL:** directory missing — client knowledge has not been started
|
|
67
|
+
|
|
68
|
+
### Check 6: 1.0/standards/ coverage
|
|
69
|
+
Check `knowledge/1.0/standards/` for at least one `.md` file.
|
|
70
|
+
- **PASS:** at least one standard exists
|
|
71
|
+
- **FAIL:** directory empty or missing — 1.0 has no documented standards
|
|
72
|
+
|
|
73
|
+
### Check 7: 2.0/standards/ coverage
|
|
74
|
+
Check `knowledge/2.0/standards/` for at least one `.md` file.
|
|
75
|
+
- **PASS:** at least one standard exists
|
|
76
|
+
- **FAIL:** directory empty or missing — 2.0 has no documented standards
|
|
77
|
+
|
|
78
|
+
### Check 8: Frontmatter completeness
|
|
79
|
+
For every doc in `knowledge/` (excluding INDEX.md files), verify presence of
|
|
80
|
+
required frontmatter: `title`, `framework`, `type`, `status`, `project`.
|
|
81
|
+
- **PASS:** all docs have all required fields
|
|
82
|
+
- **FAIL:** list docs with missing fields (up to 5; note count if more)
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Step 3 — Score and report
|
|
87
|
+
|
|
88
|
+
Each check is worth equal weight (100 / 8 = 12.5 points each, rounded).
|
|
89
|
+
Partial credit is not awarded — a check either passes or fails.
|
|
90
|
+
|
|
91
|
+
Present results in this format:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
95
|
+
Harness Audit — TOGA Technology Claude Knowledge Repo
|
|
96
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
97
|
+
|
|
98
|
+
✓ [PASS] knowledge.js validate
|
|
99
|
+
✓ [PASS] registry.json has entries (5 repos)
|
|
100
|
+
✗ [FAIL] all repos have architecture.md
|
|
101
|
+
→ Missing: 1.0/apps/library/architecture.md
|
|
102
|
+
✓ [PASS] INDEX.md files appear auto-generated
|
|
103
|
+
✓ [PASS] clients/ directory exists
|
|
104
|
+
✓ [PASS] 1.0/standards/ has standards (1 file)
|
|
105
|
+
✓ [PASS] 2.0/standards/ has standards (1 file)
|
|
106
|
+
✗ [FAIL] all docs have required frontmatter
|
|
107
|
+
→ 2.0/apps/worker2/features/clickup.md: missing "project"
|
|
108
|
+
|
|
109
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
110
|
+
Score: 75/100 (6/8 checks passed)
|
|
111
|
+
|
|
112
|
+
Top 3 action items:
|
|
113
|
+
1. Create knowledge/1.0/apps/library/architecture.md
|
|
114
|
+
Run /capture after any Library session to generate this.
|
|
115
|
+
2. Fix frontmatter in 2.0/apps/worker2/features/clickup.md: add "project: Worker"
|
|
116
|
+
Then run: node knowledge.js validate
|
|
117
|
+
3. [next failing check...]
|
|
118
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Step 4 — Action item guidance
|
|
124
|
+
|
|
125
|
+
For each failing check, provide a specific actionable fix (not a vague suggestion):
|
|
126
|
+
|
|
127
|
+
| Check | Specific fix |
|
|
128
|
+
|-------|-------------|
|
|
129
|
+
| validate fails | Run `node knowledge.js validate`, read each `ERROR:` line, fix the listed file/frontmatter |
|
|
130
|
+
| missing architecture.md | Run `/capture` after the next session on that repo — architecture docs are created then |
|
|
131
|
+
| hand-edited INDEX.md | Run `node knowledge.js index` to regenerate — do NOT re-hand-edit |
|
|
132
|
+
| clients/ missing | Run `mkdir knowledge/clients && touch knowledge/clients/.gitkeep` |
|
|
133
|
+
| standards/ empty | During the next session on that framework, include a standards update in `/capture` |
|
|
134
|
+
| missing frontmatter | Open the file, add the missing field per `knowledge/CONVENTIONS.md` template, re-run validate |
|
|
135
|
+
|
|
136
|
+
After listing action items, offer: "Would you like me to fix any of these now?"
|
|
137
|
+
If the developer says yes, execute the fixes one at a time and re-run `validate` after
|
|
138
|
+
each write.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Notes
|
|
143
|
+
|
|
144
|
+
- A score of 100 means the knowledge base is structurally complete and valid.
|
|
145
|
+
It does NOT mean the content is high quality — quality is a human judgment.
|
|
146
|
+
- A score below 60 means the knowledge base has significant structural problems
|
|
147
|
+
that will cause `kickoff` or `capture` to produce unreliable results.
|
|
148
|
+
- Run `/harness-audit` before any major PR to the `claude` repo to confirm nothing
|
|
149
|
+
was inadvertently broken.
|
|
150
|
+
- The `PostToolUse` hook in `.claude/settings.json` auto-validates any write to
|
|
151
|
+
`knowledge/` files, so most integrity errors are caught at the moment of writing.
|
|
152
|
+
This audit catches structural and coverage gaps that validate doesn't check.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kickoff
|
|
3
|
+
description: Start-of-session context loader for TOGA Technology projects. Run this at the BEGINNING of a coding session to prime Claude with the right knowledge before writing any code. It asks what you're working on (framework 1.0/2.0/both, front/back/hybrid, which repo/project, which client) and loads the matching coding standards, framework-core architecture, the repo's knowledge, and relevant client knowledge from the team `claude` knowledge base. Trigger whenever the user says "kickoff", "start a session", "prime me", "I'm starting work on X", or "load context for repo/client".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Kickoff — prime a coding session from the team knowledge base
|
|
7
|
+
|
|
8
|
+
Load the right knowledge before the developer starts coding. **Never assume missing
|
|
9
|
+
facts — ask.** Heuristics may pre-fill a *suggested default* in a question, but record
|
|
10
|
+
nothing without confirmation.
|
|
11
|
+
|
|
12
|
+
## Core data model (this is the structure you maintain — same as `knowledge/CONVENTIONS.md`)
|
|
13
|
+
|
|
14
|
+
- The team knowledge lives in the **`claude` repo** under `knowledge/`.
|
|
15
|
+
- **Framework partitions everything:** `1.0/` (the `App_` framework, core repo `library`)
|
|
16
|
+
and `2.0/` (the `_underscore` framework, core repo `_underscore`).
|
|
17
|
+
- Within a framework: `apps/<repo>/architecture.md` + `apps/<repo>/features/*.md`, and
|
|
18
|
+
`standards/*.md`. App folders are keyed by **repo name** (e.g. `worker2`), not project name.
|
|
19
|
+
- Client-specific knowledge is shared at the top level: `clients/<client>/{profile.md,
|
|
20
|
+
features/, workflows/}`, each doc tagging its `framework`.
|
|
21
|
+
- `knowledge/registry.json` maps **repo ↔ project ↔ framework ↔ role(core|app) ↔ dependsOn**.
|
|
22
|
+
- Every repo implicitly depends on its framework's `core` repo; `dependsOn` adds more.
|
|
23
|
+
|
|
24
|
+
## Step 1 — Resolve the team-repo (`claude`) path
|
|
25
|
+
|
|
26
|
+
You need this to read the registry and knowledge. Resolve in order; **persist the result
|
|
27
|
+
to Claude memory** so you never ask twice:
|
|
28
|
+
|
|
29
|
+
1. **Claude memory** — a `reference` memory named `team-repo-path` (recalled memories
|
|
30
|
+
appear in your context). Use it if present and it contains `knowledge/INDEX.md`.
|
|
31
|
+
2. **Env var** `CLAUDE_TEAM_REPO`.
|
|
32
|
+
3. **Auto-discover** — probe `./claude`, `../claude`, `../../claude`, and walk up the tree
|
|
33
|
+
for a directory containing `knowledge/INDEX.md`.
|
|
34
|
+
4. **Ask** the developer: "Where is the team `claude` repo checked out on this machine?"
|
|
35
|
+
|
|
36
|
+
When resolved, if there was no `team-repo-path` memory, **write one** (memory type
|
|
37
|
+
`reference`) and add its `MEMORY.md` pointer. From here on call the helper as
|
|
38
|
+
`node "<TEAM_REPO>/knowledge.js" <command>`.
|
|
39
|
+
|
|
40
|
+
## Step 2 — Interview the developer (ask the work questions FIRST)
|
|
41
|
+
|
|
42
|
+
Ask these in one message. **Present the repos/projects you already know** (read
|
|
43
|
+
`registry.json`) so the developer can just pick:
|
|
44
|
+
|
|
45
|
+
1. **Framework** — 1.0, 2.0, or **both**?
|
|
46
|
+
2. **Layer** — front-end, back-end, or hybrid?
|
|
47
|
+
3. **Repo / project** — list the registered repos for the chosen framework(s) as options
|
|
48
|
+
(show `repo` and `project`), plus **"a new repo not listed"**. If they pick the new-repo
|
|
49
|
+
option, run **New-repo onboarding** (below) before continuing.
|
|
50
|
+
4. **Client** — which client is this for, or "shared / internal"? (List `clients/` folders
|
|
51
|
+
you know of, plus "a new client".)
|
|
52
|
+
5. **What are you building or changing?** (a sentence — used to pick relevant feature docs.)
|
|
53
|
+
|
|
54
|
+
If any answer is unclear, ask again. Do not guess the framework, repo, project, or client.
|
|
55
|
+
|
|
56
|
+
## Step 3 — Resolve the repos this work needs (local paths, lazily)
|
|
57
|
+
|
|
58
|
+
Compute the load set with `node "<TEAM_REPO>/knowledge.js" deps --repo=<chosen-repo>`
|
|
59
|
+
(returns the framework core + chosen repo + transitive `dependsOn`, in load order). For
|
|
60
|
+
**both** frameworks, run it for each chosen repo and union the results.
|
|
61
|
+
|
|
62
|
+
For **each** repo in the load set, get its local path so you can navigate the actual code:
|
|
63
|
+
- Look for a Claude `reference` memory named `repo-path-<repo>` (e.g. `repo-path-worker2`).
|
|
64
|
+
- If absent, **ask**: "What is the path to the `<repo>` repository?" Validate it exists,
|
|
65
|
+
then **write a `repo-path-<repo>` memory** (+ MEMORY.md pointer).
|
|
66
|
+
|
|
67
|
+
Never ask for a repo the session doesn't touch.
|
|
68
|
+
|
|
69
|
+
## Step 4 — Load the knowledge
|
|
70
|
+
|
|
71
|
+
Read and internalize, in this order:
|
|
72
|
+
1. **Framework core architecture** — `<TEAM_REPO>/knowledge/<fw>/apps/<core-repo>/architecture.md`
|
|
73
|
+
(e.g. `2.0/apps/_underscore/architecture.md`). Always load this first.
|
|
74
|
+
2. **The chosen repo's** `architecture.md`, then feature docs matched to the description:
|
|
75
|
+
`node "<TEAM_REPO>/knowledge.js" search --framework=<fw> --repo=<repo> --q=<keywords>`.
|
|
76
|
+
3. **Coding standards** for the framework(s): `<fw>/standards/backend-php.md` and/or
|
|
77
|
+
`frontend.md` per the layer answer (plus any others that exist). Load whichever exist.
|
|
78
|
+
4. **Client knowledge** (if a real client): `clients/<client>/profile.md` and that client's
|
|
79
|
+
`features/`/`workflows/` filtered to the selected framework(s)
|
|
80
|
+
(`search --client=<client> --framework=<fw>`).
|
|
81
|
+
|
|
82
|
+
For **both** frameworks, union across `1.0/` and `2.0/`.
|
|
83
|
+
|
|
84
|
+
**Do NOT create or modify any `CLAUDE.md` stub** — kickoff only reads. A blank session is a
|
|
85
|
+
valid choice; this skill is opt-in.
|
|
86
|
+
|
|
87
|
+
## Step 5 — Summarize and confirm
|
|
88
|
+
|
|
89
|
+
Tell the developer concisely:
|
|
90
|
+
- Which framework(s), repo(s), and client are in scope, and the local path of each repo.
|
|
91
|
+
- Which knowledge docs you loaded (by title). Where a repo/standard has **no knowledge yet**,
|
|
92
|
+
say so explicitly: "No knowledge captured yet for X — `capture` will build it as you work."
|
|
93
|
+
- Confirm you're primed and ready for their first task.
|
|
94
|
+
|
|
95
|
+
## New-repo onboarding (when the developer names a repo not in `registry.json`)
|
|
96
|
+
|
|
97
|
+
Ask **all** of the following — assume nothing (you may offer a suggested default, e.g.
|
|
98
|
+
framework based on whether the path is under a 1.0 or 2.0 tree, but require confirmation):
|
|
99
|
+
|
|
100
|
+
1. **Repo name** — the exact on-disk folder/repo name.
|
|
101
|
+
2. **Framework** — 1.0 or 2.0.
|
|
102
|
+
3. **Project name** — the logical name.
|
|
103
|
+
4. **Role** — framework **core** or regular **app**?
|
|
104
|
+
5. **dependsOn** — any other repos it depends on beyond the framework core? (default none)
|
|
105
|
+
6. **Local path** on this machine (if not already remembered).
|
|
106
|
+
|
|
107
|
+
Then: append the entry to `<TEAM_REPO>/knowledge/registry.json`, write a `repo-path-<repo>`
|
|
108
|
+
memory, run `node "<TEAM_REPO>/knowledge.js" validate` (fix any error before continuing),
|
|
109
|
+
and proceed. The `<fw>/apps/<repo>/` folder is created later by `capture` when the first
|
|
110
|
+
doc is written — an empty knowledge set is fine; just note it.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
### Claude Code Hooks Integration
|
|
115
|
+
|
|
116
|
+
This repo has a `PostToolUse` hook configured in `.claude/settings.json` that
|
|
117
|
+
**automatically runs `node knowledge.js validate`** whenever any knowledge file is
|
|
118
|
+
written or edited. This means:
|
|
119
|
+
|
|
120
|
+
- You do NOT need to run `validate` manually after a `capture` session write — you
|
|
121
|
+
will see validation results inline in the terminal as Claude writes each file.
|
|
122
|
+
- If an error appears in the hook output (lines starting with `ERROR:`), the capture
|
|
123
|
+
step that caused it should be corrected before continuing.
|
|
124
|
+
- The hook output is condensed: it shows only `ERROR`, `WARNING`, `OK`/`FAILED`
|
|
125
|
+
lines, so it does not flood the terminal.
|
|
126
|
+
|
|
127
|
+
This hook is active whenever you have this repo open in Claude Code. It does not
|
|
128
|
+
run in other project repos (it is scoped to the `claude` repo's `.claude/settings.json`).
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### Before kickoff: should you run /session-resume first?
|
|
133
|
+
|
|
134
|
+
If the developer says any of the following, suggest they run `/session-resume` **before**
|
|
135
|
+
this kickoff to reload their prior session context:
|
|
136
|
+
|
|
137
|
+
- "I'm continuing from yesterday"
|
|
138
|
+
- "I'm picking up where I left off"
|
|
139
|
+
- "I was in the middle of something"
|
|
140
|
+
- "I saved my session last time"
|
|
141
|
+
- Any mention of a prior unfinished feature or task
|
|
142
|
+
|
|
143
|
+
In that case, say: "It sounds like you're continuing prior work. If you saved your
|
|
144
|
+
session with `/session-save`, run `/session-resume latest` first — it will reload
|
|
145
|
+
your exact state (what worked, what failed, and your next step) before we prime
|
|
146
|
+
framework context with kickoff. This prevents you from re-trying approaches that
|
|
147
|
+
already failed."
|
|
148
|
+
|
|
149
|
+
If they have already run `/session-resume` and are now running `/kickoff`, proceed
|
|
150
|
+
normally — the session context they loaded will complement the framework knowledge
|
|
151
|
+
loaded here.
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: php-patterns
|
|
3
|
+
description: PHP pattern reference for TOGA Technology projects covering both frameworks. Framework 1.0 (App_ prefix), Framework 2.0 (_underscore prefix), PDO prepared statements, session handling, caching, api2 envelope format. Trigger when the user says "php-patterns", "show me the pattern for X", "how do I do X in 1.0/2.0", or starts writing new PHP code.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# PHP Patterns Skill
|
|
7
|
+
|
|
8
|
+
Reference skill for TOGA Technology PHP patterns across both frameworks. Load this skill when writing new PHP code, reviewing patterns, or onboarding to a repo.
|
|
9
|
+
|
|
10
|
+
## When to invoke
|
|
11
|
+
|
|
12
|
+
- `/php-patterns` — general reference for both frameworks
|
|
13
|
+
- `/php-patterns 1.0` — Framework 1.0 (App_) specific patterns
|
|
14
|
+
- `/php-patterns 2.0` — Framework 2.0 (_underscore) specific patterns
|
|
15
|
+
- `/php-patterns pdo` — PDO/database patterns
|
|
16
|
+
- `/php-patterns queue` — queue/worker dispatch patterns
|
|
17
|
+
- `/php-patterns api` — API response patterns
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Framework 1.0 (App_) Patterns
|
|
22
|
+
|
|
23
|
+
### Controller structure
|
|
24
|
+
|
|
25
|
+
```php
|
|
26
|
+
<?php
|
|
27
|
+
|
|
28
|
+
class App_Controller_Orders extends App_Controller {
|
|
29
|
+
|
|
30
|
+
public function indexAction(): void {
|
|
31
|
+
$model = App_Registry::get('Order');
|
|
32
|
+
$orders = $model->getAll(['user_id' => $this->auth->getUserId()]);
|
|
33
|
+
$this->view->assign('orders', $orders);
|
|
34
|
+
$this->view->render('orders/index');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public function createAction(): void {
|
|
38
|
+
if (!$this->request->isPost()) {
|
|
39
|
+
$this->response->error('Method not allowed', 405);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
$params = $this->request->post(['user_id', 'items', 'address_id']);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
$order = App_Registry::get('Order')->create($params);
|
|
47
|
+
} catch (App_Exception_Validation $e) {
|
|
48
|
+
$this->response->error($e->getMessage(), 400);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
$this->response->success(['order_id' => $order->id]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Key points:
|
|
58
|
+
- Get models via `App_Registry::get('ModelName')` — never `new App_Model_*`
|
|
59
|
+
- Filter request input explicitly: `$this->request->post(['key1', 'key2'])`
|
|
60
|
+
- Return early on errors; happy path is last
|
|
61
|
+
- Catch specific exception types, not bare `Exception`
|
|
62
|
+
|
|
63
|
+
### Model structure
|
|
64
|
+
|
|
65
|
+
```php
|
|
66
|
+
<?php
|
|
67
|
+
|
|
68
|
+
class App_Model_Order extends App_Model {
|
|
69
|
+
|
|
70
|
+
protected string $table = 'orders';
|
|
71
|
+
|
|
72
|
+
public function getByUser(int $userId): array {
|
|
73
|
+
if ($userId <= 0) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
return $this->db->select($this->table, ['user_id' => $userId]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public function create(array $params): object {
|
|
80
|
+
$this->validate($params); // throws App_Exception_Validation on failure
|
|
81
|
+
$id = $this->db->insert($this->table, [
|
|
82
|
+
'user_id' => (int) $params['user_id'],
|
|
83
|
+
'address_id' => (int) $params['address_id'],
|
|
84
|
+
'status' => 'pending',
|
|
85
|
+
'created_at' => date('Y-m-d H:i:s'),
|
|
86
|
+
]);
|
|
87
|
+
return $this->db->find($this->table, $id);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Cron/scheduled task pattern (1.0)
|
|
93
|
+
|
|
94
|
+
```php
|
|
95
|
+
<?php
|
|
96
|
+
// crons/ProcessPendingOrders.php
|
|
97
|
+
|
|
98
|
+
class App_Cron_ProcessPendingOrders extends App_Cron {
|
|
99
|
+
|
|
100
|
+
public function run(): void {
|
|
101
|
+
$model = App_Registry::get('Order');
|
|
102
|
+
$pending = $model->getPending(100); // always LIMIT — never unbounded
|
|
103
|
+
|
|
104
|
+
foreach ($pending as $order) {
|
|
105
|
+
try {
|
|
106
|
+
$model->process($order->id);
|
|
107
|
+
} catch (App_Exception_Processing $e) {
|
|
108
|
+
error_log('Cron: failed to process order ' . $order->id . ': ' . $e->getMessage());
|
|
109
|
+
// continue to next — do not let one failure abort the batch
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Framework 2.0 (_underscore) Patterns
|
|
119
|
+
|
|
120
|
+
### Worker structure
|
|
121
|
+
|
|
122
|
+
```php
|
|
123
|
+
<?php
|
|
124
|
+
|
|
125
|
+
class _Worker_ProcessOrder extends _Worker {
|
|
126
|
+
|
|
127
|
+
public function run(): void {
|
|
128
|
+
$orderId = (int) ($this->payload['order_id'] ?? 0);
|
|
129
|
+
|
|
130
|
+
if ($orderId <= 0) {
|
|
131
|
+
error_log('_Worker_ProcessOrder: missing order_id in payload');
|
|
132
|
+
return; // discard — bad payload, no retry
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
_Model_Order::process($orderId);
|
|
137
|
+
} catch (_Exception_OrderNotFound $e) {
|
|
138
|
+
error_log('_Worker_ProcessOrder: order not found: ' . $orderId);
|
|
139
|
+
return; // discard
|
|
140
|
+
} catch (_Exception_Database $e) {
|
|
141
|
+
error_log('_Worker_ProcessOrder: DB error: ' . $e->getMessage());
|
|
142
|
+
throw $e; // re-throw — queue will retry
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Worker dispatch
|
|
149
|
+
|
|
150
|
+
```php
|
|
151
|
+
// Dispatch a single job
|
|
152
|
+
_Queue::dispatch('_Worker_ProcessOrder', ['order_id' => $orderId]);
|
|
153
|
+
|
|
154
|
+
// Dispatch with delay (seconds)
|
|
155
|
+
_Queue::dispatch('_Worker_ProcessOrder', ['order_id' => $orderId], 30);
|
|
156
|
+
|
|
157
|
+
// Dispatch to a specific queue name
|
|
158
|
+
_Queue::dispatch('_Worker_ProcessOrder', $payload, 0, 'high-priority');
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Never call `$worker->run()` or `new _Worker_ProcessOrder()` directly in application code.
|
|
162
|
+
|
|
163
|
+
### API controller structure (api2)
|
|
164
|
+
|
|
165
|
+
```php
|
|
166
|
+
<?php
|
|
167
|
+
|
|
168
|
+
class _Controller_Orders extends _Controller {
|
|
169
|
+
|
|
170
|
+
public function getAction(): array {
|
|
171
|
+
$orderId = (int) ($this->request->get('id') ?? 0);
|
|
172
|
+
|
|
173
|
+
if ($orderId <= 0) {
|
|
174
|
+
return ['success' => false, 'data' => null, 'errors' => ['Invalid order ID']];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
$order = _Model_Order::find($orderId);
|
|
178
|
+
|
|
179
|
+
if ($order === null) {
|
|
180
|
+
return ['success' => false, 'data' => null, 'errors' => ['Order not found']];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return ['success' => true, 'data' => $order->toArray(), 'errors' => []];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
public function createAction(): array {
|
|
187
|
+
$params = $this->request->post(['customer_id', 'items']);
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
$order = _Model_Order::create($params);
|
|
191
|
+
} catch (_Exception_Validation $e) {
|
|
192
|
+
return ['success' => false, 'data' => null, 'errors' => $e->getErrors()];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return ['success' => true, 'data' => ['order_id' => $order->id], 'errors' => []];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Cross-Framework Patterns
|
|
203
|
+
|
|
204
|
+
### PDO prepared statements
|
|
205
|
+
|
|
206
|
+
```php
|
|
207
|
+
// SELECT with parameters
|
|
208
|
+
$stmt = $pdo->prepare('SELECT * FROM orders WHERE user_id = ? AND status = ?');
|
|
209
|
+
$stmt->execute([$userId, $status]);
|
|
210
|
+
$orders = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
211
|
+
|
|
212
|
+
// INSERT
|
|
213
|
+
$stmt = $pdo->prepare(
|
|
214
|
+
'INSERT INTO orders (user_id, status, created_at) VALUES (?, ?, ?)'
|
|
215
|
+
);
|
|
216
|
+
$stmt->execute([$userId, 'pending', date('Y-m-d H:i:s')]);
|
|
217
|
+
$newId = $pdo->lastInsertId();
|
|
218
|
+
|
|
219
|
+
// UPDATE with WHERE (always include WHERE — never unbounded UPDATE)
|
|
220
|
+
$stmt = $pdo->prepare('UPDATE orders SET status = ? WHERE id = ?');
|
|
221
|
+
$stmt->execute([$newStatus, $orderId]);
|
|
222
|
+
|
|
223
|
+
// IN clause with variable number of params
|
|
224
|
+
$ids = [1, 2, 3, 4];
|
|
225
|
+
$placeholders = implode(',', array_fill(0, count($ids), '?'));
|
|
226
|
+
$stmt = $pdo->prepare("SELECT * FROM orders WHERE id IN ($placeholders)");
|
|
227
|
+
$stmt->execute($ids);
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Session handling
|
|
231
|
+
|
|
232
|
+
```php
|
|
233
|
+
// Start session safely
|
|
234
|
+
if (session_status() === PHP_SESSION_NONE) {
|
|
235
|
+
session_start();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Regenerate after login (prevents session fixation)
|
|
239
|
+
session_regenerate_id(true);
|
|
240
|
+
|
|
241
|
+
// Store only IDs in session — never full objects
|
|
242
|
+
$_SESSION['user_id'] = $user->id;
|
|
243
|
+
|
|
244
|
+
// Access session value safely
|
|
245
|
+
$userId = (int) ($_SESSION['user_id'] ?? 0);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Caching pattern
|
|
249
|
+
|
|
250
|
+
```php
|
|
251
|
+
// Check cache before hitting DB
|
|
252
|
+
$cacheKey = 'user_profile_' . $userId;
|
|
253
|
+
$profile = _Cache::get($cacheKey);
|
|
254
|
+
|
|
255
|
+
if ($profile === null) {
|
|
256
|
+
$profile = _Model_User::getProfile($userId);
|
|
257
|
+
_Cache::set($cacheKey, $profile, 300); // TTL in seconds
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Invalidate on write
|
|
261
|
+
_Model_User::updateProfile($userId, $data);
|
|
262
|
+
_Cache::delete('user_profile_' . $userId);
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Logging pattern
|
|
266
|
+
|
|
267
|
+
```php
|
|
268
|
+
// Log with context — never log sensitive fields
|
|
269
|
+
error_log(sprintf(
|
|
270
|
+
'[%s] Order %d processing failed: %s',
|
|
271
|
+
date('Y-m-d H:i:s'),
|
|
272
|
+
$orderId,
|
|
273
|
+
$e->getMessage()
|
|
274
|
+
));
|
|
275
|
+
|
|
276
|
+
// Use log levels if the framework provides them
|
|
277
|
+
_Log::error('Order processing failed', ['order_id' => $orderId, 'error' => $e->getMessage()]);
|
|
278
|
+
// Do NOT include: passwords, tokens, card numbers, PII
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Input validation helper
|
|
282
|
+
|
|
283
|
+
```php
|
|
284
|
+
// Validate and sanitize common input types
|
|
285
|
+
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 0;
|
|
286
|
+
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL) ?: '';
|
|
287
|
+
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT,
|
|
288
|
+
['options' => ['min_range' => 1, 'max_range' => 1000],
|
|
289
|
+
'flags' => FILTER_NULL_ON_FAILURE]) ?? 1;
|
|
290
|
+
|
|
291
|
+
// Allowlist for enum-like inputs
|
|
292
|
+
$allowed_statuses = ['pending', 'processing', 'complete', 'cancelled'];
|
|
293
|
+
$status = in_array($_GET['status'] ?? '', $allowed_statuses, true)
|
|
294
|
+
? $_GET['status']
|
|
295
|
+
: 'pending';
|
|
296
|
+
```
|