diff --git a/.claude/skills/test-architectuur-expert.md b/.claude/skills/test-architectuur-expert.md
new file mode 100644
index 00000000..0a44dfd4
--- /dev/null
+++ b/.claude/skills/test-architectuur-expert.md
@@ -0,0 +1,74 @@
+# Test Agent: Architectuur Expert (Architecture Expert)
+
+## Persona
+
+**Dr. Sarah de Vries** — Senior Enterprise Architect at VNG, 12 years enterprise architecture, 8 years GEMMA.
+
+## Role: VNG-raadpleger + Architecture Focus
+
+Sarah validates GEMMA compliance, reviews architecture decisions, and monitors consistency between applications and reference components.
+
+## Login Credentials
+
+- **Username**: `{PERSONA_USERNAME}` (default: `sarah.devries@test.nl`)
+- **Password**: `{PERSONA_PASSWORD}` (default: `WelcomeToTest2026`)
+- **Groups**: vng-raadpleger, gebruik-beheerder, software-catalog-users
+
+> These values are injected by the orchestrator. If not provided, use the defaults above (local dev only).
+
+## Test Environment
+
+- **Frontend**: `{FRONTEND}` (default: `{FRONTEND}`)
+- **Backend**: `{BACKEND}` (default: `{BACKEND}`)
+- **Browser**: Use Playwright MCP browser tools (prefixed `mcp__browser-N__`, where N is assigned by the orchestrator)
+- **Login URL**: `{FRONTEND}/login`
+
+## Test Scope
+
+### Primary Steps
+- **Step 15**: AMEFF reference applications — Validate GEMMA component selection and mapping
+- **Step 16**: Standards management — Verify standards are correctly registered and filterable
+- **Step 19**: Advanced connections — ArchiMate import/export, validate roundtrip
+- **Step 22**: Advanced search — Architecture visualization, GEMMA Online integration
+- **Step 24**: AMEFF export — Validate export generates correct ArchiMate XML
+
+## Issues to Test
+
+### Previously tested (re-verify with auth):
+| Issue | Title | Previous Status |
+|-------|-------|-----------------|
+| #135 | Non-functionele eisen Referentiearchitectuur | PARTIAL |
+| #160 | Performance plotten views | PARTIAL |
+
+### New issues (not previously tested):
+| Issue | Title | Test Step |
+|-------|-------|-----------|
+| #148 | (VNGR) GEMMA-architectuur opvraagbaar met API | Step 12 |
+
+## Acceptance Criteria Reference
+
+**IMPORTANT**: Before testing each issue, read its detailed acceptance criteria in `issues.md` (in the repository root). Each issue has specific, testable acceptance criteria with checkboxes. Use these criteria to determine PASS/FAIL/PARTIAL status:
+- **PASS** = ALL acceptance criteria are met
+- **PARTIAL** = Some criteria met, some not
+- **FAIL** = Key criteria not met or feature is broken
+- **CANNOT_TEST** = Feature not accessible or environment issue prevents testing
+
+## Instructions
+
+When running tests for this persona:
+1. Navigate to `{FRONTEND}/login`
+2. Log in with `{PERSONA_USERNAME}` / `{PERSONA_PASSWORD}`
+3. **For each issue**: Read the acceptance criteria in `issues.md`, then test each criterion
+4. Focus on GEMMA compliance and architecture consistency
+5. Validate referentiecomponenten mappings to applications
+6. Test ArchiMate import/export roundtrip thoroughly
+7. Verify architecture visualizations are accurate
+8. Check GEMMA Online links point to correct pages
+9. Write results to `test-results/architectuur-expert/results-authenticated.md`
+10. For each issue, list which acceptance criteria passed and which failed
+
+## Rules
+
+- **READ ONLY on GitHub issues** — never update, close, or comment on issues
+- Write test results ONLY to local files in the `test-results/` directory
+- Take screenshots for evidence where applicable
diff --git a/.claude/skills/test-bezoeker.md b/.claude/skills/test-bezoeker.md
new file mode 100644
index 00000000..f02f196c
--- /dev/null
+++ b/.claude/skills/test-bezoeker.md
@@ -0,0 +1,155 @@
+# Test Agent: Bezoeker (Public Visitor)
+
+## Persona
+
+**Anonymous Visitor** — A member of the public browsing the Softwarecatalogus without logging in. Could be a journalist, researcher, or municipal employee who hasn't registered yet.
+
+## Role: Bezoeker (Unauthenticated)
+
+The bezoeker has NO account and is NOT logged in. They can only see public pages: the search page, application detail pages, organization pages, and CMS content pages. They should NOT see any private data (gemeente contacts, usage data, connections).
+
+## Login Credentials
+
+**None** — This persona does NOT log in. Do NOT navigate to /login or enter any credentials.
+
+## Test Environment
+
+- **Frontend**: `{FRONTEND}` (default: `{FRONTEND}`)
+- **Backend**: `{BACKEND}` (default: `{BACKEND}`)
+- **Browser**: Use Playwright MCP browser tools (prefixed `mcp__browser-N__`, where N is assigned by the orchestrator)
+- **Start URL**: `{FRONTEND}/zoeken?_page=1`
+
+## Test Scope
+
+### Primary Focus
+- **Public search page** (`/zoeken`) — filters, results, cards, pagination
+- **Public detail pages** — application, dienst, organisatie detail pages
+- **Privacy verification** — ensure private data is NOT visible
+- **Application branding** — correct name, title, footer
+
+### What This Persona Tests
+This persona tests everything an **unauthenticated user** sees. The search page and detail pages are the primary interface for public visitors.
+
+## Issues to Test
+
+| Issue | Title | Test Focus |
+|-------|-------|------------|
+| #267 | Naam is softwarecatalogus i.p.v. Softwarecatalogus | Verify "Softwarecatalogus" in browser tab, header, footer, homepage |
+| #263 | Niet ingelogd: gebruik tab toont gemeenten | Verify "Gebruik" tab is NOT visible on application detail pages |
+| #278 | Filterteksten aanpassen | Check filter labels on /zoeken are correct and consistent |
+| #315 | Zoekpagina toont gemeentelijk applicatielandschap | Verify municipalities NOT shown as suppliers, no private data |
+| #345 | Dienst verschijnt niet in filters | Verify "Diensttype" filter is populated, diensten appear in results |
+| #347 | Dienstkaartje toont array | Verify dienst cards show readable text, not raw JSON arrays |
+| #394 | Contactpersonen gemeenten publiekelijk zichtbaar | Verify gemeente contact PII is NOT visible on public pages |
+| #443 | Dienst pagina: diensttypen aan elkaar geschreven | Verify diensttypen shown comma-separated on dienst detail page |
+| #444 | Vormgeving veranderd bij te lange URL's | Verify long URLs don't break page layout |
+| #447 | Zoeken: concept leverancier direct vindbaar | Verify concept/unapproved suppliers NOT visible in search |
+| #448 | Overzichtspagina's: vormgeving inconsistent | Verify dienst/koppeling detail pages match applicatie layout |
+| #453 | Zoeken: filters van slag met filter Type=Koppeling | Verify Type=Koppeling filter correctly scopes other facets |
+| #455 | Tabblad koppelingen en contactpersonen publiekelijk niet getoond | Verify Koppelingen and Contactpersonen tabs visible on public app detail pages |
+
+## Acceptance Criteria Reference
+
+**IMPORTANT**: Before testing each issue, read its detailed acceptance criteria in `issues.md` (in the repository root). Use these criteria to determine status:
+- **PASS** = ALL acceptance criteria are met
+- **PARTIAL** = Some criteria met, some not
+- **FAIL** = Key criteria not met or feature is broken
+- **CANNOT_TEST** = Feature not accessible or environment issue prevents testing
+
+## RBAC Reference
+
+As an unauthenticated visitor, you should only see data that has `"public"` read access:
+
+| Data Type | Should Be Visible? | Notes |
+|-----------|-------------------|-------|
+| Applicaties (leverancier) | YES | Only where `geregistreerdDoor: Leverancier` |
+| Applicaties (gemeente) | NO | Municipality application landscapes are private |
+| Diensten | YES | Public schema |
+| Organisaties | YES | Public schema |
+| Contactpersonen (leverancier) | YES | Visible via publication extensions |
+| Contactpersonen (gemeente) | NO | Private — this is the #394 bug |
+| Contactpersonen (samenwerking) | NO | Private |
+| Koppelingen | NO | Private schema |
+| Gebruik (usage) | NO | Private schema — this is the #263 check |
+
+## Testing Instructions
+
+### Step 1: Navigate to Search Page
+1. Navigate to `{FRONTEND}/zoeken?_page=1`
+2. Do NOT log in — remain anonymous
+3. Verify the page loads with search results
+
+### Step 2: Test #267 — Application Name
+1. Check the browser tab title
+2. Check the header/logo area for the application name
+3. Check the footer for the application name
+4. Navigate to the homepage (`/`) and check
+5. **Expected**: "Softwarecatalogus" everywhere (not "Development Catalogus" or just "softwarecatalogus")
+
+### Step 3: Test #345 — Dienst in Filters
+1. On `/zoeken?_page=1`, look at the available filter facets on the left
+2. Look for a "Type" or filter that includes "Dienst" as an option
+3. Look for a "Diensttype" filter — it should be populated with values
+4. Click on a dienst-type filter value and verify results filter correctly
+5. **Expected**: Diensten appear in results, diensttype filter has values
+
+### Step 4: Test #347 — Dienst Card Display
+1. Find a dienst in the search results (filter by type=Dienst if available)
+2. Look at the dienst card
+3. **Expected**: Service types shown as readable comma-separated text, NOT `["type1", "type2"]`
+4. Check that "Concept" status is clear (tooltip or alternative term)
+
+### Step 5: Test #278 — Filter Texts
+1. On the search page, examine all filter labels
+2. Verify labels are consistent with terminology used elsewhere
+3. Check for:
+ - "Organisatietype" filter: should contain only valid types (Leverancier, Gemeente, Samenwerking), NOT "Applicatie", "extern", "intern"
+ - "Aangeboden door" or "Leverancier" filter: should only contain actual suppliers
+ - Filter labels should match wizards and management page terminology
+
+### Step 6: Test #315 — Municipal Data Exposure
+1. On the search page, check the "Aangeboden door" or supplier filter
+2. **Expected**: Only actual vendors/suppliers listed, NOT municipalities like "Bloemendaal-Heemstede"
+3. Check search result cards — the "aangeboden door" text should show a real vendor, not a municipality
+4. Check the "Organisatietype" filter — should NOT contain contaminated values
+5. Navigate to an application detail page — verify the supplier is correct
+
+### Step 7: Test #263 — Gebruik Tab Visibility
+1. Find an application in search results and click it to open the detail page
+2. Look at the available tabs (Beschrijving, Diensten, Standaarden, etc.)
+3. **Expected**: There should be NO "Gebruik" tab visible — usage data is private
+4. If a "Gebruik" tab exists, check whether it shows municipality names (it should NOT)
+
+### Step 8: Test #394 — Contact Person Privacy
+1. Navigate to an application detail page of a **leverancier** application
+2. Check if contact person information is visible
+3. **Expected for leverancier**: Contact person name, email, phone MAY be visible (this is expected)
+4. Check the API directly: `curl {BACKEND}/index.php/apps/openregister/api/objects/voorzieningen/module?_extend[]=contactpersonen&_limit=5`
+5. In the API response, check contactpersonen:
+ - Leverancier contacts: expected to be visible
+ - Gemeente contacts (look for `organisatie` field → type "Gemeente"): should NOT be visible
+ - Samenwerking contacts: should NOT be visible
+6. Also check: `curl {BACKEND}/index.php/apps/openregister/api/objects/voorzieningen/contactpersoon?_limit=5` (without auth — should return 0 results since contactpersoon is not public)
+
+### Step 9: Additional Checks
+1. Check that the search page paginates correctly
+2. Verify sort options work (A-Z, Z-A, etc.)
+3. Check that clicking a search result navigates to a proper detail page (not `/publicatie/undefined`)
+4. Verify no "beheer" or admin links are visible in the navigation
+
+## Output Format
+
+Write results to: `softwarecatalog/test-results/bezoeker/results-public.md`
+
+Use this format:
+- Header with persona name, date, environment
+- Summary table: | Issue | Title | Previous Status | Current Status | Severity |
+- Per-issue sections with acceptance criteria checkboxes marked [x] or [ ]
+- Evidence screenshots saved to the same directory
+
+## Rules
+
+- **READ ONLY on GitHub issues** — never update, close, or comment on issues
+- Write test results ONLY to local files in the `test-results/` directory
+- Take screenshots for evidence where applicable
+- Do NOT log in — all testing is done as an anonymous visitor
diff --git a/.claude/skills/test-functioneel-beheerder.md b/.claude/skills/test-functioneel-beheerder.md
new file mode 100644
index 00000000..938196ed
--- /dev/null
+++ b/.claude/skills/test-functioneel-beheerder.md
@@ -0,0 +1,282 @@
+# Test Agent: Functioneel Beheerder (Functional Manager)
+
+## Persona
+
+**Peter van Dijk** — Functional Manager at VNG, 6 years GEMMA experience, 10 years municipal ICT.
+
+## Role: Functioneel beheerder (Full Admin)
+
+Peter has full system access. He activates organizations, manages users, maintains the GEMMA model, monitors data quality, and configures the system.
+
+## Login Credentials
+
+- **Username**: `{PERSONA_USERNAME}` (default: `peter.vandijk@test.nl`)
+- **Password**: `{PERSONA_PASSWORD}` (default: `WelcomeToTest2026`)
+- **Groups**: functioneel-beheerder, gebruik-beheerder, aanbod-beheerder, software-catalog-admins, software-catalog-users
+
+> These values are injected by the orchestrator. If not provided, use the defaults above (local dev only).
+
+## Test Environment
+
+- **Frontend**: `{FRONTEND}` (default: `{FRONTEND}`)
+- **Backend**: `{BACKEND}` (default: `{BACKEND}`)
+- **Browser**: Use Playwright MCP browser tools (prefixed `mcp__browser-N__`, where N is assigned by the orchestrator)
+- **Login URL**: `{FRONTEND}/login`
+- **Backend Admin**: `{BACKEND}/` ({ADMIN_USER}:{ADMIN_PASS})
+
+## Test Scope
+
+### Primary Steps
+- **Step 3**: Organization activation — Activate organizations, manage users, set passwords
+- **Step 5**: User management — Create users, assign roles, manage access
+- **Step 12**: Privacy — Verify admin has full access to all data
+- **Step 15**: AMEFF reference applications — Manage GEMMA component mappings
+- **Step 19**: Advanced connections — ArchiMate import/export, legacy data
+- **Step 21**: Admin and configuration — Content management, system settings, reports
+- **Step 23**: Functional manager overview — Dashboard, data quality monitoring
+- **Step 24**: AMEFF export — Specialized exports
+
+## Issues to Test
+
+### Previously tested (re-verify with auth):
+| Issue | Title | Previous Status |
+|-------|-------|-----------------|
+| #155 | Definities via interactieve optie (Begrippenlijst) | RE-TEST (new admin criteria added: empty external link, keywords as text) |
+| #267 | Naam is softwarecatalogus i.p.v. Softwarecatalogus | **MOVED → bezoeker** (public page check) |
+| #332 | Voorpagina inrichten | PARTIAL |
+| #397 | Pagina aanmaken via CMS | PASS |
+| #403 | Tekst verwijderen aanpassen | CANNOT_TEST → **re-test (see hint #2)** |
+| #406 | SiteImprove verwijderen | PARTIAL |
+| #409 | Footer anders: inlog of uitgelogd | PARTIAL |
+| #410 | Dashboard schrijfwijze softwarecatalogus | CANNOT_TEST → **re-test (login as leverancier)** |
+| #92 | Webstatistiekenpakket (Piwik Pro) | PARTIAL |
+| #169 | Rest issues Organisatie en Configuratie | PARTIAL |
+
+### New issues (not previously tested):
+| Issue | Title | Test Step |
+|-------|-------|-----------|
+| #85 | (VNGR) Publieke API toegang tot aanbodinformatie | Step 12 |
+| #141 | Organisaties samenvoegen na herindeling/overname | Step 21 — **see hint #5** |
+| #148 | (VNGR) GEMMA-architectuur opvraagbaar met API | Step 12 |
+| #225 | Testresultaten 29-10-2025 | General |
+| #278 | Filterteksten aanpassen | Step 14 |
+| #286 | 500-error bij wachtwoord wijzigen | Step 5 |
+| #392 | Geimporteerde gebruiker error bij omzetten naar user | Step 3 — **see hint #4** |
+| #393 | Backend: fouten in voorzieningenregister | Step 19 |
+| #396 | Verouderde NextCloud versie | Infra |
+| N/A | Themes management (exploratory) | Step 21 |
+| #15 | Exporteren van gegevens (CSV/Excel) | Step 24 |
+| #355 | Exporteren functies (Applicatie export) | Step 24 — **bug fixed, re-test** |
+| N/A | Schema export (OpenRegister registers) | Step 24 |
+| N/A | Import round-trip (export → modify → reimport) | Step 24 |
+| N/A | Facet editing (OpenRegister schemas) | Step 21 |
+| #187 | Tekstvoorstellen (remaining text changes) | Step 7 |
+| #449 | Handleiding facets configureren klopt niet | Step 21 |
+| #450 | Back-end: Icoon voor publiceren verwijderen | Step 6 |
+
+## Acceptance Criteria Reference
+
+**IMPORTANT**: Before testing each issue, read its detailed acceptance criteria in `issues.md` (in the repository root). Each issue has specific, testable acceptance criteria with checkboxes. Use these criteria to determine PASS/FAIL/PARTIAL status:
+- **PASS** = ALL acceptance criteria are met
+- **PARTIAL** = Some criteria met, some not
+- **FAIL** = Key criteria not met or feature is broken
+- **CANNOT_TEST** = Feature not accessible or environment issue prevents testing
+
+## Testing Hints for Specific Issues
+
+1. **#155 (glossary management)**: Navigate to the Nextcloud backend at `{BACKEND}/index.php/apps/opencatalogi/#/glossary` (Catalogi → Instellingen → Glossary). Test:
+ - Click **"Add Glossary"** to open the term modal
+ - Leave the **External Link** field empty and fill in term, summary, description — save should succeed without validation error
+ - In the **Keywords** field, type a keyword and press Enter — it should appear as a text tag (not a UUID)
+ - Add multiple keywords and verify they all display as readable text
+ - Save the term, then click to edit it — verify keywords load back as readable text tags
+ - If an existing term has keywords, click edit and verify they show as text, not UUIDs
+ - Take screenshots of: empty external link saving, keywords as text tags, editing existing term
+
+2. **#403 (delete dialog text)**: Previously CANNOT_TEST because Peter's admin account has no own-organization applications. **Workaround**: Log in as **Jan Pietersen** (leverancier) first to test the delete dialog on the frontend, OR test via the backend:
+ - **Option A (frontend as leverancier)**: Log in as `jan.pietersen@test.nl` / `WelcomeToTest2026`, navigate to `/beheer/applicaties`, find "Test Applicatie Leverancier", click Acties → Verwijderen. Verify dialog text, then Cancel.
+ - **Option B (backend as admin)**: Navigate to `{BACKEND}/index.php/apps/openregister` → Search/Views, find any object, click the three-dot menu → Delete. Verify the dialog text shows the object type and name.
+ - In both cases verify:
+ - The dialog shows the correct object type ("applicatie", "dienst", or "koppeling")
+ - The dialog shows the object name
+ - The dialog checks if the object is in use by municipalities
+ - Click **Cancel** to abort — do NOT actually delete.
+
+3. **#286 (500-error bij wachtwoord wijzigen)**: Test password change via Nextcloud backend user management:
+ 1. Navigate to `{BACKEND}/settings/users`
+ 2. Find a test user (e.g., `maria.vanderberg@test.nl`)
+ 3. Click the **three-dot menu** (⋮) on the user row → click **"Edit"** or open the user detail
+ 4. Find the password field and enter a new password (e.g., `NewTestPassword2026`)
+ 5. Save the change
+ 6. Verify: No 500 error occurs, and a success message appears
+ 7. **Revert**: Change the password back to `WelcomeToTest2026` so other tests still work
+ 8. Also test via OCS API: `curl -u {ADMIN_USER}:{ADMIN_PASS} -X PUT "{BACKEND}/ocs/v2.php/cloud/users/maria.vanderberg%40test.nl" -d "key=password" -d "value=WelcomeToTest2026" -H "OCS-APIRequest: true"` — verify HTTP 200 response (not 500)
+ 9. Take screenshots of the password change flow
+
+4. **#392 (geimporteerde gebruiker error bij omzetten)**: Previously CANNOT_TEST because no imported organization was available. **Setup first**, then test:
+
+ **Setup — Create an "imported" organization** (one not created via wizard):
+ 1. Use the API to create a test organization directly:
+ ```
+ curl -s -u {ADMIN_USER}:{ADMIN_PASS} -X POST '{BACKEND}/index.php/apps/openregister/api/objects/3/15' \
+ -H 'Content-Type: application/json' \
+ -d '{"naam":"Test Import Org","type":["Leverancier"],"website":"https://test-import.nl","beschrijvingKort":"Organisatie voor import test"}'
+ ```
+ 2. Note the returned UUID — this is your "imported" organization
+
+ **Test — Create a contact person for the imported org**:
+ 1. Navigate to `{BACKEND}/index.php/apps/openregister` → **Search / Views**
+ 2. Filter by register: **Voorzieningen**, schema: **Contactpersoon**
+ 3. Click **"Add"** (or the + button) to create a new contact person
+ 4. Fill in: voornaam: `Test`, achternaam: `Import`, email: `test.import@test.nl`
+ 5. Link it to the "Test Import Org" organization created above
+ 6. Save the contact person
+ 7. Verify: No error occurs during save — the contact person should be created AND automatically converted to a Nextcloud user
+ 8. Check the Nextcloud users list (`{BACKEND}/settings/users`) to see if `test.import@test.nl` was created
+ 9. Check the backend logs for errors: `docker exec nextcloud tail -20 /var/www/html/data/nextcloud.log`
+ 10. **Clean up**: Delete the test contact person and user after testing
+ 11. Take screenshots of each step
+
+5. **#141 (merge organizations)**: Previously CANNOT_TEST because no suitable merge candidate existed. **Setup first**, then test:
+
+ **Setup — Ensure a merge candidate exists**:
+ 1. First, check existing organizations: `curl -s -u {ADMIN_USER}:{ADMIN_PASS} '{BACKEND}/index.php/apps/openregister/api/objects/3/15?_fields=naam,id&_limit=20'`
+ 2. If there is no duplicate/redundant organization to merge, create one:
+ ```
+ curl -s -u {ADMIN_USER}:{ADMIN_PASS} -X POST '{BACKEND}/index.php/apps/openregister/api/objects/3/15' \
+ -H 'Content-Type: application/json' \
+ -d '{"naam":"Test Leverancier BV (oud)","type":["Leverancier"],"website":"https://test-leverancier-oud.nl","beschrijvingKort":"Oude organisatie voor merge test"}'
+ ```
+ 3. Note the returned UUID
+
+ **Test — Merge organizations via backend**:
+ 1. Navigate to `{BACKEND}/index.php/apps/openregister`
+ 2. Click **"Search / Views"** in the left sidebar
+ 3. In the filter area, select register: **"voorzieningen"** and schema: **"organisatie"**
+ 4. Find the source organization "Test Leverancier BV (oud)" in the results
+ 5. Click the **three-dot menu** (⋮) on the right of the row → click **"Merge"**
+ 6. A merge dialog should open — select the **target organization** "Test Leverancier BV"
+ 7. Walk through the merge dialog steps:
+ - **Property selection**: For each field, choose whether to keep source or target value
+ - **Relations/references**: Choose how to handle linked objects
+ 8. **Do NOT click the final "Merge" button** — click **Cancel** to abort
+ 9. Take screenshots of each dialog step
+ 10. Document whether the merge dialog loads without timeout errors (previous run had 30000ms timeout)
+
+3. **CMS pages (#397, #332)**: Manage CMS content at **{BACKEND}/index.php/apps/opencatalogi/pages#** (NOT `/#/pages`). This is the OpenCatalogi backend Pages management view. Test:
+ - Navigate to the pages URL
+ - Verify existing pages are listed (privacy, terms, FAQ, disclaimer, etc.)
+ - Create a new test page: click "Add", set title "Test Page", add content, save
+ - Edit an existing page: click on it, modify text, save
+ - Verify saved changes appear on the public frontend (e.g., `/test-page`)
+ - Delete the test page afterward to clean up
+
+4. **Themes**: Manage themes at **{BACKEND}/index.php/apps/opencatalogi/themes#**. Test:
+ - Navigate to the themes URL
+ - Verify the themes management page loads correctly
+ - Document what themes are available and which is active
+ - If possible: create a new theme, modify colors/branding, save, and verify the change is reflected on the frontend
+ - Check if the "Open Tilburg" footer branding can be changed via theme settings
+ - Take screenshots of the themes management interface
+
+4. **#15 / #355 (export)**: The export 500 error (#355) is **FIXED**. Test exports work correctly:
+ - In any beheer table, click **"Acties"** dropdown → **"Exporteren"** → **"Als CSV"** or **"Als Excel"**. Verify the download works.
+ - **Also verify via curl** (backend): `curl -s -o /dev/null -w '%{http_code}' -u {ADMIN_USER}:{ADMIN_PASS} '{BACKEND}/index.php/apps/openregister/api/objects/3/25/export?format=csv'` — should return `200` (not `500`).
+ - Check that exported CSV/Excel contains readable column values, not raw UUIDs. UUID relation columns should have a companion `_columnName` column with the resolved name.
+
+5. **Export & Import — Full Round-Trip Testing (OpenRegister)**:
+ Test ALL export formats and the round-trip workflow (export → modify → reimport → verify).
+
+ **5a. Schema-level object export (Excel)**:
+ 1. Navigate to `{BACKEND}/index.php/apps/openregister/registers#`
+ 2. Find the **"Voorzieningen"** register card and locate the **"Applicatie"** schema row
+ 3. Click the **three-dot menu** (⋮) on the Applicatie row → click **"Export"**
+ 4. In the export dialog, select **"Excel"** as the format
+ 5. Click **"Export"** — verify a .xlsx file downloads
+ 6. Open the file and verify it contains Applicatie object data with columns matching schema properties
+ 7. Take a screenshot of the export dialog and note the file size
+ 8. Document: Did the download succeed? Does the file contain expected columns (naam, beschrijving, etc.)? Are id values present?
+
+ **5b. Schema-level object export (CSV)**:
+ 1. Repeat step 5a but select **"CSV"** as the format
+ 2. Verify a .csv file downloads
+ 3. Open the file and verify the data matches the Excel export
+ 4. Document: Did CSV export work? Is the data comma-separated? Are special characters (Dutch diacritics) preserved?
+
+ **5c. Register-level API specification download (JSON config)**:
+ 1. Click the **three-dot menu** (⋮) on the **register card heading** "Voorzieningen" (NOT on a schema row)
+ 2. Click **"Download API Specification"**
+ 3. Verify a JSON file downloads containing the register configuration
+ 4. Open the file and check it contains register metadata, schema definitions, and property definitions
+ 5. Document: Is the JSON valid? Does it include all schemas? Are property types and constraints preserved?
+
+ **5d. Register-level import dialog**:
+ 1. Click the **three-dot menu** (⋮) on the **register card heading** "Voorzieningen"
+ 2. Click **"Import"**
+ 3. Verify the import dialog appears with:
+ - "Select File" button
+ - Supported file types listed: JSON, Excel (.xlsx, .xls), CSV
+ - Import requirements (id column, UUID format, metadata columns)
+ - Toggle options: Include objects, Enable validation, Enable events, Enable RBAC, Enable Multi-tenancy, Auto-publish
+ 4. Take a screenshot of the import dialog
+ 5. Click **"Cancel"** — do NOT import yet
+
+ **5e. Round-trip test: Export → Modify → Reimport → Verify**:
+ This is the critical test — verifying that data can be exported, modified externally, and reimported with changes applied.
+
+ 1. **Export**: Export the **"Organisatie"** schema from the **"Voorzieningen"** register as **Excel**
+ - Use the three-dot menu on the Organisatie row → Export → Excel
+ 2. **Download and inspect**: Note the current value of a field (e.g., the "naam" or "beschrijving" of one organisation)
+ 3. **Modify the file**: You cannot edit files locally, but you CAN test the import with the unmodified export file to verify the round-trip pipeline works:
+ - Click the register-level three-dot menu → **"Import"**
+ - Select the exported Excel file
+ - Ensure **"Include objects in the import"** is ON and **"Enable validation"** is ON
+ - Click **"Import"**
+ 4. **Verify**: After import completes:
+ - Check that no errors were reported
+ - Navigate to the Organisatie schema and verify objects still exist with correct data
+ - Check the audit trail (Dashboard → Audit Trail Actions) for import-related entries
+ 5. Document the entire flow with screenshots at each step
+
+ **5f. Import with different formats**:
+ If time permits, also test:
+ - Import a CSV file (schema-level import via the schema three-dot menu → Import)
+ - Import a JSON configuration file (register-level)
+ - Verify error handling: try importing a file with invalid data (wrong column names) and verify validation catches it
+
+6. **#410 (Dashboard schrijfwijze softwarecatalogus)**: Previously CANNOT_TEST because Peter's admin account doesn't see the supplier/gemeente dashboard. **Workaround**: Log in as Jan Pietersen (`jan.pietersen@test.nl` / `WelcomeToTest2026`) and navigate to `/beheer`. Check the dashboard welcome heading for the capitalization of "softwarecatalogus" vs "Softwarecatalogus". Use `browser_snapshot` to capture the exact text. Compare with the header, footer, and browser tab title.
+
+7. **Facet editing (OpenRegister schemas page)**: Test renaming a facet on a schema property:
+ 1. Navigate to `{BACKEND}/index.php/apps/openregister/schemas#`
+ 2. Find and click on a schema that has faceted properties (e.g., "dienst" which has "dienstType" with a facet)
+ 3. In the schema detail view, find the properties list
+ 4. Click the **action menu** (three-dot menu) on a faceted property (e.g., "dienstType")
+ 5. Select **"Edit"** or click to open the property editor
+ 6. Find the **facet configuration** section — it should show the current facet title
+ 7. Change the facet title (e.g., rename it to "Test Facet Title")
+ 8. Save the property changes
+ 9. Verify the facet title updated in the schema by refreshing the page
+ 10. **Revert the change** — rename it back to the original title (e.g., "Diensttype") and save
+ 11. Take screenshots of the facet editing interface
+ 12. Document: Is the facet editing UI intuitive? Does saving work without errors? Does the change persist after refresh?
+
+## Instructions
+
+When running tests for this persona:
+1. Navigate to `{FRONTEND}/login`
+2. Log in with `{PERSONA_USERNAME}` / `{PERSONA_PASSWORD}`
+3. Also test the Nextcloud backend at `{BACKEND}/` (login `{ADMIN_USER}`/`{ADMIN_PASS}`)
+4. **For each issue**: Read the acceptance criteria in `issues.md`, then test each criterion
+5. Focus on admin-specific functionality other personas can't access
+6. Test organization lifecycle: concept → active → inactive → reactivated
+7. Test user lifecycle: create → assign role → deactivate → reactivate
+8. Test content management and system configuration
+9. For issues previously PARTIAL, verify the remaining parts now with auth
+10. Write results to `test-results/functioneel-beheerder/results-authenticated.md`
+11. For each issue, list which acceptance criteria passed and which failed
+
+## Rules
+
+- **READ ONLY on GitHub issues** — never update, close, or comment on issues
+- Write test results ONLY to local files in the `test-results/` directory
+- Take screenshots for evidence where applicable
diff --git a/.claude/skills/test-gemeente.md b/.claude/skills/test-gemeente.md
new file mode 100644
index 00000000..85a4e439
--- /dev/null
+++ b/.claude/skills/test-gemeente.md
@@ -0,0 +1,305 @@
+# Test Agent: Gemeente (Municipality)
+
+## Persona
+
+**Maria van der Berg** — ICT-coördinator at a medium-sized Dutch municipality, 8 years experience.
+
+## Role: Gebruik-beheerder
+
+Maria manages her municipality's software landscape in the Softwarecatalogus. She registers which applications her municipality uses, manages connections between systems, and uses benchmarking to compare with similar municipalities.
+
+## Login Credentials
+
+- **Username**: `{PERSONA_USERNAME}` (default: `maria.vanderberg@test.nl`)
+- **Password**: `{PERSONA_PASSWORD}` (default: `WelcomeToTest2026`)
+- **Groups**: gebruik-beheerder, software-catalog-users
+
+> These values are injected by the orchestrator. If not provided, use the defaults above (local dev only).
+
+## Test Environment
+
+- **Frontend**: `{FRONTEND}` (default: `{FRONTEND}`)
+- **Backend**: `{BACKEND}` (default: `{BACKEND}`)
+- **Browser**: Use Playwright MCP browser tools (prefixed `mcp__browser-N__`, where N is assigned by the orchestrator)
+- **Login URL**: `{FRONTEND}/login`
+
+## Test Scope
+
+### Primary Steps
+- **Step 4**: First login — Log in as gemeente user, verify dashboard
+- **Step 6**: Organization profile — Complete municipality profile, join samenwerkingen
+- **Step 9**: Dienst wizard — Register diensten for municipality applications (gemeente perspective)
+- **Step 10**: Usage reporting — Register application usage, create usage reports
+- **Step 11**: Connection wizard — Register connections between applications (gemeente perspective)
+- **Step 12**: Privacy and visibility — Verify gemeente can only see own usage/connections
+- **Step 13**: Excel export — Export municipality data
+- **Step 14**: Search and results — Search for applications, filter results
+- **Step 17**: "Gluren bij de buren" — Compare with other municipalities
+
+### Secondary Steps (observe/verify)
+- **Step 7/8**: Product pages — Verify product detail pages show correct info
+- **Step 16**: Standards — Filter on standards support
+- **Step 22**: Advanced search — Complex filter combinations
+
+## Issues to Test
+
+### Previously tested (re-verify with auth):
+| Issue | Title | Previous Status |
+|-------|-------|-----------------|
+| #144 | Overzicht organisaties met zoek- en filteropties | PASS |
+| #266 | Na inloggen: Mijn account & persoonlijke gegevens leeg? | CANNOT_TEST |
+| #280 | Zoeken: sorteren gaat niet goed | PARTIAL |
+| #340 | Bevindingen op tussenoplevering Zoeken | PARTIAL |
+| #342 | Zoeken: op kaartjes referentiecomponenten duidelijk maken | FAIL |
+| #344 | Zoeken: Geen resultaten bij Gravenbeheercomponent | PASS |
+| #350 | De link achter de gebruikersnaam verwijzen naar Mijn account | CANNOT_TEST |
+| #353 | Mijn account – Je "functie" wordt niet aangepast na bewerken en opslaan | CANNOT_TEST → **re-test (see hint #6)** |
+| #355 | Diensten: Export geeft allerlei UUID's | CANNOT_TEST → **re-test (bug fixed)** |
+| #395 | Menu linkerkant verdwijnt | PARTIAL |
+
+### New issues (not previously tested):
+| Issue | Title | Test Step |
+|-------|-------|-----------|
+| #15 | Data vanuit softwarecatalogus exporteren | Step 13 |
+| #278 | Filterteksten aanpassen | Step 14 |
+| #286 | Aanmelden organisatie: 500-error bij wachtwoord wijzigen | **MOVED → functioneel-beheerder** |
+| #315 | Hoge prioriteit: Zoekpagina toont deel van gemeentelijk applicatielandschap | Step 14 |
+| #316 | Dienst toevoegen: Stap 1 Dienst zoeken | Step 9 (gemeente dienst wizard) |
+| #317 | Dienst toevoegen: Stap 2 Gebruiksinformatie | Step 9 (gemeente dienst wizard) |
+| #318 | Dienst toevoegen: Stap 3 Controleren | Step 9 (gemeente dienst wizard) |
+| #319 | Koppeling toevoegen: Stap 1 Koppeling zoeken | Step 11 (gemeente koppeling wizard) |
+| #320 | Koppeling toevoegen: Stap 2 Gebruiksinformatie | Step 11 (gemeente koppeling wizard) |
+| #321 | Koppeling toevoegen: Stap 3 Deelnemer | Step 11 (gemeente koppeling wizard) |
+| #322 | Koppeling toevoegen: Stap 4 Controleren | Step 11 (gemeente koppeling wizard) |
+| #323 | Applicatie toevoegen: Stap 1 Applicatie zoeken | Step 10 (gemeente app wizard) |
+| #324 | Applicatie toevoegen: Stap 2 Gebruiksinformatie | Step 10 (gemeente app wizard) |
+| #325 | Applicatie toevoegen: Stap 3 Referentiecomponenten | Step 10 (gemeente app wizard) |
+| #326 | Applicatie toevoegen: Stap 4 Deelnemer | Step 10 (gemeente app wizard) |
+| #327 | Applicatie toevoegen: Stap 5 Controleren | Step 10 (gemeente app wizard) |
+| #328 | Applicatie toevoegen: Stap 1.1 Nieuwe applicatie opvoeren | Step 10 (gemeente app wizard) |
+| #343 | Zoeken: Filter 'Type koppeling' toevoegen | Step 14 |
+| #345 | Zoeken: toegevoegde dienst verschijnt niet in filters | **MOVED → bezoeker** (public search page) |
+| #346 | Zoeken: paginering werkt niet | Step 14 |
+| #347 | Zoeken: Dienstkaartje toont array | **MOVED → bezoeker** (public search page) |
+| #349 | Zoeken: UUID's onder standaarden filter | Step 14 |
+
+## Acceptance Criteria Reference
+
+**IMPORTANT**: Before testing each issue, read its detailed acceptance criteria in `issues.md` (in the repository root). Each issue has specific, testable acceptance criteria with checkboxes. Use these criteria to determine PASS/FAIL/PARTIAL status:
+- **PASS** = ALL acceptance criteria are met
+- **PARTIAL** = Some criteria met, some not
+- **FAIL** = Key criteria not met or feature is broken
+- **CANNOT_TEST** = Feature not accessible or environment issue prevents testing
+
+## Detail Page Testing
+
+For each detail page type, navigate to the public detail page and verify the following:
+
+### Applicatie Detail Page
+- Navigate to an application detail page (e.g., from search results → click an application)
+- **Tabs**: Verify all tabs load (Beschrijving, Diensten, Koppelingen, Standaarden, Gebruik, Versies)
+- **Tab loading**: Check that tabs load consistently without excessive delays (#351)
+- **Tab titles**: Verify tab titles match the design specification (#248)
+- **Diensten tab**: Verify linked services are shown (#373)
+- **Standaarden tab**: Check standards display correctly, no UUIDs visible (#371, #374)
+- **Compliance**: Verify compliance display is consistent and counts match
+- **Gebruik tab**: As logged-in gemeente user, verify you can see usage data for your own municipality
+- **Gebruik tab privacy**: Verify you canNOT see other municipalities' detailed usage (#315)
+- **Referentiecomponenten**: Check that reference components are clearly labeled on the card (#342)
+- **Versies**: Check version display
+
+### Koppeling Detail Page
+- Navigate to a connection detail page (from search or from an application's Koppelingen tab)
+- **Card display**: Verify the card shows meaningful data, not empty fields (#401)
+- **Direction**: Check that the connection direction (richting) is displayed
+- **Linked applications**: Verify both source and target applications are shown with names (not UUIDs)
+- **Filter "Type koppeling"**: Verify the connection type filter exists and works (#343)
+
+### Dienst Detail Page
+- Navigate to a service detail page (from search results or from an application's Diensten tab)
+- **Beschrijving tab**: Verify description tab exists and shows content (#408)
+- **Labels**: Check that labels are consistent (no mix of "Diensttype" and "Type") (#357)
+- **Array display**: Check that fields don't show raw arrays like `["value1","value2"]` (#347)
+- **Search filter**: After viewing a dienst, verify it appears in the search filters (#345)
+
+### Organisatie Detail Page
+- Navigate to an organization detail page (e.g., from search or organization overview)
+- **Profile fields**: Verify all profile fields are shown correctly
+- **Type**: Verify the organization type (Leverancier/Gemeente/Samenwerking) is displayed
+- **Privacy**: As gemeente user, verify **gemeente** contactpersonen are NOT publicly visible (#394). Note: leverancier contactpersonen ARE expected to be public via publications.
+- **Applications**: If the org is a leverancier, verify their published applications are listed
+
+## Wizard Walkthroughs — MANDATORY
+
+**CRITICAL**: As gebruik-beheerder, you have access to "toevoegen" (add) wizards. You MUST execute ALL THREE wizards below before testing search/filter issues. This creates test data for your municipality.
+
+### Wizard 1: Applicatie toevoegen (gebruik registreren)
+
+**Route**: Navigate to `/beheer` dashboard and click **"Applicatie toevoegen"** button (or go to `/forms/gebruik/applicatie?type=gemeente`)
+
+**Step 1 — Applicatie selecteren:**
+1. In the dropdown, search for and select an existing application (e.g., "Centric Burgerzaken")
+2. If the desired application is not listed, click **"Ik kan de gewenste applicatie niet vinden"** and fill in naam: `Test Gemeente App`
+3. Click **"Volgende"**
+4. Take screenshot: `wizard-gemeente-app-step1.png`
+
+**Step 2 — Gebruiksinformatie:**
+1. Select **Hosting**: any option (SaaS/on-premise/hybrid)
+2. Fill in **Interne notitie**: `Testregistratie via wizard`
+3. **Status**: Leave default (Verwerving)
+4. **Startdatum**: Auto-filled with today's date
+5. Select **Applicatieversie** if available
+6. Click **"Volgende"**
+7. Take screenshot: `wizard-gemeente-app-step2.png`
+
+**Step 3 — Referentiecomponenten:**
+1. Review "Referentiecomponenten aangegeven door leverancier" (read-only)
+2. In the **Referentiecomponenten toevoegen** dropdown, search for and select a component (e.g., "Zaakregistratiecomponent")
+3. Click **"Volgende"**
+4. Take screenshot: `wizard-gemeente-app-step3.png`
+
+**Step 4 — Controleren:**
+1. Verify all data: status, startdatum, applicatie, referentiecomponenten
+2. Take screenshot: `wizard-gemeente-app-review.png`
+3. Click **"Gebruik registreren"**
+4. Verify success: "Gebruik succesvol geregistreerd!"
+5. Take screenshot: `wizard-gemeente-app-success.png`
+
+### Wizard 2: Dienst toevoegen
+
+**Route**: Navigate to `/beheer/diensten` and click **"Toevoegen"** button
+
+**Step 1 — Applicaties:**
+1. In the dropdown, search for and select an applicatie (e.g., an app from your municipality's landscape)
+2. Click **"Volgende"**
+3. Take screenshot: `wizard-gemeente-dienst-step1.png`
+
+**Step 2 — Dienst informatie:**
+1. Fill in **naam**: `Test Gemeente Dienst`
+2. Fill in **website**: `https://test-gemeente.nl/dienst` (optional)
+3. Fill in **beschrijvingKort**: `Dienst geregistreerd door Test Gemeente`
+4. Select **diensttype**: "Functioneel beheer" (multi-select, options: Functioneel beheer, Applicatiebeheer, Technisch beheer, Implementatieondersteuning, Opleidingen, Licentiereseller)
+5. Skip optional fields (logo, uitgebreide omschrijving, contactpersoon)
+6. Click **"Volgende"**
+7. Take screenshot: `wizard-gemeente-dienst-step2.png`
+
+**Step 3 — Controleren:**
+1. Verify all data: naam, diensttype, linked applicatie
+2. Take screenshot: `wizard-gemeente-dienst-review.png`
+3. Click **"Dienst registreren"**
+4. Verify success: "Dienst succesvol aangemeld!"
+5. Take screenshot: `wizard-gemeente-dienst-success.png`
+
+### Wizard 3: Koppeling toevoegen
+
+**Route**: Navigate to `/beheer/koppelingen` and click **"Toevoegen"** button
+
+**Step 1 — Koppeling zoeken:**
+1. Select an **Applicatie** from the dropdown (e.g., one from your municipality's landscape)
+2. Review "Bestaande koppelingen" section (may be empty)
+3. Click **"Volgende"**
+4. Take screenshot: `wizard-gemeente-koppeling-step1.png`
+
+**Step 2 — Koppeling definiëren:**
+1. **Applicatie A** is pre-filled and locked
+2. Select **Richting**: "A -> B" (options: "A -> B", "B -> A", "Bi-directioneel")
+3. In **Applicatie B of BGV**, search for and select a target (e.g., "MijnOverheid.nl" or another app)
+4. Fill in **Naam**: `Test Gemeente Koppeling`
+5. Select **Status**: any option
+6. Click **"Volgende"**
+7. Take screenshot: `wizard-gemeente-koppeling-step2.png`
+
+**Step 3 — Aanvullende informatie:**
+1. Fill in **beschrijvingKort**: `Koppeling geregistreerd door Test Gemeente` (max 255 chars)
+2. Skip optional fields (lange beschrijving, standaardversies, transportprotocol, intermediair)
+3. Click **"Volgende"**
+4. Take screenshot: `wizard-gemeente-koppeling-step3.png`
+
+**Step 4 — Controleren:**
+1. Verify koppeling naam and direction (e.g., "Applicatie A -> Applicatie B")
+2. Take screenshot: `wizard-gemeente-koppeling-review.png`
+3. Click **"Opslaan"**
+4. Verify success: "Koppelingen succesvol opgeslagen!"
+5. Take screenshot: `wizard-gemeente-koppeling-success.png`
+
+### After Wizards: Verify Created Objects
+
+After completing all three wizards:
+1. Navigate to `/beheer/applicaties` — verify the registered applicatie appears in the table
+2. Navigate to `/beheer/diensten` — verify "Test Gemeente Dienst" appears
+3. Navigate to `/beheer/koppelingen` — verify "Test Gemeente Koppeling" appears
+4. Take screenshots of each table showing the created objects
+
+---
+
+## Testing Hints for Specific Issues
+
+1. **#344 (Referentiecomponenten filter)**: Navigate to `{FRONTEND}/zoeken` and test the filter:
+ 1. Find the **"Referentiecomponenten"** filter dropdown on the left side
+ 2. Click it to open the dropdown
+ 3. **TYPE "Graven"** in the search field inside the dropdown — NcSelect supports type-to-filter
+ 4. Verify that "Gravenbeheercomponent" (or similar) appears as a filterable option
+ 5. Select it and verify search results update to show only applications with that component
+ 6. Take a screenshot of the filter dropdown with typed text and the filtered results
+2. **#286**: **MOVED to functioneel-beheerder** — this is an admin-level password change test via the Nextcloud backend, not a gemeente flow.
+3. **#15 (export)**: Test CSV and Excel export from any beheer page. Steps:
+ 1. Navigate to `{FRONTEND}/beheer/applicaties` (or any beheer page like `/beheer/diensten`, `/beheer/koppelingen`)
+ 2. Find the **"Acties"** dropdown button (top-right of the table, near the search/filter area)
+ 3. Click **"Acties"** → **"Exporteren"** → **"Als CSV"**
+ 4. Verify a CSV file downloads containing the table data
+ 5. Repeat with **"Als Excel"** and verify an Excel file downloads
+ 6. Check that exported data contains readable column names and values (not UUIDs)
+ 7. Take screenshots of the Acties dropdown with export options visible
+4. **#355 (diensten export UUIDs)**: This bug is now **FIXED** — exports return HTTP 200 with resolved names for UUID columns. Test the fix:
+ 1. Navigate to `{FRONTEND}/beheer/diensten`
+ 2. Click **"Acties"** → **"Exporteren"** → **"Als CSV"**
+ 3. Open the CSV and check that columns use **readable names** (e.g., "dienstType" shows "SaaS" not a UUID)
+ 4. If any column shows UUIDs instead of human-readable values, mark as FAIL
+ 5. **Also verify via curl** (backend export): `curl -s -u {PERSONA_USERNAME_URLENCODED}:{PERSONA_PASSWORD} '{BACKEND}/index.php/apps/openregister/api/objects/3/26/export?format=csv' -o /tmp/dienst-export.csv && head -2 /tmp/dienst-export.csv` — verify the CSV contains readable column headers and resolved values
+5. **#349 (UUID's in standaarden filter)**: Navigate to `{FRONTEND}/zoeken` and test the standards filter:
+ 1. Find the **"Standaardversies"** filter dropdown on the left side
+ 2. Click it to expand/open the dropdown
+ 3. Scroll through the options and check if they show **human-readable names** or raw **UUIDs**
+ 4. If any option shows a UUID (e.g., `a1b2c3d4-...`) instead of a readable standard name, mark as FAIL
+ 5. Take a screenshot of the expanded filter dropdown showing the options
+6. **#353 (Functie niet aangepast na bewerken)**: Navigate to Mijn Account and test editing:
+ 1. Navigate to `{FRONTEND}/account` (or find the "Mijn Account" link in the user menu / header). Note: `/mijn-account` now redirects to `/account`.
+ 2. Find the **"functie"** (job title) field on the account page
+ 3. Note the current value
+ 4. Change the value to something different (e.g., "ICT Test Coordinator")
+ 5. Click **Save** (or the save button)
+ 6. Refresh the page (F5) and check if the new value persists
+ 7. Navigate away and come back — verify the change is still there
+ 8. If the value reverts to the old value, mark as FAIL
+ 9. Take screenshots before and after the edit
+7. **#328 (Nieuwe applicatie opvoeren sub-step)**: Previously CANNOT_TEST because the agent thought it was only in the supplier wizard. It IS available in the gemeente app wizard too. During the Applicatie wizard (Wizard 1):
+ 1. Navigate to the gemeente applicatie wizard: `/forms/gebruik/applicatie?type=gemeente`
+ 2. In Step 1 ("Applicatie zoeken/selecteren"), use `browser_snapshot` to capture the full page
+ 3. Look for a button or link labeled **"Ik kan de gewenste applicatie niet vinden"** (or similar text — it may also say "Applicatie niet gevonden?" or "Nieuwe applicatie toevoegen")
+ 4. If the button/link exists, click it — it should open sub-step 1.1
+ 5. Verify the sub-step shows a form for entering a new application:
+ - Title text about adding/registering a new application
+ - Fields for application name, leverancier selection, website
+ 6. Take a screenshot of the sub-step form
+ 7. Click **Back** or navigate back to the normal wizard flow — do NOT submit this form (it would create a duplicate)
+ 8. If the button/link does NOT exist in the gemeente wizard, mark as **FAIL** with note: "Sub-step 1.1 is not available in the gemeente applicatie wizard" (this would be a real issue, not a test limitation)
+
+## Instructions
+
+When running tests for this persona:
+1. Navigate to `{FRONTEND}/login`
+2. Log in with `{PERSONA_USERNAME}` / `{PERSONA_PASSWORD}`
+3. **FIRST**: Execute ALL THREE wizard walkthroughs above (applicatie, dienst, koppeling). This is mandatory.
+4. After wizards complete, verify created objects in beheer tables
+5. **THEN**: Test each issue from the Issues to Test table, using acceptance criteria from `issues.md`
+6. For wizard-related issues (#316-#328): test during or immediately after the relevant wizard execution
+7. Test all search and filter functionality
+8. Pay special attention to privacy — gemeente should NOT see other organizations' private data
+9. Write results to `test-results/gemeente/results-authenticated.md`
+10. For each issue, list which acceptance criteria passed and which failed
+
+## Rules
+
+- **READ ONLY on GitHub issues** — never update, close, or comment on issues
+- Write test results ONLY to local files in the `test-results/` directory
+- Take screenshots for evidence where applicable
diff --git a/.claude/skills/test-leverancier.md b/.claude/skills/test-leverancier.md
new file mode 100644
index 00000000..c7008c72
--- /dev/null
+++ b/.claude/skills/test-leverancier.md
@@ -0,0 +1,482 @@
+# Test Agent: Leverancier (Vendor)
+
+## Persona
+
+**Jan Pietersen** — Director of a small software company (8 employees), 15 years experience in municipal software.
+
+## Role: Aanbod-beheerder
+
+Jan manages his company's products in the Softwarecatalogus. He registers applications, services, connections, and standards. He also manages which municipalities use his products.
+
+## Login Credentials
+
+- **Username**: `{PERSONA_USERNAME}` (default: `jan.pietersen@test.nl`)
+- **Password**: `{PERSONA_PASSWORD}` (default: `WelcomeToTest2026`)
+- **Groups**: aanbod-beheerder, software-catalog-users
+
+> These values are injected by the orchestrator. If not provided, use the defaults above (local dev only).
+
+## Test Environment
+
+- **Frontend**: `{FRONTEND}` (default: `{FRONTEND}`)
+- **Backend**: `{BACKEND}` (default: `{BACKEND}`)
+- **Browser**: Use Playwright MCP browser tools (prefixed `mcp__browser-N__`, where N is assigned by the orchestrator)
+- **Login URL**: `{FRONTEND}/login`
+
+## Test Scope
+
+This agent tests the following steps from the test flow (`testen.md`):
+
+### Primary Steps
+- **Step 2**: Organization registration — Register as a new vendor
+- **Step 3**: Organization activation — Activate vendor account via backend
+- **Step 4**: First login — Log in as vendor, verify dashboard and wizards
+- **Step 5**: Colleague invitations — Add team members, manage roles
+- **Step 6**: Organization profile — Complete vendor profile
+- **Step 7**: Product creation (single module) — Full product wizard
+- **Step 8**: Product creation (multi module) — Complex modular products
+- **Step 9**: Service wizard — Add services to products
+- **Step 12**: Privacy and visibility — Verify vendor can see own product usage
+- **Step 16**: Standards management — Register standards for products
+- **Step 18**: Vendor usage management — "Applicatiegebruik melden" wizard, view customers, manage usage reports
+
+### Secondary Steps (observe/verify)
+- **Step 13**: Excel export — Export product data
+- **Step 14**: Search and results — Verify products appear in search
+- **Step 15**: AMEFF reference — Select GEMMA components
+
+## Issues to Test
+
+### Previously tested (re-verify with auth):
+| Issue | Title | Previous Status |
+|-------|-------|-----------------|
+| #294 | Applicatie publiceren: uitlijning rechthoek | CANNOT_TEST → **re-test (see hint #12)** |
+| #300 | Beheer: overzicht applicaties teveel applicaties | CANNOT_TEST |
+| #302 | Beheer: applicatie bewerken (ophalen van gegevens is traag) | CANNOT_TEST |
+| #370 | Applicatie: teveel kolommen worden getoond | PASS |
+| #373 | Applicatie: Gekoppelde diensten worden niet getoond | FAIL → **re-test (bug fixed)** |
+| #375 | Applicaties: versie voor SaaS applicaties? | PARTIAL → **re-test (bug fixed)** |
+| #376 | Applicaties: labels wizard en tabel zijn anders | CANNOT_TEST → **re-test (see hint #18)** |
+| #377 | Applicaties: tabel toont diensten niet | CANNOT_TEST |
+| #379 | Applicatie: verschillende manier van tonen compliancy | PARTIAL |
+| #380 | Applicatie: compliance aantallen komen niet overeen | CANNOT_TEST → **re-test (see hint #23)** |
+| #381 | Applicaties: non-compliant vervangen door niet ondersteund | PASS |
+| #382 | Applicatie: compliancy link werkt niet | PASS |
+| #383 | Applicatie: selectie vakken werken niet | CANNOT_TEST |
+| #384 | Applicaties: eenduidige manier van bewerken | CANNOT_TEST |
+| #385 | Applicatie: Geen huidige versie in gebruik | PASS |
+| #386 | Applicaties – Uw applicatie publiceren: andere labels | CANNOT_TEST → **re-test (see hint #19)** |
+| #387 | Applicaties – Uw applicatie publiceren: i niet aanwezig | CANNOT_TEST → **re-test (see hint #20)** |
+| #390 | Applicaties – Uw applicatie publiceren: labels komen niet overeen | CANNOT_TEST → **re-test (see hint #21)** |
+| #399 | Versies: versie van andere leverancier geeft foutmelding | CANNOT_TEST → **re-test (bug fixed)** |
+| #105 | Aanbieders zien applicatielandschappen en koppelingen niet | CANNOT_TEST (moved from security-officer — needs aanbod-beheerder role) |
+
+### New issues (not previously tested):
+| Issue | Title | Test Step |
+|-------|-------|-----------|
+| #185 | Detailpagina's | Step 7 |
+| #248 | Titels van de tabs in orde maken | Step 7 |
+| #263 | Niet ingelogd: onder een applicatie staat in het tabje gebruik de gemeenten | **MOVED → bezoeker** (unauthenticated test) |
+| #274 | Wizard dienst: tekst dient nog aangepast te worden naar nieuwe benamingen | Step 9 |
+| #306 | Dienst: Overzicht controleren verbeteren | Step 9 |
+| #307 | Diensten overzicht: meer dienst bij organisatie dan er horen | Step 9 |
+| #308 | Diensten overzicht: default kolommen + kolom verwijderen | Step 9 |
+| #312 | Koppeling heeft verplicht een naam | Step 11 |
+| #314 | Wizard Koppeling publiceren vind zelf aangemaakte applicaties niet | Step 11 |
+| #345 | Zoeken: toegevoegde dienst verschijnt niet in filters | **MOVED → bezoeker** (public search page test) |
+| #347 | Zoeken: Dienstkaartje toont array | **MOVED → bezoeker** (public search page test) |
+| #348 | Het aantal standaarden komen niet overeen bij Centric Begraven | Step 7 |
+| #351 | Het laden van de tabbladen gaat ongelijk | Step 7 |
+| #352 | Mijn account - Contactpersoon bij applicatie publiceren niet veranderd | Step 7 |
+| #354 | Diensten - incomplete lijst applicaties | Step 9 |
+| #356 | Diensten: geen tussenvoegsel bij namen | Step 9 |
+| #357 | Diensten: Diensttype en Type wordt door elkaar gebruikt | Step 9 |
+| #358 | Diensten: De status "Concept" wordt nog op verschillende plekken getoond | Step 9 |
+| #359 | Diensten wizard: Uw dienst publiceren - tekst aanpassen | Step 9 |
+| #360 | Diensten wizard – Uw dienst publiceren: Meerdere i komen niet overeen met ppt | Step 9 |
+| #361 | Diensten wizard – Uw dienst publiceren: inconsistentie in labels | Step 9 |
+| #362 | Diensten wizard – Uw dienst publiceren: onlogische tekst bovenaan aanmeld-stap | Step 9 |
+| #363 | Diensten wizard – Uw dienst publiceren: catalogus i.p.v. softwarecatalogus | Step 9 |
+| #364 | Contactpersonen: e-mailadres is leeg | Step 5 |
+| #365 | Contactpersonen: error bij het opslaan van een contactpersoon | Step 5 |
+| #366 | Contactpersonen: veld Rollen niet consistent | Step 5 |
+| #367 | Contactpersonen: Tussenvoegsel wordt niet getoond | Step 5 |
+| #368 | Applicatie publiceren: Zonder een richting aan te geven is de koppeling op te voeren | Step 11 |
+| #369 | Applicatie publiceren: de aangemaakte koppeling is niet zichtbaar | Step 11 |
+| #371 | Applicatie: UUID onder compliance | Step 7 |
+| #372 | Applicaties: Kolom Contactpersoon toont geen tussenvoegsel | Step 7 |
+| #374 | Applicaties: Standaarden, Standaarden GEMMA en Standaardversies? | Step 7 |
+| #378 | Applicatie: Standaarden na wijzigen veranderd | Step 7 |
+| #391 | Testen met een gebruiker van een bestaande organisatie | Step 3 |
+| #392 | Back-end: geimporteerde gebruiker geeft error bij omzetten naar user | **MOVED → functioneel-beheerder** |
+| #400 | Koppeling - Opslaan van een koppeling geeft een foutmelding | Step 11 |
+| #401 | Koppeling - geïmporteerde koppelingen kaartjes zijn leeg | Step 11 |
+| #402 | Verschil tussen Edge en Chrome bij laden applicaties | Step 7 |
+| #407 | Toegevoegde standaarden verwijzen naar id-id-.... | Step 16 |
+| #408 | Tabblad beschrijving bij Dienst | Step 9 |
+| #187 | Tekstvoorstellen (remaining text changes) | Step 7 |
+| #443 | Dienst pagina: diensttypen aan elkaar geschreven | Step 9 |
+| #444 | Vormgeving veranderd bij te lange URL's | Step 7 |
+| #445 | Nieuwe dienst verkeerde afsluitende pagina | Step 9 |
+| #446 | Dienst publiceren: tekstuele inconsistenties | Step 9 |
+| #448 | Overzichtspagina's: verschillende vormgeving en acties | Step 7 |
+| #450 | Back-end: Icoon voor publiceren verwijderen | Step 6 |
+| #451 | Koppeling: UUID's zichtbaar bij standaardversies | Step 11 |
+| #452 | Applicaties overzicht: toont niet alle koppelingen | Step 7 |
+| #453 | Zoeken: filters van slag met filter Type=Koppeling | Step 14 |
+| #454 | Wizard koppelingen: Reeds bestaande koppelingen voor worden niet gevonden | Step 11 |
+| #456 | Consistentie in werking van wizards | Step 7 |
+
+## Acceptance Criteria Reference
+
+**IMPORTANT**: Before testing each issue, read its detailed acceptance criteria in `issues.md` (in the repository root). Each issue has specific, testable acceptance criteria with checkboxes. Use these criteria to determine PASS/FAIL/PARTIAL status:
+- **PASS** = ALL acceptance criteria are met
+- **PARTIAL** = Some criteria met, some not
+- **FAIL** = Key criteria not met or feature is broken
+- **CANNOT_TEST** = Feature not accessible or environment issue prevents testing
+
+## Detail Page Testing — MANDATORY
+
+**CRITICAL**: Many issues (20+) were CANNOT_TEST in the previous run because detail pages were never opened. You MUST open detail pages for applicaties, diensten, koppelingen, and organisaties.
+
+### How to open a detail page
+
+Detail pages are opened via the **publicatie URL pattern**:
+```
+{FRONTEND}/publicatie/{id}
+```
+
+**To find the ID of an object:**
+1. Go to the beheer table (e.g., `/beheer/applicaties`)
+2. Click on a row — the URL or page content will show the object ID (UUID)
+3. Alternatively, use the API to find IDs:
+ ```
+ curl -s -u {ADMIN_USER}:{ADMIN_PASS} '{BACKEND}/index.php/apps/openregister/api/objects/3/25?_limit=5&_fields=naam,id'
+ ```
+ (register 3 = Voorzieningen, schema 25 = Applicatie, schema 26 = Dienst, schema 28 = Koppeling, schema 15 = Organisatie)
+
+**After completing the wizard** (which creates "Test Wizard App"), find its ID in the beheer table or API, then navigate to:
+```
+{FRONTEND}/publicatie/{test-wizard-app-id}
+```
+
+For existing applications (e.g., well-known apps with many standards), search the API:
+```
+curl -s -u {ADMIN_USER}:{ADMIN_PASS} '{BACKEND}/index.php/apps/openregister/api/objects/3/25?_limit=5&_search=Begraven&_fields=naam,id'
+```
+
+### Applicatie Detail Page
+- Navigate to `{FRONTEND}/publicatie/{applicatie-id}` for your wizard-created app AND at least one existing app
+- **Tabs**: Verify all tabs load (Beschrijving, Diensten, Koppelingen, Standaarden, Gebruik, Versies)
+- **Tab loading**: Check that tabs load consistently without delays (#351)
+- **Tab titles**: Verify tab titles match the design specification (#248)
+- **Diensten tab**: Verify linked services are shown (not empty) (#373)
+- **Standaarden tab**: Check standards display correctly, no UUIDs (#371, #374)
+- **Compliance**: Verify compliance display is consistent (#379), counts match (#380)
+- **Contactpersoon**: Verify contact person shows full name including tussenvoegsel (#372)
+- **Gebruik tab**: When NOT logged in, municipality names should NOT be visible (#263)
+- **Versies**: Check version display, especially for SaaS applications (#375)
+- **Standaarden na wijzigen**: After editing standards, verify they haven't changed unexpectedly (#378)
+- **Compliancy link**: Click a compliancy link and verify it works (#382)
+- Take screenshots of EACH tab
+
+### Koppeling Detail Page
+- Navigate to `{FRONTEND}/publicatie/{koppeling-id}` for your wizard-created koppeling
+- **Card display**: Verify the card shows meaningful data, not empty (#401)
+- **Direction**: Check that the connection direction (richting) is displayed
+- **Linked applications**: Verify both source and target applications are shown
+- **Name**: Verify the connection has a proper name (#312)
+
+### Dienst Detail Page
+- Navigate to `{FRONTEND}/publicatie/{dienst-id}` for your wizard-created dienst
+- **Beschrijving tab**: Verify description tab exists and shows content (#408)
+- **Labels**: Check that "Diensttype" vs "Type" is used consistently (#357)
+- **Status**: Verify "Concept" status is not shown in unintended places (#358)
+- **Contactpersoon**: Verify tussenvoegsel is shown in names (#356)
+- **Array display**: Check that fields don't show raw arrays (#347)
+
+### Organisatie Detail Page
+- Navigate to `{FRONTEND}/publicatie/{organisatie-id}` for "Test Leverancier BV"
+- **Profile fields**: Verify all profile fields are shown correctly
+- **Contactpersonen**: Check that linked contact persons are displayed
+- **Type**: Verify the organization type (Leverancier/Gemeente/Samenwerking) is shown
+- **Status**: Check status display (Concept/Actief/Inactief)
+
+## Wizard Walkthrough — MANDATORY
+
+**CRITICAL**: You MUST execute all four wizard flows below BEFORE testing individual issues. Many issues depend on having wizard-created objects. Execute each wizard completely, documenting every step, every field, and every button click. Take a screenshot after each step.
+
+### Wizard 1: Applicatie publiceren
+
+**Route**: Click **"Applicatie publiceren"** on the dashboard (or navigate to `/forms/applicatie?type=eigen` — NOTE: do NOT use `/beheer/forms/...` as it causes a 500 error)
+
+**Step 1 — Applicatie-informatie:**
+1. Fill in field **naam**: `Test Wizard App`
+2. Fill in field **website**: `https://test-leverancier.nl/app`
+3. Fill in field **beschrijvingKort**: `Applicatie aangemaakt via wizard test`
+4. Fill in field **beschrijvingLang**: `Dit is een uitgebreide beschrijving van de test applicatie, aangemaakt door de geautomatiseerde leverancier test.`
+5. Skip **logo** (optional)
+6. If a **contactpersoon** dropdown is visible, select "Jan Pietersen" if available
+7. Click **"Volgende"** to advance
+8. Take screenshot: `wizard-app-step1.png`
+
+**Step 2 — Licentie & Hosting:**
+1. Select **licentietype**: "Open source"
+2. If a **licentie** dropdown appears, select any option (e.g., "EUPL-1.2")
+3. Select **cloudDienstverleningsmodel**: check "Software-as-a-Service"
+4. If **hostingLocatie** appears, select any option
+5. Click **"Volgende"**
+6. Take screenshot: `wizard-app-step2.png`
+
+**Step 3 — Referentiecomponenten:**
+1. In the multi-select dropdown, search for and select 1-2 GEMMA referentiecomponenten (e.g., type "Zaak" and select the first result)
+2. Click **"Volgende"**
+3. Take screenshot: `wizard-app-step4.png`
+
+**Step 4 — Standaarden:**
+1. Observe the standards table that loaded from referentiecomponenten
+2. If standards are listed, check the **"Compliant"** checkbox on the first one
+3. Note whether the "Bewijs" upload is enabled when compliant is checked
+4. Click **"Volgende"**
+5. Take screenshot: `wizard-app-step5.png`
+
+**Step 5 — Koppelingen:**
+1. Click **"+ Koppeling toevoegen"** to add a connection
+2. In the **Applicatie B** dropdown, search for and select any application
+3. Select **Richting**: "Bi-directioneel" (options are "A -> B", "B -> A", "Bi-directioneel")
+4. Fill in **Naam**: `Test koppeling`
+5. Click **"Volgende"**
+6. Take screenshot: `wizard-app-step6.png`
+
+**Step 6 — Controleren (Review):**
+1. Verify ALL entered data is shown correctly:
+ - Application name, website, descriptions
+ - License type and hosting model
+ - Referentiecomponenten selection
+ - Standards compliance
+ - Koppelingen
+2. Take screenshot: `wizard-app-step6-review.png`
+3. Click **"Applicatie aanmelden"** to submit (button label varies per wizard)
+4. Verify success notification appears: "Applicatie succesvol aangemeld!"
+5. Take screenshot: `wizard-app-success.png`
+
+### Wizard 2: Dienst publiceren
+
+**Route**: Click **"Dienst publiceren"** on the dashboard (or navigate to `/forms/dienst?type=eigen`)
+
+**Step 1 — Applicaties:**
+1. In the dropdown, search for and select "Test Wizard App" (the app you created above)
+2. Click **"Volgende"**
+3. Take screenshot: `wizard-dienst-step1.png`
+
+**Step 2 — Dienst-informatie:**
+1. Fill in **naam**: `Test Wizard Dienst`
+2. Fill in **website**: `https://test-leverancier.nl/dienst`
+3. Fill in **beschrijvingKort**: `Dienst aangemaakt via wizard test`
+4. Select **diensttype**: "Implementatieondersteuning" (or any available option)
+5. Click **"Volgende"**
+6. Take screenshot: `wizard-dienst-step2.png`
+
+**Step 3 — Controleren:**
+1. Verify all data
+2. Take screenshot: `wizard-dienst-step3.png`
+3. Click **"Dienst registreren"**
+4. Verify success: "Dienst succesvol aangemeld!"
+
+### Wizard 3: Koppeling publiceren
+
+**Route**: Click **"Koppeling publiceren"** on the dashboard (or navigate to `/forms/koppeling?type=eigen-organisatie`)
+
+**Step 1 — Koppeling zoeken (Applicatie selectie):**
+1. Select an applicatie from the dropdown (e.g., "Test Wizard App")
+2. The page shows existing koppelingen for the selected app
+3. Take screenshot: `wizard-koppeling-step1.png`
+4. Click **"Volgende"**
+
+**Step 2 — Koppeling details:**
+1. **Applicatie A** is pre-filled and locked (Test Wizard App)
+2. Select **Richting**: "Bi-directioneel" (options: "A -> B", "B -> A", "Bi-directioneel")
+3. In **Applicatie B**, search for and select another application (e.g., "DigiD")
+4. Fill in **Naam**: `Test Wizard Koppeling`
+5. Click **"Volgende"**
+6. Take screenshot: `wizard-koppeling-step2.png`
+
+**Step 3 — Aanvullende informatie:**
+1. Fill in **beschrijvingKort**: `Koppeling aangemaakt via wizard test`
+2. Skip optional fields (lange beschrijving, standaardversies, transportprotocol, intermediair)
+3. Click **"Volgende"**
+4. Take screenshot: `wizard-koppeling-step3.png`
+
+**Step 4 — Controleren:**
+1. Verify all data
+2. Take screenshot: `wizard-koppeling-step4.png`
+3. Click **"Opslaan"**
+4. Verify success: "Koppelingen succesvol opgeslagen!"
+
+### Wizard 4: Applicatiegebruik melden
+
+**Route**: Click **"Applicatiegebruik melden"** on the dashboard (or navigate to `/forms/gebruik/applicatie?type=ontbrekend-organisatie`)
+
+**Step 1 — Selecteren:**
+1. In the **Applicatie** dropdown, select one of your published applications (e.g., "Test Wizard App")
+2. In the **Klant(en)** multi-select dropdown, search for and select one or more municipalities/samenwerkingen (e.g., "Amsterdam")
+3. Click **"Volgende"**
+4. Take screenshot: `wizard-gebruik-voorstellen-step1.png`
+
+**Step 2 — Controleren:**
+1. Verify the overview: applicatie name, selected klant(en)
+2. Note the informational alert about visibility
+3. Take screenshot: `wizard-gebruik-voorstellen-step2.png`
+4. Click **"Verzenden"**
+5. Verify success: "Gebruik succesvol geregistreerd!"
+6. Note the explanation that the klant must approve before it becomes definitive
+7. Take screenshot: `wizard-gebruik-voorstellen-success.png`
+
+### After Wizards: Verify Created Objects and Open Detail Pages
+
+After completing all four wizards:
+1. Navigate to `/beheer/applicaties` — verify "Test Wizard App" appears in the table
+2. Navigate to `/beheer/diensten` — verify "Test Wizard Dienst" appears
+3. Navigate to `/beheer/koppelingen` — verify "Test Wizard Koppeling" appears
+4. Take screenshots of each table showing the created objects
+5. **Find the IDs** of each created object:
+ - Use the API: `curl -s -u {ADMIN_USER}:{ADMIN_PASS} '{BACKEND}/index.php/apps/openregister/api/objects/3/25?_search=Test+Wizard+App&_fields=naam,id&_limit=3'`
+ - Or click the row in the beheer table and note the ID from the URL/detail panel
+6. **Open the detail page** for each created object:
+ - Navigate to `{FRONTEND}/publicatie/{applicatie-id}` for the app
+ - Navigate to `{FRONTEND}/publicatie/{dienst-id}` for the dienst
+ - Navigate to `{FRONTEND}/publicatie/{koppeling-id}` for the koppeling
+7. On EACH detail page, test the tabs and content as described in the "Detail Page Testing" section above
+8. Take screenshots of each detail page and each tab
+
+---
+
+## Testing Hints for Specific Issues
+
+1. **#399 (cross-vendor version access)**: This bug is now **FIXED** (ModuleVersionService registered in DI). Go to the public search page `/zoeken?_page=1`. Find "Test Applicatie Leverancier 2" (from the other vendor), click it, go to the Versies tab, click on a version. Verify no error appears. If no Versies tab is visible, check the API: `curl -s -u {ADMIN_USER}:{ADMIN_PASS} '{BACKEND}/index.php/apps/openregister/api/objects/3/25?_search=Test+Applicatie+Leverancier+2&_extend=versies'`
+2. **#375 (SaaS version)**: This bug is now **FIXED** — SaaS apps get a default 1.0.0 version automatically. After creating the wizard app, open its detail page at `{FRONTEND}/publicatie/{id}`, check the Versies tab. A "1.0.0" default version should exist. Also verify via API: `curl -s -u {PERSONA_USERNAME_URLENCODED}:{PERSONA_PASSWORD} '{BACKEND}/index.php/apps/openregister/api/objects/3/25?_search=Test+Wizard+App&_extend=versies&_fields=naam,versies'`
+3. **#105 (RBAC)**: Navigate to `/beheer/applicatielandschappen` — it should ONLY show your own org's applications. The test is about **data scoping** (own org only), not page visibility.
+4. **#352 (Mijn Account)**: Navigate to `/account` (or find the "Mijn Account" link in the header/menu) to check contact person data. Note: `/mijn-account` now redirects to `/account`.
+5. **#364/#365 (contactpersonen)**: Navigate to `/beheer/contactpersonen` — Jan Pietersen should be listed. Click edit on a contact person to test #365.
+6. **#402 (Edge vs Chrome)**: **SKIP** — untestable (single browser engine).
+7. **#403 (delete dialog)**: In the applicaties table, click delete on "Test Wizard App", verify the dialog text, then click **Cancel** (don't actually delete).
+8. **#15 (export)**: In the applicaties table, click the **"Acties"** dropdown button, then hover/click **"Exporteren"**, then click **"Als CSV"**. Verify a file downloads. Also test "Als Excel". The export bug (#355) is **FIXED** — exports now return HTTP 200 with proper data including resolved names for UUID columns.
+9. **#141 (merge)**: Not for this persona — tested by functioneel-beheerder via Nextcloud backend.
+10. **#348 (Centric Begraven standaarden)**: Find "Centric Begraven" by searching the API:
+ ```
+ curl -s -u {ADMIN_USER}:{ADMIN_PASS} '{BACKEND}/index.php/apps/openregister/api/objects/3/25?_search=Begraven&_fields=naam,id&_limit=5'
+ ```
+ Then navigate to `{FRONTEND}/publicatie/{id}` and check the Standaarden tab — verify the standard count matches between the tab header badge and the actual list.
+11. **Detail page issues (#248, #351, #371-#378, #380, #382, #385, #408)**: These ALL require opening detail pages. After the wizards, use the IDs from step "After Wizards" above and navigate to `{FRONTEND}/publicatie/{id}`. Test EACH tab on the detail page systematically. Do NOT skip this — it covers 20+ issues.
+
+### PowerPoint comparison issues — How to test WITHOUT a PowerPoint
+
+Issues #294, #359, #360, #361, #362, #363, #376, #386, #387, #390 were previously CANNOT_TEST because they reference a PowerPoint presentation for label comparison. You do NOT need the PowerPoint. Instead, test these by **reading the actual text** on the wizard pages and comparing it against the acceptance criteria in `issues.md`. Use `browser_snapshot` to capture all text on each wizard step.
+
+12. **#294 (uitlijning rechthoek)**: During the Applicatie wizard, at the Referentiecomponenten step, observe whether the selection area/rectangle is properly aligned. Take a screenshot and check visual alignment. Use `browser_snapshot` to capture the page structure.
+
+13. **#359 (dienst wizard tekst aanpassen)**: Re-run the dienst wizard (navigate to `/forms/dienst?type=eigen`). At each step, use `browser_snapshot` to read ALL text including:
+ - Step headers and subtitles
+ - Form field labels
+ - Tooltip text (hover over `i` icons using `browser_hover`)
+ - Button labels ("Volgende", "Dienst registreren")
+ Compare against the acceptance criteria in `issues.md` for #359.
+
+14. **#360 (dienst wizard tooltip `i` icons)**: During the dienst wizard, at each step:
+ 1. Use `browser_snapshot` to find all `i` (info) icons
+ 2. Hover over each `i` icon using `browser_hover` and take a screenshot
+ 3. Read the tooltip text — compare against `issues.md` #360 criteria
+ 4. Check: Do all fields that should have an `i` icon actually have one?
+
+15. **#361 (dienst wizard label inconsistentie)**: During the dienst wizard:
+ 1. At the input step, use `browser_snapshot` to record all field labels (e.g., "Naam", "Website", "Korte omschrijving")
+ 2. Advance to the review/controleren step
+ 3. Use `browser_snapshot` to record all labels on the review
+ 4. Compare: Do the review labels match the input labels exactly? (e.g., "Naam" in input → "Naam" in review, not "Dienstnaam")
+
+16. **#362 (dienst wizard header text)**: At the success/confirmation page of the dienst wizard:
+ 1. Use `browser_snapshot` to capture the full page text
+ 2. Check the header text — does it make logical sense? (e.g., should say "Dienst registreren" not "Uw dienst publiceren" if it's not published yet)
+
+17. **#363 (catalogus vs softwarecatalogus)**: At the success page of the dienst wizard:
+ 1. Use `browser_snapshot` and search for the word "catalogus"
+ 2. Verify it says "softwarecatalogus" (full name), not just "catalogus"
+ 3. Check all wizard steps for this consistency
+
+18. **#376 (wizard vs table labels)**: After completing the Applicatie wizard:
+ 1. Use `browser_snapshot` on the last wizard step (review) — note all field labels
+ 2. Navigate to `/beheer/applicaties` — use `browser_snapshot` to read all column headers
+ 3. Compare: Do the wizard field labels match the table column headers? E.g., if the wizard says "Korte omschrijving", the table should also say "Korte omschrijving" (not "Beschrijving")
+
+19. **#386 (applicatie wizard andere labels)**: During the Applicatie wizard (`/forms/applicatie?type=eigen`):
+ 1. At each step, use `browser_snapshot` to capture all field labels
+ 2. Compare against `issues.md` #386 acceptance criteria
+ 3. Focus on: Are the step titles correct? Do form labels match expected naming?
+
+20. **#387 (applicatie wizard tooltip `i` missing)**: During the Applicatie wizard:
+ 1. At each step, use `browser_snapshot`
+ 2. Check which fields have an `i` (info) icon next to them
+ 3. Hover over each `i` icon with `browser_hover` → take screenshot
+ 4. List which fields have `i` icons and which don't
+ 5. Compare against `issues.md` #387 — which fields SHOULD have tooltip icons?
+
+21. **#390 (applicatie wizard review labels)**: During the Applicatie wizard:
+ 1. On input steps, record all field labels using `browser_snapshot`
+ 2. On the final review/controleren step, record all displayed labels
+ 3. Compare: Do the review labels match the input labels?
+ 4. Example mismatch: input says "Clouddienstverleningsmodel", review says "Cloud" — this is a label inconsistency
+
+### Standards and compliance testing
+
+22. **#378 (standaarden change after edit)**: This requires a before/after comparison:
+ 1. Navigate to the detail page of "Test Wizard App" at `{FRONTEND}/publicatie/{id}`
+ 2. Go to the **Standaarden** tab — use `browser_snapshot` to record the exact list of standards and their compliance status. **Save this as the "before" state.**
+ 3. Navigate to `/beheer/applicaties`, click **Acties** → **Bewerken** on "Test Wizard App"
+ 4. In the wizard, advance to the Standards step — make a small change (e.g., toggle one compliance checkbox)
+ 5. Complete the wizard (save the edit)
+ 6. Navigate back to the detail page → Standaarden tab
+ 7. Use `browser_snapshot` to record the standards list. **Compare with the "before" state.**
+ 8. Check: Did any standards disappear or change unexpectedly? Did only your intended change take effect?
+
+23. **#380 (compliance counts mismatch)**: On an application detail page with standards:
+ 1. Note the **tab badge count** on the "Standaarden" tab (e.g., "Standaarden (45)")
+ 2. Click the Standaarden tab
+ 3. Count the actual number of standards listed in the table (use `browser_snapshot` and count rows)
+ 4. Compare: Does the badge count (45) match the actual number of rows?
+ 5. Also check subcategories: count "Verplicht", "Aanbevolen", "Toegevoegd" separately and verify they add up to the total
+ 6. Test on both "Test Wizard App" and a well-known app like "Centric Begraven"
+
+### Koppeling and data issues
+
+24. **#401 (empty koppeling cards)**: Navigate to an **imported** koppeling detail page (not one you created via wizard):
+ 1. Find imported koppelingen via API: `curl -s -u {PERSONA_USERNAME_URLENCODED}:{PERSONA_PASSWORD} '{BACKEND}/index.php/apps/openregister/api/objects/3/28?_limit=10&_fields=naam,id,applicatieA,applicatieB'`
+ 2. Navigate to `{FRONTEND}/publicatie/{koppeling-id}` for an imported koppeling
+ 3. Verify the card shows: naam, applicatie A name, applicatie B name, richting, beschrijving
+ 4. If the card is mostly empty (shows only UUID or blank fields), mark as FAIL
+ 5. Compare with your wizard-created koppeling — does it show the same fields?
+
+25. **#391 (existing org user test)**: This tests adding a user to an already-registered organization:
+ 1. This requires the functioneel-beheerder to have created a second user for "Test Leverancier BV" organization beforehand
+ 2. If a second leverancier user exists (check `issues.md` for setup), log in with that user
+ 3. Verify they see the same organization data as Jan Pietersen
+ 4. If no second user exists, mark as **BLOCKED** (not CANNOT_TEST) with note: "Requires functioneel-beheerder to create a second user for Test Leverancier BV"
+
+## Instructions
+
+When running tests for this persona:
+1. Navigate to `{FRONTEND}/login`
+2. Log in with `{PERSONA_USERNAME}` / `{PERSONA_PASSWORD}`
+3. **FIRST**: Execute ALL FOUR wizard walkthroughs above (applicatie, dienst, koppeling, applicatiegebruik melden). This is mandatory.
+4. After wizards complete, verify created objects in beheer tables
+5. **THEN**: Test each issue from the Issues to Test table, using the acceptance criteria from `issues.md`
+6. For wizard-related issues (#294, #274, #306-#308, #312, #314, #354-#363, #368-#369, #376-#378, #380, #383-#390, #407, #408): test during or immediately after the relevant wizard execution
+7. For use-reporting issues (#8, #10, #54): test during or after the applicatiegebruik melden wizard (Wizard 4)
+7. Check that vendor-specific data (customer lists) is private
+8. Write results to `test-results/leverancier/results-authenticated.md`
+9. For each issue, list which acceptance criteria passed and which failed
+
+## Rules
+
+- **READ ONLY on GitHub issues** — never update, close, or comment on issues
+- Write test results ONLY to local files in the `test-results/` directory
+- Take screenshots for evidence where applicable
diff --git a/.claude/skills/test-samenwerking.md b/.claude/skills/test-samenwerking.md
new file mode 100644
index 00000000..43b063a7
--- /dev/null
+++ b/.claude/skills/test-samenwerking.md
@@ -0,0 +1,78 @@
+# Test Agent: Samenwerking (Collaboration)
+
+## Persona
+
+**Linda Bakker** — Coordinator at a municipal collaboration (samenwerkingsverband), 12 years experience.
+
+## Role: Gebruik-beheerder
+
+Linda represents a collaboration that acts as BOTH a supplier (offering shared services to member municipalities) AND a consumer (using software on behalf of members). She manages membership, shared licenses, and collective procurement.
+
+## Login Credentials
+
+- **Username**: `{PERSONA_USERNAME}` (default: `linda.bakker@test.nl`)
+- **Password**: `{PERSONA_PASSWORD}` (default: `WelcomeToTest2026`)
+- **Groups**: gebruik-beheerder, software-catalog-users
+
+> These values are injected by the orchestrator. If not provided, use the defaults above (local dev only).
+
+## Test Environment
+
+- **Frontend**: `{FRONTEND}` (default: `{FRONTEND}`)
+- **Backend**: `{BACKEND}` (default: `{BACKEND}`)
+- **Browser**: Use Playwright MCP browser tools (prefixed `mcp__browser-N__`, where N is assigned by the orchestrator)
+- **Login URL**: `{FRONTEND}/login`
+
+## Test Scope
+
+### Primary Steps
+- **Step 2**: Organization registration — Register as a samenwerking
+- **Step 6**: Organization profile — Set up collaboration profile, define member municipalities
+- **Step 10**: Usage reporting — Register usage on behalf of member municipalities
+- **Step 11**: Connection wizard — Register connections for shared infrastructure
+- **Step 20**: Collaborations and multi-org management — Core functionality for this persona
+
+### Secondary Steps (observe/verify)
+- **Step 5**: Colleague invitations — Manage users across the collaboration
+- **Step 7/8**: Product creation — Create shared products/solutions
+- **Step 12**: Privacy — Verify collaboration-specific visibility rules
+- **Step 17**: "Gluren bij de buren" — Compare member municipalities
+
+## Issues to Test
+
+### Previously tested (re-verify with auth):
+| Issue | Title | Previous Status |
+|-------|-------|-----------------|
+| #57 | Pakketten opvoeren voor samenwerkingsverband | PARTIAL |
+
+### New issues (not previously tested):
+| Issue | Title | Test Step |
+|-------|-------|-----------|
+| #186 | Koppelingen | Step 11 |
+
+## Acceptance Criteria Reference
+
+**IMPORTANT**: Before testing each issue, read its detailed acceptance criteria in `issues.md` (in the repository root). Each issue has specific, testable acceptance criteria with checkboxes. Use these criteria to determine PASS/FAIL/PARTIAL status:
+- **PASS** = ALL acceptance criteria are met
+- **PARTIAL** = Some criteria met, some not
+- **FAIL** = Key criteria not met or feature is broken
+- **CANNOT_TEST** = Feature not accessible or environment issue prevents testing
+
+## Instructions
+
+When running tests for this persona:
+1. Navigate to `{FRONTEND}/login`
+2. Log in with `{PERSONA_USERNAME}` / `{PERSONA_PASSWORD}`
+3. **For each issue**: Read the acceptance criteria in `issues.md`, then test each criterion
+4. Focus on the dual-role nature: both supplier AND consumer
+5. Test multi-organization management thoroughly (Step 20)
+6. Verify that member municipalities' data is correctly scoped
+7. Test bulk operations and collective license management
+8. Write results to `test-results/samenwerking/results-authenticated.md`
+9. For each issue, list which acceptance criteria passed and which failed
+
+## Rules
+
+- **READ ONLY on GitHub issues** — never update, close, or comment on issues
+- Write test results ONLY to local files in the `test-results/` directory
+- Take screenshots for evidence where applicable
diff --git a/.claude/skills/test-security-officer.md b/.claude/skills/test-security-officer.md
new file mode 100644
index 00000000..64c0d84d
--- /dev/null
+++ b/.claude/skills/test-security-officer.md
@@ -0,0 +1,137 @@
+# Test Agent: Security Officer
+
+## Persona
+
+**Mark Jansen** — Information Security Officer, 10 years cybersecurity, 5 years municipal ICT.
+
+## Role: Security Focus
+
+Mark monitors security requirements, validates privacy implementations, and ensures access control boundaries are respected.
+
+## Login Credentials
+
+- **Username**: `{PERSONA_USERNAME}` (default: `mark.jansen@test.nl`)
+- **Password**: `{PERSONA_PASSWORD}` (default: `WelcomeToTest2026`)
+- **Groups**: gebruik-beheerder, software-catalog-users
+
+> These values are injected by the orchestrator. If not provided, use the defaults above (local dev only).
+
+## Test Environment
+
+- **Frontend**: `{FRONTEND}` (default: `{FRONTEND}`)
+- **Backend**: `{BACKEND}` (default: `{BACKEND}`)
+- **Browser**: Use Playwright MCP browser tools (prefixed `mcp__browser-N__`, where N is assigned by the orchestrator)
+- **Login URL**: `{FRONTEND}/login`
+
+## Test Scope
+
+### Primary Steps
+- **Step 3**: Organization activation — Verify password management security
+- **Step 4**: First login — Verify session management, logout behavior
+- **Step 5**: Colleague invitations — Verify password only set via backend
+- **Step 12**: Privacy and visibility — CORE TEST: Comprehensive privacy validation
+
+### Security Test Scenarios
+
+#### RBAC Verification
+- [ ] Unauthenticated users cannot see **gemeente/samenwerking** contactpersonen (leverancier contacts ARE expected to be public via publications)
+- [ ] Unauthenticated users cannot access admin endpoints
+- [ ] Aanbod-beheerder cannot see other vendor's customers
+- [ ] Gebruik-beheerder cannot see other municipality's usage
+- [ ] Deactivated users cannot log in
+
+#### Privacy Verification
+- [ ] Gemeente/samenwerking contactpersonen NOT publicly visible (#394) — but leverancier contacts ARE expected to be visible
+- [ ] Usage data scoped to own organization
+- [ ] API endpoints enforce same rules as UI
+- [ ] Direct URL access to restricted resources returns 403/404
+
+#### RBAC Reference
+The authoritative RBAC rules are in `softwarecatalog/lib/Settings/softwarecatalogus_register.json`. Each schema has an `authorization` block. Key rules:
+- **contactpersoon**: NOT public read. Leverancier contacts visible via publications only. Gemeente/samenwerking contacts should be hidden.
+- **module** (applicatie): Public can read only where `geregistreerdDoor: Leverancier`. Aanbod-beheerder sees own org only.
+- **koppeling**: NOT public. Gebruik-beheerder sees all; aanbod-beheerder sees own org only.
+- **gebruik**: NOT public. Gebruik-beheerder sees all; aanbod-beheerder sees own org only.
+- **organisatie**: Public readable by everyone.
+
+## Issues to Test
+
+### Previously tested (re-verify with auth):
+| Issue | Title | Previous Status |
+|-------|-------|-----------------|
+| #394 | Contactpersonen van gemeenten publiekelijk zichtbaar | FAIL (note: only gemeente contacts should be hidden; leverancier contacts ARE expected to be public) |
+| #183 | Wachtwoord vergeten optie | PARTIAL |
+| #404 | Regelmatig witte schermen | CANNOT_TEST → **re-test (see hint #2)** |
+| #395 | Menu linkerkant verdwijnt | CANNOT_TEST |
+| #409 | Footer anders: inlog of uitgelogd | PARTIAL |
+| #406 | SiteImprove verwijderen | PARTIAL |
+| #105 | Aanbieders zien applicatielandschappen en koppelingen niet | MOVED → leverancier agent (requires aanbod-beheerder role) |
+
+### New issues (not previously tested):
+| Issue | Title | Test Step |
+|-------|-------|-----------|
+| #85 | (VNGR) Publieke API toegang tot aanbodinformatie | Step 12 |
+| #315 | Hoge prioriteit: Zoekpagina toont deel gemeentelijk applicatielandschap | Step 14 |
+| #447 | Zoeken: concept leverancier zonder VNG triage direct vindbaar | Step 3 |
+| #455 | Tabblad koppelingen en contactpersonen publiekelijk niet getoond — RBAC? | Step 12 |
+
+## Testing Hints for Specific Issues
+
+1. **#395 (Menu linkerkant verdwijnt)**: This issue is about the left sidebar disappearing after pressing F5/Ctrl+R. It may be caused by a **narrow browser viewport** — the sidebar collapses on small screens. Test as follows:
+ 1. First, **resize the browser** to a wide viewport: use `browser_resize` with width **1920** and height **1080**
+ 2. Navigate to `{FRONTEND}/beheer/applicaties` (or any beheer page)
+ 3. Verify the left navigation menu is visible (with links like Applicaties, Diensten, Koppelingen, etc.)
+ 4. Press **F5** (use `browser_press_key` with key "F5") to refresh the page
+ 5. Check if the left menu is still visible after refresh
+ 6. Repeat on other beheer pages: `/beheer/diensten`, `/beheer/koppelingen`
+ 7. Also test by navigating directly to the URL (not via SPA navigation) — paste the URL and press Enter
+ 8. Take screenshots before and after the refresh
+ 9. If the menu disappears, try with different viewport widths (1280, 1024) to see if it's viewport-related
+
+2. **#404 (Regelmatig witte schermen)**: Previously CANNOT_TEST because white screens are intermittent. To reproduce, try these scenarios:
+ 1. **Rapid navigation**: Navigate quickly between pages without waiting for full load:
+ - Click `/beheer/applicaties` → immediately click `/beheer/diensten` → immediately click `/beheer/koppelingen`
+ - After each rapid navigation sequence, check if the page renders or shows a blank white screen
+ 2. **Direct URL access**: Navigate directly to deep URLs without going through the SPA:
+ - Paste `{FRONTEND}/beheer/applicaties` in the URL bar and press Enter
+ - Paste `{FRONTEND}/publicatie/{any-id}` and press Enter
+ - Check if the page loads or shows a white screen
+ 3. **Browser refresh (F5)**: On various pages, press F5 to refresh:
+ - Refresh on `/beheer/applicaties`, `/beheer/diensten`, `/zoeken`
+ - Check if the page reloads correctly or shows a white screen
+ 4. **Console errors**: After each white screen attempt, check `browser_console_messages` for JavaScript errors that might indicate the root cause
+ 5. If you cannot reproduce the white screen after 5-10 attempts, mark as **PASS** with note: "White screen not reproducible in automated testing at [date]"
+
+## Acceptance Criteria Reference
+
+**IMPORTANT**: Before testing each issue, read its detailed acceptance criteria in `issues.md` (in the repository root). Each issue has specific, testable acceptance criteria with checkboxes. Use these criteria to determine PASS/FAIL/PARTIAL status:
+- **PASS** = ALL acceptance criteria are met
+- **PARTIAL** = Some criteria met, some not
+- **FAIL** = Key criteria not met or feature is broken
+- **CANNOT_TEST** = Feature not accessible or environment issue prevents testing
+
+## Instructions
+
+When running tests for this persona:
+1. Navigate to `{FRONTEND}/login`
+2. Log in with `{PERSONA_USERNAME}` / `{PERSONA_PASSWORD}`
+3. ALSO test as unauthenticated user (incognito) to verify public access restrictions
+4. **For each issue**: Read the acceptance criteria in `issues.md`, then test each criterion
+5. Test each role transition — log out fully between switches
+6. Try accessing resources you should NOT have access to
+7. Check API responses (DevTools → Network) for data leakage
+8. Pay special attention to #394 — verify that **leverancier** contact persons ARE visible (expected), but **gemeente/samenwerking** contact persons are NOT visible publicly
+9. **IMPORTANT for #394**: You are logged in as gebruik-beheerder, which has read access to all contact persons. To test PUBLIC visibility, you MUST test the API **without authentication** (use `curl` without `-u` flag, or open an incognito/private browser window). If you only test while logged in, you will get a false positive — seeing data does NOT mean it's publicly exposed.
+10. Test the local API **both authenticated and unauthenticated**:
+ - Authenticated: `{BACKEND}/index.php/apps/opencatalogi/api/publications?_extend=contactpersonen` (via browser)
+ - Unauthenticated: use `curl` without auth: `curl '{BACKEND}/index.php/apps/opencatalogi/api/publications?_schema=organisatie&_extend[]=contactpersonen'`
+ - Compare results — gemeente contacts should only appear in the authenticated response
+11. Document findings with severity: CRITICAL / HIGH / MEDIUM / LOW
+12. Write results to `test-results/security-officer/results-authenticated.md`
+13. For each issue, list which acceptance criteria passed and which failed
+
+## Rules
+
+- **READ ONLY on GitHub issues** — never update, close, or comment on issues
+- Write test results ONLY to local files in the `test-results/` directory
+- Take screenshots for evidence where applicable
diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml
new file mode 100644
index 00000000..2c4a305e
--- /dev/null
+++ b/.github/workflows/code-quality.yml
@@ -0,0 +1,70 @@
+name: Code Quality
+
+on:
+ pull_request:
+ branches: [main, master, development]
+
+concurrency:
+ group: quality-${{ github.head_ref || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ php-checks:
+ name: ${{ matrix.check.name }}
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ check:
+ - { name: "PHP Lint", command: "composer lint" }
+ - { name: "PHPCS", command: "./vendor/bin/phpcs --standard=phpcs.xml" }
+ - { name: "PHPMD", command: "./vendor/bin/phpmd lib text phpmd.xml" }
+ - { name: "Psalm", command: "./vendor/bin/psalm --threads=1 --no-cache --output-format=github" }
+ - { name: "PHPStan", command: "./vendor/bin/phpstan analyse --memory-limit=1G" }
+ - { name: "PHPUnit", command: "./vendor/bin/phpunit --colors=always" }
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.1'
+ extensions: mbstring, xml, ctype, iconv, intl, dom, filter, gd, json, posix, zip, soap
+ tools: composer:v2
+
+ - name: Cache Composer dependencies
+ uses: actions/cache@v4
+ with:
+ path: vendor
+ key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
+ restore-keys: ${{ runner.os }}-composer-
+
+ - name: Install dependencies
+ run: composer install --no-progress --prefer-dist --optimize-autoloader
+
+ - name: ${{ matrix.check.name }}
+ run: ${{ matrix.check.command }}
+ frontend-quality:
+ name: Frontend Quality
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: ESLint
+ run: npm run lint
+
+ - name: Stylelint
+ run: npm run stylelint
diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml
new file mode 100644
index 00000000..69aa6b2e
--- /dev/null
+++ b/.github/workflows/documentation.yml
@@ -0,0 +1,66 @@
+name: Documentation
+
+on:
+ push:
+ branches:
+ - development
+ pull_request:
+ branches:
+ - development
+
+jobs:
+ deploy:
+ name: Deploy Documentation
+ runs-on: ubuntu-latest
+ if: github.event_name == 'push'
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup Node.js 18
+ uses: actions/setup-node@v3
+ with:
+ node-version: '18'
+
+ - name: Clear build cache and install dependencies
+ timeout-minutes: 3
+ run: |
+ cd docusaurus
+ rm -rf node_modules/.cache
+ rm -rf .docusaurus
+ rm -rf build
+ npm run ci
+
+ - name: Verify build output
+ run: |
+ cd docusaurus/build
+ if [ ! -f index.html ]; then
+ echo "ERROR: index.html not found in build directory!"
+ exit 1
+ fi
+
+ - name: Create .nojekyll and CNAME files
+ run: |
+ cd docusaurus/build
+ touch .nojekyll
+ echo "softwarecatalog.app" > CNAME
+
+ - name: Deploy to GitHub Pages
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ./docusaurus/build
+ publish_branch: gh-pages
+ user_name: 'github-actions[bot]'
+ user_email: 'github-actions[bot]@users.noreply.github.com'
+ force_orphan: false
+ allow_empty_commit: true
+ keep_files: false
+
+ - name: Verify deployment
+ run: |
+ git fetch origin gh-pages
+ echo "Deployment completed. Latest commit: $(git rev-parse origin/gh-pages)"
diff --git a/.gitignore b/.gitignore
index 373d1423..e1bf2dab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,12 @@
# Data files
/data/
*.csv
+
+# Test artifacts (generated per run — only .md results are committed)
+/test-results/**/*.json
+/test-results/**/*.html
+/test-results/**/*.png
+/reacties/screenshots/
+
+# Sync timestamp (local only)
+.last-update
diff --git a/LICENSE b/LICENSE
index 261eeb9e..6d8cea43 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,201 +1,190 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+EUROPEAN UNION PUBLIC LICENCE v. 1.2
+EUPL © the European Union 2007, 2016
+
+This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined below) which is provided under the
+terms of this Licence. Any use of the Work, other than as authorised under this Licence is prohibited (to the extent such
+use is covered by a right of the copyright holder of the Work).
+The Work is provided under the terms of this Licence when the Licensor (as defined below) has placed the following
+notice immediately following the copyright notice for the Work:
+ Licensed under the EUPL
+or has expressed by any other means his willingness to license under the EUPL.
+
+1.Definitions
+In this Licence, the following terms have the following meaning:
+— ‘The Licence’:this Licence.
+— ‘The Original Work’:the work or software distributed or communicated by the Licensor under this Licence, available
+as Source Code and also as Executable Code as the case may be.
+— ‘Derivative Works’:the works or software that could be created by the Licensee, based upon the Original Work or
+modifications thereof. This Licence does not define the extent of modification or dependence on the Original Work
+required in order to classify a work as a Derivative Work; this extent is determined by copyright law applicable in
+the country mentioned in Article 15.
+— ‘The Work’:the Original Work or its Derivative Works.
+— ‘The Source Code’:the human-readable form of the Work which is the most convenient for people to study and
+modify.
+— ‘The Executable Code’:any code which has generally been compiled and which is meant to be interpreted by
+a computer as a program.
+— ‘The Licensor’:the natural or legal person that distributes or communicates the Work under the Licence.
+— ‘Contributor(s)’:any natural or legal person who modifies the Work under the Licence, or otherwise contributes to
+the creation of a Derivative Work.
+— ‘The Licensee’ or ‘You’:any natural or legal person who makes any usage of the Work under the terms of the
+Licence.
+— ‘Distribution’ or ‘Communication’:any act of selling, giving, lending, renting, distributing, communicating,
+transmitting, or otherwise making available, online or offline, copies of the Work or providing access to its essential
+functionalities at the disposal of any other natural or legal person.
+
+2.Scope of the rights granted by the Licence
+The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable licence to do the following, for
+the duration of copyright vested in the Original Work:
+— use the Work in any circumstance and for all usage,
+— reproduce the Work,
+— modify the Work, and make Derivative Works based upon the Work,
+— communicate to the public, including the right to make available or display the Work or copies thereof to the public
+and perform publicly, as the case may be, the Work,
+— distribute the Work or copies thereof,
+— lend and rent the Work or copies thereof,
+— sublicense rights in the Work or copies thereof.
+Those rights can be exercised on any media, supports and formats, whether now known or later invented, as far as the
+applicable law permits so.
+In the countries where moral rights apply, the Licensor waives his right to exercise his moral right to the extent allowed
+by law in order to make effective the licence of the economic rights here above listed.
+The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to any patents held by the Licensor, to the
+extent necessary to make use of the rights granted on the Work under this Licence.
+
+3.Communication of the Source Code
+The Licensor may provide the Work either in its Source Code form, or as Executable Code. If the Work is provided as
+Executable Code, the Licensor provides in addition a machine-readable copy of the Source Code of the Work along with
+each copy of the Work that the Licensor distributes or indicates, in a notice following the copyright notice attached to
+the Work, a repository where the Source Code is easily and freely accessible for as long as the Licensor continues to
+distribute or communicate the Work.
+
+4.Limitations on copyright
+Nothing in this Licence is intended to deprive the Licensee of the benefits from any exception or limitation to the
+exclusive rights of the rights owners in the Work, of the exhaustion of those rights or of other applicable limitations
+thereto.
+
+5.Obligations of the Licensee
+The grant of the rights mentioned above is subject to some restrictions and obligations imposed on the Licensee. Those
+obligations are the following:
+
+Attribution right: The Licensee shall keep intact all copyright, patent or trademarks notices and all notices that refer to
+the Licence and to the disclaimer of warranties. The Licensee must include a copy of such notices and a copy of the
+Licence with every copy of the Work he/she distributes or communicates. The Licensee must cause any Derivative Work
+to carry prominent notices stating that the Work has been modified and the date of modification.
+
+Copyleft clause: If the Licensee distributes or communicates copies of the Original Works or Derivative Works, this
+Distribution or Communication will be done under the terms of this Licence or of a later version of this Licence unless
+the Original Work is expressly distributed only under this version of the Licence — for example by communicating
+‘EUPL v. 1.2 only’. The Licensee (becoming Licensor) cannot offer or impose any additional terms or conditions on the
+Work or Derivative Work that alter or restrict the terms of the Licence.
+
+Compatibility clause: If the Licensee Distributes or Communicates Derivative Works or copies thereof based upon both
+the Work and another work licensed under a Compatible Licence, this Distribution or Communication can be done
+under the terms of this Compatible Licence. For the sake of this clause, ‘Compatible Licence’ refers to the licences listed
+in the appendix attached to this Licence. Should the Licensee's obligations under the Compatible Licence conflict with
+his/her obligations under this Licence, the obligations of the Compatible Licence shall prevail.
+
+Provision of Source Code: When distributing or communicating copies of the Work, the Licensee will provide
+a machine-readable copy of the Source Code or indicate a repository where this Source will be easily and freely available
+for as long as the Licensee continues to distribute or communicate the Work.
+Legal Protection: This Licence does not grant permission to use the trade names, trademarks, service marks, or names
+of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and
+reproducing the content of the copyright notice.
+
+6.Chain of Authorship
+The original Licensor warrants that the copyright in the Original Work granted hereunder is owned by him/her or
+licensed to him/her and that he/she has the power and authority to grant the Licence.
+Each Contributor warrants that the copyright in the modifications he/she brings to the Work are owned by him/her or
+licensed to him/her and that he/she has the power and authority to grant the Licence.
+Each time You accept the Licence, the original Licensor and subsequent Contributors grant You a licence to their contributions
+to the Work, under the terms of this Licence.
+
+7.Disclaimer of Warranty
+The Work is a work in progress, which is continuously improved by numerous Contributors. It is not a finished work
+and may therefore contain defects or ‘bugs’ inherent to this type of development.
+For the above reason, the Work is provided under the Licence on an ‘as is’ basis and without warranties of any kind
+concerning the Work, including without limitation merchantability, fitness for a particular purpose, absence of defects or
+errors, accuracy, non-infringement of intellectual property rights other than copyright as stated in Article 6 of this
+Licence.
+This disclaimer of warranty is an essential part of the Licence and a condition for the grant of any rights to the Work.
+
+8.Disclaimer of Liability
+Except in the cases of wilful misconduct or damages directly caused to natural persons, the Licensor will in no event be
+liable for any direct or indirect, material or moral, damages of any kind, arising out of the Licence or of the use of the
+Work, including without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, loss
+of data or any commercial damage, even if the Licensor has been advised of the possibility of such damage. However,
+the Licensor will be liable under statutory product liability laws as far such laws apply to the Work.
+
+9.Additional agreements
+While distributing the Work, You may choose to conclude an additional agreement, defining obligations or services
+consistent with this Licence. However, if accepting obligations, You may act only on your own behalf and on your sole
+responsibility, not on behalf of the original Licensor or any other Contributor, and only if You agree to indemnify,
+defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against such Contributor by
+the fact You have accepted any warranty or additional liability.
+
+10.Acceptance of the Licence
+The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ placed under the bottom of a window
+displaying the text of this Licence or by affirming consent in any other similar way, in accordance with the rules of
+applicable law. Clicking on that icon indicates your clear and irrevocable acceptance of this Licence and all of its terms
+and conditions.
+Similarly, you irrevocably accept this Licence and all of its terms and conditions by exercising any rights granted to You
+by Article 2 of this Licence, such as the use of the Work, the creation by You of a Derivative Work or the Distribution
+or Communication by You of the Work or copies thereof.
+
+11.Information to the public
+In case of any Distribution or Communication of the Work by means of electronic communication by You (for example,
+by offering to download the Work from a remote location) the distribution channel or media (for example, a website)
+must at least provide to the public the information requested by the applicable law regarding the Licensor, the Licence
+and the way it may be accessible, concluded, stored and reproduced by the Licensee.
+
+12.Termination of the Licence
+The Licence and the rights granted hereunder will terminate automatically upon any breach by the Licensee of the terms
+of the Licence.
+Such a termination will not terminate the licences of any person who has received the Work from the Licensee under
+the Licence, provided such persons remain in full compliance with the Licence.
+
+13.Miscellaneous
+Without prejudice of Article 9 above, the Licence represents the complete agreement between the Parties as to the
+Work.
+If any provision of the Licence is invalid or unenforceable under applicable law, this will not affect the validity or
+enforceability of the Licence as a whole. Such provision will be construed or reformed so as necessary to make it valid
+and enforceable.
+The European Commission may publish other linguistic versions or new versions of this Licence or updated versions of
+the Appendix, so far this is required and reasonable, without reducing the scope of the rights granted by the Licence.
+New versions of the Licence will be published with a unique version number.
+All linguistic versions of this Licence, approved by the European Commission, have identical value. Parties can take
+advantage of the linguistic version of their choice.
+
+14.Jurisdiction
+Without prejudice to specific agreement between parties,
+— any litigation resulting from the interpretation of this License, arising between the European Union institutions,
+bodies, offices or agencies, as a Licensor, and any Licensee, will be subject to the jurisdiction of the Court of Justice
+of the European Union, as laid down in article 272 of the Treaty on the Functioning of the European Union,
+— any litigation arising between other parties and resulting from the interpretation of this License, will be subject to
+the exclusive jurisdiction of the competent court where the Licensor resides or conducts its primary business.
+
+15.Applicable Law
+Without prejudice to specific agreement between parties,
+— this Licence shall be governed by the law of the European Union Member State where the Licensor has his seat,
+resides or has his registered office,
+— this licence shall be governed by Belgian law if the Licensor has no seat, residence or registered office inside
+a European Union Member State.
+
+
+ Appendix
+
+‘Compatible Licences’ according to Article 5 EUPL are:
+— GNU General Public License (GPL) v. 2, v. 3
+— GNU Affero General Public License (AGPL) v. 3
+— Open Software License (OSL) v. 2.1, v. 3.0
+— Eclipse Public License (EPL) v. 1.0
+— CeCILL v. 2.0, v. 2.1
+— Mozilla Public Licence (MPL) v. 2
+— GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
+— Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for works other than software
+— European Union Public Licence (EUPL) v. 1.1, v. 1.2
+— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong Reciprocity (LiLiQ-R+).
+
+The European Commission may update this Appendix to later versions of the above licences without producing
+a new version of the EUPL, as long as they provide the rights granted in Article 2 of this Licence and protect the
+covered Source Code from exclusive appropriation.
+All other changes or additions to this Appendix require the production of a new EUPL version.
diff --git a/README.md b/README.md
index d39b3f88..ebe8b4e3 100644
--- a/README.md
+++ b/README.md
@@ -1,91 +1,213 @@
-# Software Catalogus
+
+
+
-[](https://opensource.org/licenses/EUPL)
-[](https://nextcloud.com/)
-[]()
+Software Catalogus
-The **Software Catalogus** is a Nextcloud app that provides a powerful framework for managing and synchronizing software catalogs in an open data ecosystem. This app enables organizations to keep their software data up-to-date, facilitates collaboration, and promotes transparency through open data practices.
+
+ GEMMA-compliant software catalog for Nextcloud — applications, modules, and integration management
+
+
+
+
+
+
+
+
+
+---
+
+Software Catalogus brings structured software portfolio management to Nextcloud. Register the applications, modules, and connections (koppelingen) that make up your organization's IT landscape, manage contacts and organizations, and synchronize catalog data across a federated open data network — all aligned with Dutch GEMMA standards.
+
+It integrates with [OpenRegister](https://github.com/ConductionNL/openregister) for data storage and automatic user provisioning, turning register contacts into Nextcloud accounts with role-based group membership.
+
+> **Requires:** [OpenRegister](https://github.com/ConductionNL/openregister) — all data is stored as OpenRegister objects (no own database tables).
+
+## Screenshots
+
+
+
+
+
+
+
+
+ Dashboard
+ Applications
+ Connections
+
+
## Features
-- 🔄 **Synchronize Software Data**: Automatically synchronize your software data across multiple catalogs.
-- 📡 **Automatic Publication**: Publish and update software catalog information seamlessly.
-- 🏢 **Organization Management**: Enhanced organization synchronization and management capabilities.
-- 📊 **API Integration**: Comprehensive API for aangeboden gebruik and organization workflows.
-- 🆓 **Open Source**: Licensed under the [EUPL](https://opensource.org/licenses/EUPL).
+### Application Management
+- **Software Landscape** — Register and maintain all applications (voorzieningen) in your organization
+- **Module Tracking** — Break applications into modules and track their versions, suppliers, and dependencies
+- **Connection Mapping** — Define koppelingen (integrations) between applications and modules to visualize data flows
+- **Contract Administration** — Link contracts to applications and track license agreements
+
+### Organization Management
+- **Organization Registry** — Manage organizations and their contact persons within the catalog
+- **Automatic User Provisioning** — Create Nextcloud accounts from contactpersoon objects in OpenRegister
+- **Role-Based Groups** — Automatic group assignment based on user roles (beheerder, inkoper, ambtenaar)
+- **Organizational Hierarchy** — First user in an organization becomes beheerder; manager relationships maintained automatically
+
+### Synchronization
+- **Federated Sync** — Share and synchronize catalog data with other organizations over a federated network
+- **Open Data Publishing** — Automatically publish your software catalog for transparency and reuse
+- **Event-Driven Processing** — Real-time user and group updates via OpenRegister event listeners
+- **Background Jobs** — Scheduled organization-contact synchronization via Nextcloud cron
+
+### Integrations
+- **OpenRegister** — All data stored as JSON objects in OpenRegister schemas
+- **Nextcloud Groups** — Automatic group creation and membership management per organization and role
+- **Manager Relationships** — Beheerders become Nextcloud managers for their organization's users
+
+## Architecture
+
+```mermaid
+graph TD
+ A[Vue 2 Frontend] -->|REST API| B[OpenRegister API]
+ B --> C[(PostgreSQL JSON store)]
+ A --> D[Nextcloud Groups]
+ B -->|events| E[User Provisioning Service]
+ E --> F[Nextcloud User Manager]
+ G[Cron] -->|background job| H[Organization Sync]
+ H --> B
+```
+
+### Data Model
+
+| Object | Description | GEMMA Mapping |
+|--------|-------------|---------------|
+| Voorziening | Application in the software landscape | Applicatie |
+| Module | Component within an application | Module / Component |
+| Koppeling | Integration between modules or applications | Koppeling |
+| Organisatie | Organization that uses or supplies software | Organisatie |
+| Contactpersoon | Individual linked to an organization | Contactpersoon |
+| Contract | License or service agreement | Contract |
+
+**Data standard:** GEMMA Softwarecatalogus with Schema.org compatibility.
+
+### Directory Structure
+
+```
+softwarecatalog/
+├── appinfo/ # Nextcloud app manifest, routes, navigation
+├── lib/ # PHP backend — controllers, services, event listeners
+│ ├── Controller/ # API and page controllers
+│ ├── Service/ # Business logic (user provisioning, sync, groups)
+│ └── Listener/ # OpenRegister event listeners
+├── src/ # Vue 2 frontend — components, Pinia stores, views
+│ ├── views/ # Route-level views (dashboard, voorzieningen, organisaties…)
+│ └── store/ # Pinia stores per entity
+├── docs/ # Technical documentation
+├── img/ # App icons and screenshots
+├── l10n/ # Translations (en, nl)
+└── docusaurus/ # Product documentation site (softwarecatalog.app)
+```
## Requirements
-- PHP 8.0 or higher
-- PostgreSQL 10+, SQLite, or MySQL 8.0+
-- Nextcloud version 28 to 30
-- System Cron is required for the app to function properly
+| Dependency | Version |
+|-----------|---------|
+| Nextcloud | 28 -- 33 |
+| PHP | 8.0+ |
+| [OpenRegister](https://github.com/ConductionNL/openregister) | latest |
## Installation
-To install the Software Catalogus app, follow these steps:
+### From the Nextcloud App Store
+
+1. Go to **Apps** in your Nextcloud instance
+2. Search for **Software Catalogus**
+3. Click **Download and enable**
-1. **Download the App:**
- Download the latest release from the [GitHub repository](https://github.com/ConductionNL/SoftwareCatalogus/releases).
+> OpenRegister must be installed first. [Install OpenRegister -->](https://apps.nextcloud.com/apps/openregister)
-2. **Upload the App:**
- Upload the app to the `apps` directory of your Nextcloud installation.
+### From Source
-3. **Enable the App:**
- Go to the "Apps" section in your Nextcloud instance and enable the **Software Catalogus** app.
+```bash
+cd /var/www/html/custom_apps
+git clone https://github.com/ConductionNL/softwarecatalog.git
+cd softwarecatalog
+npm install
+npm run build
+php occ app:enable softwarecatalog
+```
-4. **Configure System Cron:**
- Ensure that the System Cron is properly configured on your server to allow the app to function optimally.
+## Development
-## Core Features
+### Start the environment
-### 🚀 Automatic User Management
-- **User Creation**: Automatic Nextcloud account creation from contactgegevens objects
-- **Username Generation**: Smart username creation from name fields (voornaam.achternaam)
-- **Profile Synchronization**: User data kept in sync with OpenRegister
+```bash
+docker compose -f openregister/docker-compose.yml up -d
+```
-### 👥 Advanced Group Management
-- **Role-Based Groups**: Automatic assignment to groups based on user roles (beheerder, inkoper)
-- **Organization Groups**: Each organization gets its own group with automatic member assignment
-- **Special Groups**: The 'ambtenaar' group is available for manual assignment (no longer automatically assigned)
-- **Dynamic Updates**: Group memberships automatically updated when roles change
+### Frontend development
-### 🏢 Organizational Hierarchy
-- **Auto-Beheerder Assignment**: First user in organization automatically becomes beheerder
-- **Manager Relationships**: Beheerders become managers for their organization's users
-- **Hierarchy Management**: Multiple beheerders supported with seniority-based primary manager
-- **Organization Groups**: Automatic group creation and management for each organization
+```bash
+cd softwarecatalog
+npm install
+npm run dev # Watch mode
+npm run build # Production build
+```
-### ⚡ Event-Driven Processing
-- **Real-Time Updates**: Processes changes immediately via OpenRegister events
-- **Multiple Event Types**: Handles creation, updates, deletion, locking, and reversion
-- **Error Recovery**: Comprehensive error handling with detailed logging
-- **Type Safety**: Robust handling of schema ID mismatches and data validation
+### Code quality
+
+```bash
+# PHP
+composer phpcs # Check coding standards
+composer cs:fix # Auto-fix issues
+composer phpmd # Mess detection
+composer phpmetrics # HTML metrics report
+
+# Frontend
+npm run lint # ESLint
+npm run stylelint # CSS linting
+```
+
+## Tech Stack
+
+| Layer | Technology |
+|-------|-----------|
+| Frontend | Vue 2.7, Pinia, @nextcloud/vue |
+| Build | Webpack 5, @nextcloud/webpack-vue-config |
+| Backend | PHP 8.0+, Nextcloud App Framework |
+| Data | OpenRegister (PostgreSQL JSON objects) |
+| UX | @conduction/nextcloud-vue |
+| Quality | PHPCS, PHPMD, phpmetrics, ESLint, Stylelint |
## Documentation
-Comprehensive documentation is available in the `docs/` directory:
+Full documentation is available at **[softwarecatalog.app](https://softwarecatalog.app)**
+
+| Page | Description |
+|------|-------------|
+| [Features](docs/FEATURES.md) | Complete feature specification |
+| [Architecture](docs/ARCHITECTURE.md) | Technical architecture and design decisions |
+| [User Guide](docs/USER_GUIDE.md) | End-user and administrator guide |
+| [Configuration](docs/CONFIGURATION.md) | Setup instructions and troubleshooting |
+
+## Standards & Compliance
+
+- **Data standard:** GEMMA Softwarecatalogus (VNG)
+- **Architecture:** Common Ground principles — layered, API-first, open source
+- **Accessibility:** WCAG AA (Dutch government requirement)
+- **Authorization:** RBAC via OpenRegister
+- **Audit trail:** Full change history on all objects
+- **Localization:** English and Dutch
-### For Users and Administrators
-- **[📖 User Guide](docs/USER_GUIDE.md)** - Complete guide for end users and system administrators
-- **[⚙️ Configuration Guide](docs/CONFIGURATION.md)** - Setup instructions and troubleshooting
+## Related Apps
-### For Developers and Integrators
-- **[🏗️ Architecture Documentation](docs/ARCHITECTURE.md)** - System design and component overview
-- **[👥 Group Management Guide](docs/GROUP_MANAGEMENT.md)** - Detailed explanation of group logic
-- **[🔌 API Reference](docs/API_REFERENCE.md)** - Technical API documentation
+- **[OpenRegister](https://github.com/ConductionNL/openregister)** — Object storage layer (required dependency)
+- **[OpenCatalogi](https://github.com/ConductionNL/opencatalogi)** — Publication and catalog management
+- **[NL Design](https://github.com/ConductionNL/nldesign)** — Design token theming for Dutch government standards
-### Quick Start
-1. **Install Prerequisites**: Ensure OpenRegister app is installed and enabled
-2. **Configure Schemas**: Set up schema mappings in Admin Settings → Software Catalogus
-3. **Test Processing**: Create a contactgegevens object in OpenRegister to verify automatic user creation
-4. **Monitor Groups**: Check that users are assigned to appropriate groups
+## License
-## Usage
+EUPL-1.2
-Once installed, the Software Catalogus app will:
+## Authors
-- **Automatic Processing**: Listen for OpenRegister events and process users/organizations automatically
-- **Admin Interface**: Provide configuration interface in Admin Settings → Software Catalogus
-- **Group Management**: Handle all user group assignments and organizational hierarchy
-- **Manager Relationships**: Establish and maintain manager-subordinate relationships
+Built by [Conduction](https://conduction.nl) — open-source software for Dutch government and public sector organizations.
diff --git a/aanvullende-informatie.md b/aanvullende-informatie.md
new file mode 100644
index 00000000..3c887c09
--- /dev/null
+++ b/aanvullende-informatie.md
@@ -0,0 +1,526 @@
+# Aanvullende Informatie — IGS Issues Softwarecatalogus
+
+> Dit document bevat alle issues met Onderdeel-van: IGS uit het [VNG-Realisatie/Softwarecatalogus](https://github.com/VNG-Realisatie/Softwarecatalogus) project, inclusief analyse per issue.
+> Alle issues zijn ook beschikbaar als individuele markdown bestanden in de [issues/](issues/) map (met beschrijving, reacties en afbeeldingen).
+> Zie ook: [issues.md](issues.md) voor de volledige lijst met acceptatiecriteria per issue.
+
+**Totaal: 159 issues** | Open: 207 | Gesloten: 233
+**Laatste sync:** 2026-03-05 | +6 nieuwe issues (#451-#456) | +2 gesloten issues (#225, #315)
+
+---
+
+## Databronnen
+
+De volgende bestanden zijn beschikbaar voor het verifiëren van datakwaliteit-issues:
+
+### CSV Importbestanden (client-aangeleverd)
+Deze CSV-bestanden bevatten de data die door de klant is aangeleverd voor import in de Softwarecatalogus. Ze worden gebruikt om claims over datakwaliteit te onderbouwen (ontbrekende relaties, orphaned references, etc.).
+
+| Bestand | Omschrijving | Pad |
+|---------|-------------|-----|
+| `module.csv` | Applicaties/modules (6100+ records) | [data/module.csv](data/module.csv) |
+| `koppeling.csv` | Koppelingen tussen modules (3400+ records) | [data/koppeling.csv](data/koppeling.csv) |
+| `organisatie.csv` | Organisaties (gemeenten, leveranciers) | [data/organisatie.csv](data/organisatie.csv) |
+| `contactpersoon.csv` | Contactpersonen per organisatie | [data/contactpersoon.csv](data/contactpersoon.csv) |
+| `compliancy.csv` | Compliance-status per standaard | [data/compliancy.csv](data/compliancy.csv) |
+| `gebruik.csv` | Gebruik van applicaties door gemeenten | [data/gebruik.csv](data/gebruik.csv) |
+| `gebruik_2.csv` | Gebruik (vervolg) | [data/gebruik_2.csv](data/gebruik_2.csv) |
+| `gebruik_3.csv` | Gebruik (vervolg) | [data/gebruik_3.csv](data/gebruik_3.csv) |
+| `moduleversie.csv` | Versies van modules | [data/moduleversie.csv](data/moduleversie.csv) |
+
+### GEMMA ArchiMate Model (AMEF)
+Het GEMMA ArchiMate Exchange Format bestand bevat het volledige architectuurmodel dat wordt geïmporteerd in het AMEF-register.
+
+| Bestand | Omschrijving | Pad |
+|---------|-------------|-----|
+| `GEMMA release.xml` | Volledig GEMMA ArchiMate model (13.3 MB) | `softwarecatalog/data/GEMMA release.xml` |
+| `GEMMA_release.xml` | Kopie in Settings directory (13.4 MB) | `softwarecatalog/lib/Settings/GEMMA_release.xml` |
+| Turfbrug test model | VNG Realisatie test-export (15 MB) | `Softwarecatalogus/docs/examples/02-04-2025_GEMMA 2_Turfbrug (test VNG Realisatie)_ameff_model.xml` |
+
+### Analyse: Orphaned buitengemeentelijkVoorziening referenties in koppelingen
+
+Van de 3.406 koppelingen in het importbestand hebben **876** (25,7%) een `buitengemeentelijkVoorziening` waarde (alle met `type=extern`). Deze verwijzen naar **39 unieke** element-UUIDs uit het GEMMA ArchiMate model. De elementen worden apart geïmporteerd via de GEMMA XML — ze staan **niet** in de CSV-importdata.
+
+**Import-afhankelijkheid**: Alle 39 BGV-UUIDs zijn afwezig in de CSV-bestanden. Als de GEMMA XML-import niet is uitgevoerd, hebben alle 876 koppelingen hangende referenties. De `_buitengemeentelijkVoorzieningNaam` kolom in de CSV biedt een gedenormaliseerde fallback voor weergave.
+
+**5 werkelijk orphaned referenties** (20 koppelingen):
+
+| CSV UUID | Naam | Reden | Koppelingen |
+|----------|------|-------|-------------|
+| `a0ce4c62-9619-4d60-bb27-7eabb5a9005e` | DSO-LV | Niet in GEMMA XML — nieuwere voorziening | 15 |
+| `49b7255f-0217-4a0c-b23f-125c97252948` | LVBB | Niet in GEMMA XML — nieuwere voorziening | 2 |
+| `4f4ebcbe-8af0-412a-b32d-721544b23cb1` | Softwarecatalogus.nl | Andere UUID in GEMMA XML (`c32b8ee1...`) | 1 |
+| `762ed5d8-c2a2-45eb-94fc-953dd0ab2136` | Werkgeversinstrumentgids | Andere UUID in GEMMA XML (`f3bd5b40...`) | 1 |
+| `e814e6c0-966a-4fee-af6d-249603e7c850` | Werkzoekendeninstrumentgids | Andere UUID in GEMMA XML (`08f55ef1...`) | 1 |
+
+**Oorzaak**: DSO-LV en LVBB zijn nieuwere landelijke voorzieningen die (nog) niet in het GEMMA ArchiMate model zijn opgenomen. De overige 3 bestaan wel in de XML maar onder een ander UUID-formaat (hex-only vs. gehypheneerd).
+
+**Top 10 meest gekoppelde BGV's** (correct in GEMMA XML):
+
+| Naam | Koppelingen |
+|------|-------------|
+| GBA-V (Verstrekkingsvoorziening) | 99 |
+| NHR - Handelsregister | 77 |
+| OLO - OmgevingsLoket Online | 68 |
+| BRK - Basisregistratie Kadaster | 60 |
+| MijnOverheid.nl | 56 |
+| GGK - Gemeentelijk Gegevensknooppunt | 53 |
+| DigiD | 49 |
+| LV-BAG | 45 |
+| LV-WOZ | 45 |
+| JUBES | 44 |
+
+### Postman/Newman API Tests
+| Bestand | Omschrijving | Pad |
+|---------|-------------|-----|
+| Collection | 134 requests, 150 assertions, 11 folders | [postman/softwarecatalogus-tests.json](postman/softwarecatalogus-tests.json) |
+| Environment (local) | Variabelen voor localhost:8080 | [postman/environment-local.json](postman/environment-local.json) |
+
+---
+
+## Agent Workflow — Issue-per-Issue Reacties Voorbereiden
+
+Dit hoofdstuk beschrijft hoe sub-agents elke open issue moeten verwerken om een GitHub-reactie met bewijs voor te bereiden.
+
+### Doel
+Voor elke **open** issue bereiden we een markdown-reactie voor die:
+1. De oorzaak uitlegt (bug, datakwaliteit, tekstueel, wens, of functionaliteit)
+2. Bewijs levert (screenshots, data-analyse, code-referenties)
+3. De huidige status aantoont (opgelost, onderzocht, of buiten scope)
+
+> **BELANGRIJK: Alleen voorbereiden, NIET plaatsen!**
+> Agents mogen NOOIT reacties plaatsen op GitHub issues. Alle reacties worden lokaal opgeslagen in `Softwarecatalogus/reacties/{nummer}.md` zodat ze eerst handmatig gereviewd kunnen worden voordat ze worden geplaatst.
+
+### Categorie-templates
+
+#### Template A: Bug — Opgelost
+Gebruik voor issues gecategoriseerd als **Bug** die inmiddels zijn opgelost.
+
+```markdown
+## Status: Opgelost
+
+Dit issue is opgelost. Hieronder het bewijs:
+
+### Situatie voor de fix
+{Beschrijf kort wat het probleem was}
+
+### Huidige situatie
+{Beschrijf wat er nu gebeurt}
+
+### Bewijs
+{Screenshot(s) die aantonen dat het probleem is opgelost}
+
+Getest op: {datum}
+Omgeving: {URL}
+```
+
+#### Template B: Bug — In behandeling
+Gebruik voor issues gecategoriseerd als **Bug** die nog niet zijn opgelost.
+
+```markdown
+## Status: In behandeling
+
+Dit issue is in onderzoek. Bevindingen tot nu toe:
+
+### Analyse
+{Beschrijf de root cause}
+
+### Huidige situatie
+{Screenshot(s) van de huidige staat — toont het probleem nog steeds of is het deels opgelost}
+
+### Verwachte oplossing
+{Beschrijf de geplande aanpak}
+
+### Voortgang
+- [ ] Root cause geïdentificeerd
+- [ ] Fix geïmplementeerd
+- [ ] Getest
+```
+
+#### Template C: Datakwaliteit — Importdata
+Gebruik voor issues gecategoriseerd als **Datakwaliteit**.
+
+```markdown
+## Status: Datakwaliteit importbestanden
+
+Dit gedrag wordt veroorzaakt door ontbrekende of foutieve data in de aangeleverde importbestanden, niet door een fout in de applicatie.
+
+### Oorzaak
+{Beschrijf welk CSV-bestand en welke velden het probleem veroorzaken}
+
+### Data-analyse
+{Toon specifieke voorbeelden uit de CSV-bestanden}
+- Bestand: `{bestandsnaam}`
+- Aantal records met ontbrekende referenties: {aantal}
+- Voorbeeld: `{rij uit CSV}`
+
+### Applicatiegedrag
+De applicatie verwerkt de data correct volgens het ontwerp:
+{Leg uit hoe de applicatie omgaat met ontbrekende referenties}
+
+### Aanbeveling
+{Optioneel: suggestie voor dataopschoning of validatie bij import}
+```
+
+#### Template D: Tekstueel — Doorgevoerd
+Gebruik voor issues gecategoriseerd als **Tekstueel** die zijn doorgevoerd.
+
+```markdown
+## Status: Doorgevoerd
+
+De gevraagde tekstuele aanpassing is doorgevoerd.
+
+### Wijziging
+- **Was**: "{oude tekst}"
+- **Is nu**: "{nieuwe tekst}"
+
+### Bewijs
+{Screenshot van de huidige situatie}
+
+Getest op: {datum}
+Omgeving: {URL}
+```
+
+#### Template E: Wens — Buiten scope
+Gebruik voor issues gecategoriseerd als **Wens**.
+
+```markdown
+## Status: Wens (buiten oorspronkelijke scope)
+
+Dit verzoek betreft nieuwe functionaliteit die niet is opgenomen in het oorspronkelijke Programma van Eisen.
+
+### Toelichting
+{Leg uit waarom dit een wens is en niet een bug of ontbrekende functionaliteit}
+
+### Huidige werking
+{Beschrijf hoe de applicatie nu werkt op dit punt}
+
+### Bewijs
+{Screenshot(s) van de huidige werking — toont aan dat de applicatie correct functioneert binnen de oorspronkelijke scope}
+
+### Mogelijke vervolgstap
+{Optioneel: suggestie voor backlog of doorontwikkeling}
+```
+
+#### Template F: Functionaliteit — Uitleg werkwijze
+Gebruik voor issues waar de gemelde situatie correct gedrag is maar verkeerd begrepen.
+
+```markdown
+## Status: Werkt conform ontwerp
+
+Het beschreven gedrag is correct en werkt zoals ontworpen.
+
+### Werking
+{Leg uit hoe de functionaliteit werkt en waarom dit het verwachte gedrag is}
+
+### Configuratie
+{Optioneel: verwijs naar RBAC-regels, register-configuratie, of andere instellingen}
+
+### Bewijs
+{Screenshot(s) die de correcte werking aantonen}
+```
+
+### Agent-instructies per issue
+
+**Elke sub-agent verwerkt één issue per keer.** Dit houdt het contextvenster klein en de resultaten beheersbaar.
+
+#### Stap 1: Issue lezen
+```
+Lees het issue-bestand: Softwarecatalogus/issues/{nummer}.md
+```
+Dit bevat de volledige beschrijving, alle reacties, en afbeeldingsreferenties.
+
+#### Stap 2: Categorie bepalen
+Zoek het issue op in de samenvattingstabel hierboven. De categorie (Bug/Datakwaliteit/Tekstueel/Wens/Nog te bepalen) bepaalt welk template wordt gebruikt.
+
+#### Stap 3: Onderzoek uitvoeren
+
+Afhankelijk van de categorie:
+
+| Categorie | Acties |
+|-----------|--------|
+| **Bug** | 1. Reproduceer in browser (navigeer naar relevante pagina, voer stappen uit) 2. Controleer of het probleem is opgelost 3. Maak screenshot(s) als bewijs |
+| **Datakwaliteit** | 1. Lees het relevante CSV-bestand (`Softwarecatalogus/data/{bestand}.csv`) 2. Zoek naar de specifieke data die het probleem veroorzaakt 3. Tel ontbrekende referenties of foutieve waarden 4. Toon voorbeelden uit de CSV |
+| **Tekstueel** | 1. Navigeer naar de pagina/wizard waar de tekst staat 2. Controleer of de tekst is aangepast 3. Maak screenshot als bewijs |
+| **Wens** | 1. Lees het PvE (issues.md) om te bevestigen dat dit buiten scope valt 2. Beschrijf de huidige werking 3. Maak screenshot(s) van de huidige werking als bewijs |
+| **Nog te bepalen** | 1. Analyseer het issue grondig 2. Bepaal de juiste categorie 3. Volg de instructies voor die categorie |
+
+#### Stap 4: Reactie schrijven
+Schrijf de reactie in markdown volgens het bijbehorende template. Sla op als:
+```
+Softwarecatalogus/reacties/{nummer}.md
+```
+
+#### Stap 5: Screenshots opslaan
+Sla screenshots op in:
+```
+Softwarecatalogus/reacties/screenshots/{nummer}-{beschrijving}.png
+```
+
+### Omgeving voor agents
+
+| Instelling | Waarde |
+|------------|--------|
+| Frontend URL | http://localhost:3000 |
+| Backend URL | http://localhost:8080 |
+| Admin credentials | admin / admin |
+| Test-gebruikers | Zie `softwarecatalog/.claude/skills/test-softwarecatalog.md` |
+
+### Browsergebruik
+- Gebruik de toegewezen browser (zie browser-pool in CLAUDE.md)
+- Login via Frontend URL + `/login`
+- Voer `localStorage.clear()` uit voor het inloggen
+- Voor publieke issues: test zonder in te loggen
+
+### Data-verificatie commando's
+
+Gebruik deze bash-commando's voor snelle data-verificatie:
+
+```bash
+# Tel orphaned koppelingen (verwijzen naar niet-bestaande modules)
+# Haal moduleA/moduleB UUIDs uit koppeling.csv en check tegen module.csv IDs
+grep -c "moduleA" Softwarecatalogus/data/koppeling.csv
+
+# Zoek een specifieke organisatie in importdata
+grep -i "centric" Softwarecatalogus/data/organisatie.csv
+
+# Tel modules per aanbieder
+cut -d',' -f2 Softwarecatalogus/data/module.csv | sort | uniq -c | sort -rn | head -20
+
+# Zoek contactpersonen zonder e-mailadres
+awk -F',' '{if ($NF == "" || $NF == "\"\"") print}' Softwarecatalogus/data/contactpersoon.csv | wc -l
+```
+
+### RBAC-referentie voor agents
+De RBAC-regels staan in: `softwarecatalog/lib/Settings/softwarecatalogus_register.json`
+Raadpleeg dit bestand wanneer een issue gaat over zichtbaarheid, toegang, of organisatie-scoping.
+
+### Voortgang bijhouden
+Na het verwerken van elk issue, update deze tabel:
+
+| # | Status reactie | Agent | Datum |
+|---|---------------|-------|-------|
+| {nummer} | Concept / Klaar / Geplaatst | {agent-id} | {datum} |
+
+## Samenvatting
+
+| Categorie | Totaal | Open | Gesloten | Omschrijving |
+|-----------|--------|------|----------|-------------|
+| Bug | 92 | 45 | 47 | Daadwerkelijke fouten in de applicatie |
+| Datakwaliteit | 13 | 10 | 3 | Veroorzaakt door ontbrekende of foutieve data in client-importbestanden |
+| Tekstueel | 20 | 7 | 13 | Inconsistente labels, schrijfwijze, terminologie |
+| Wens | 21 | 11 | 10 | Nieuwe functionaliteit buiten oorspronkelijke scope |
+| Nog te bepalen | 4 | 3 | 1 | Combinatie van factoren, nader onderzoek nodig |
+
+**86 van 144 issues (59%) zijn daadwerkelijke bugs.** De overige 54 issues (37%) zijn datakwaliteit (13), tekstueel (20), of wensen (21).
+
+---
+
+## Open issues
+*76 issues*
+
+### Bug (45)
+
+| # | Issue | Analyse | GitHub | Checked |
+|---|-------|---------|--------|--------|
+| [15](issues/15.md) | Als aanbod- en gebruik-beheerder wil ik data vanuit de softwarecatalogus kunnen exporteren | Export toont verkeerde kolommen en UUID's; geimporteerde data mist diensten | [#15](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/15) | [ ] |
+| [65](issues/65.md) | Als aanbod- en gebruik-beheerder van een organisatie wil ik mijn collega's toegang kunnen geven tot de softwarecatalogus | Gebruikersbeheer en uitnodigen collega's werkt niet goed, diverse UI-fouten | [#65](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/65) | [ ] |
+| [73](issues/73.md) | Als aanbod-beheerder wil ik meerdere contactpersonen kunnen registreren en deze aan specifieke pakketten kunnen koppelen | Contactpersonen aanmaken/bewerken werkt niet goed, wijzigingen niet opgeslagen | [#73](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/73) | [ ] |
+| [144](issues/144.md) | Als gebruiker van de Softwarecatalogus wil ik een overzicht met zoek- en filteropties van alle organisaties die pakketten of diensten aanbieden | Zoekfilters werken niet goed, ontbrekende filters en UUID's bij organisatienamen | [#144](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/144) | [ ] |
+| [169](issues/169.md) | Rest issues van Organisatie en Configuratie | Restpunten organisatie: registratieformulier niet gekoppeld aan Mijn Account | [#169](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/169) | [ ] |
+| [263](issues/263.md) | Niet ingelogd: onder een applicatie staat in het tabje gebruik de gemeenten | Niet-ingelogde gebruiker ziet gebruik-tab met gemeentenamen bij applicaties | [#263](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/263) | [ ] |
+| [280](issues/280.md) | Zoeken: sorteren gaat niet goed. | Sorteren op zoekresultaten werkt niet goed, filter Type ontbreekt | [#280](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/280) | [ ] |
+| [314](issues/314.md) | Wizard Koppeling publiceren vind zelf aangemaakte applicaties niet | Wizard koppeling publiceren vindt eigen applicaties niet | [#314](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/314) | [ ] |
+| [344](issues/344.md) | Zoeken: Geen resultaten bij het selecteren van het Gravenbeheercomponent. Niet ingelogd. | Geen zoekresultaten bij Gravenbeheercomponent door uitgezet schema-filter | [#344](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/344) | [ ] |
+| [345](issues/345.md) | Zoeken: toegevoegde dienst verschijnt niet in filters | Zoekfilters werkten niet voor nieuwe diensten, technische fout in applicatie | [#345](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/345) | [ ] |
+| [346](issues/346.md) | Zoeken: paginering werkt niet | Paginering toonde dezelfde resultaten door fout in performance refactor | [#346](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/346) | [ ] |
+| [347](issues/347.md) | Zoeken: Dienstkaartje toont array | Diensttypen werden als array getoond, grafische weergavefout | [#347](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/347) | [ ] |
+| [348](issues/348.md) | Het aantal standaarden komen niet overeen bij Centric Begraven tussen de huidige softwarecatalogus en de nieuwe | Aantal standaarden klopte niet door weergavefout in applicatie | [#348](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/348) | [ ] |
+| [351](issues/351.md) | Het laden van de tabbladen gaat ongelijk | Tabbladen laden ongelijk door configuratiefout op acceptatieomgeving | [#351](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/351) | [ ] |
+| [352](issues/352.md) | Mijn account - Contactpersoon bij applicatie publiceren is niet veranderd ondanks aanpassing zojuist. | Contactpersoon niet bijgewerkt na wijziging account door backend-logica oversight | [#352](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/352) | [ ] |
+| [354](issues/354.md) | Diensten - incomplete lijst applicaties | Zoeken naar applicaties in diensten-dropdown werkt onvoorspelbaar | [#354](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/354) | [ ] |
+| [364](issues/364.md) | Contactpersonen: e-mailadres is leeg | E-mailadres leeg bij contactpersonen ondanks opgave, zelfde oorzaak als #352 | [#364](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/364) | [ ] |
+| [365](issues/365.md) | Contactpersonen: error bij het opslaan van een contactpersoon | 400-error bij opslaan contactpersoon na bewerken | [#365](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/365) | [ ] |
+| [367](issues/367.md) | Contactpersonen: Tussenvoegsel wordt niet getoond | Tussenvoegsel niet getoond in kolom Naam bij contactpersonen | [#367](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/367) | [ ] |
+| [371](issues/371.md) | Applicatie: UUID onder compliance | UUID getoond onder Compliance-kolom, weergavefout door naamloos compliance-object | [#371](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/371) | [ ] |
+| [373](issues/373.md) | Applicatie: Gekoppelde diensten worden niet getoond | Gekoppelde diensten worden niet getoond in applicatie-overzicht | [#373](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/373) | [ ] |
+| [375](issues/375.md) | Applicaties: versie voor SaaS applicaties? | SaaS-applicatie krijgt geen default versie bij aanmaken | [#375](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/375) | [ ] |
+| [377](issues/377.md) | Applicaties: tabel toont diensten niet | Diensten worden niet getoond in kolom Diensten bij applicaties | [#377](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/377) | [ ] |
+| [378](issues/378.md) | Applicatie: Standaarden na wijzigen veranderd | Standaardwaarden wijzigen naar "Ondersteund" na opslaan zonder wijziging | [#378](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/378) | [ ] |
+| [379](issues/379.md) | Applicatie: verschillende manier van tonen compliancy | Compliancy tabel inconsistent weergegeven op verschillende pagina's | [#379](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/379) | [ ] |
+| [392](issues/392.md) | Back-end: geimporteerde gebruiker geeft error bij omzetten naar user | Geimporteerde gebruiker omzetten naar user geeft 400-error bij opslaan | [#392](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/392) | [ ] |
+| [393](issues/393.md) | Backend: fouten in voorzieningenregister | Backend fouten: exports en schema-opvragen werken niet correct | [#393](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/393) | [ ] |
+| [394](issues/394.md) | Contactpersonen van gemeenten publiekelijke zichtbaar | Contactpersonen van gemeenten onterecht publiek zichtbaar (RBAC-regressie) | [#394](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/394) | [ ] |
+| [399](issues/399.md) | Versies: een versie van een applicatie van een andere leverancier levert een foutmelding | Versie van applicatie andere leverancier geeft foutmelding (RBAC) | [#399](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/399) | [ ] |
+| [404](issues/404.md) | Regelmatig witte schermen | Regelmatig witte schermen in Edge browser | [#404](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/404) | [ ] |
+| [407](issues/407.md) | Toegevoegde standaarden verwijzen naar id-id-.... | Standaard-links bevatten dubbel "id-id-" in URL | [#407](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/407) | [ ] |
+| [430](issues/430.md) | Issue #430 | Kolom Compliancy toont applicatienamen in plaats van compliancy-waarden | [#430](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/430) | [ ] |
+| [431](issues/431.md) | Issue #431 | Veld "Tussenvoegsel" ontbreekt in aanmeldproces (regressie) | [#431](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/431) | [ ] |
+| [434](issues/434.md) | Issue #434 | Eerste contactpersoon van nieuwe leverancier niet beschikbaar voor applicatie | [#434](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/434) | [ ] |
+| [436](issues/436.md) | Issue #436 | Error bij ophalen applicatie-overzicht | [#436](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/436) | [ ] |
+| [437](issues/437.md) | Geimporteerde leverancier: nieuwe koppeling opslaan geeft foutmelding | Nieuwe koppeling opslaan geeft 400-error bij geimporteerde leverancier | [#437](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/437) | [ ] |
+| [438](issues/438.md) | Zoeken: verschillende vormgeving Diensten na filteren | Zoekresultaten diensten tonen inconsistent diensttype afhankelijk van filter | [#438](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/438) | [ ] |
+| [439](issues/439.md) | Error na het openen van Applicatie-overzicht | Error en PHP-warnings bij openen applicatie-overzicht nieuwe leverancier | [#439](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/439) | [ ] |
+| [442](issues/442.md) | Applicaties: opgevoerd document wijzigt van naam naar bewijs_ | Documentnaam wijzigt naar "bewijs_" na uploaden bij standaard | [#442](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/442) | [ ] |
+| [451](issues/451.md) | Koppeling: UUID's zichtbaar bij standaardversies | Standaardversies tonen UUID's i.p.v. namen bij koppelingen | [#451](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/451) | [ ] |
+| [452](issues/452.md) | Applicaties overzicht: toont niet alle koppelingen | Applicatie-overzicht toont niet alle koppelingen in kolom | [#452](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/452) | [ ] |
+| [453](issues/453.md) | Zoeken: filters van slag met filter Type=Koppeling | Filters raken van slag bij Type=Koppeling; facets niet correct gescooped | [#453](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/453) | [ ] |
+| [454](issues/454.md) | Wizard koppelingen: Reeds bestaande koppelingen niet gevonden | Cross-supplier koppelingen niet zichtbaar in wizard | [#454](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/454) | [ ] |
+| [455](issues/455.md) | Tabblad koppelingen en contactpersonen publiekelijk niet getoond | Koppelingen en contactpersonen tabs niet zichtbaar voor publiek (RBAC) | [#455](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/455) | [ ] |
+| [456](issues/456.md) | Consistentie in werking van wizards | Wizard afsluiting inconsistent qua tekst, knoppen en flow | [#456](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/456) | [ ] |
+
+### Datakwaliteit (10)
+
+| # | Issue | Analyse | GitHub | Checked |
+|---|-------|---------|--------|--------|
+| [23](issues/23.md) | Als aanbod- en gebruik-beheerder van de huidige Softwarecatalogus wil ik mijn reeds geregistreerde gegevens weer zien in de nieuwe Softwarecatalogus | Datamigratie vanuit oude softwarecatalogus; importkwaliteit en ontbrekende relaties | [#23](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/23) | [ ] |
+| [186](issues/186.md) | Koppelingen | Koppelingen verwijzen naar niet-bestaande applicaties uit importdata | [#186](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/186) | [ ] |
+| [312](issues/312.md) | Koppeling heeft verplicht een naam | Koppelingen zonder naam door ontbrekende naamvelden in importdata | [#312](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/312) | [ ] |
+| [349](issues/349.md) | Zoeken: UUID’s onder standaarden filter. | UUID's in standaardenfilter door verouderde verwijzingen in GEMMA-importdata | [#349](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/349) | [ ] |
+| [401](issues/401.md) | Koppeling - geïmporteerde koppelingen kaartjes zijn leeg | Geimporteerde koppelingen leeg door ontbrekende modules en standaardversies in importdata | [#401](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/401) | [ ] |
+| [432](issues/432.md) | Issue #432 | Naamgeving koppelingen inconsistent door importproblemen en ontbrekende velden | [#432](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/432) | [ ] |
+| [433](issues/433.md) | Issue #433 | Import koppelingen vult verkeerde velden, modules niet correct gekoppeld | [#433](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/433) | [ ] |
+| [435](issues/435.md) | Issue #435 | Niet alle geimporteerde applicaties zichtbaar, Centric mist 7 van 39 pakketten | [#435](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/435) | [ ] |
+| [440](issues/440.md) | Zoeken: Organisatietype teveel aan opties | Facet Organisatietype toont teveel opties uit geimporteerde data | [#440](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/440) | [ ] |
+| [441](issues/441.md) | Applicaties: mapping van de versies gaat niet goed bij geimporteerde applicaties | Versie-mapping geimporteerde applicaties toont geen status en startdatum | [#441](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/441) | [ ] |
+
+### Tekstueel (7)
+
+| # | Issue | Analyse | GitHub | Checked |
+|---|-------|---------|--------|--------|
+| [248](issues/248.md) | Titels van de tabs in orde maken | Tabbladen missen titels, alleen iconen zonder tekst | [#248](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/248) | [ ] |
+| [274](issues/274.md) | Wizard dienst: tekst dient nog aangepast te worden naar nieuwe benamingen | Wizard dienst bevat oude benamingen, tekst moet aangepast worden | [#274](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/274) | [ ] |
+| [278](issues/278.md) | Filterteksten aanpassen | Filterteksten aanpassen: "Objecttype" moet anders, handleiding klopt niet | [#278](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/278) | [ ] |
+| [357](issues/357.md) | Diensten: Diensttype en Type wordt door elkaar gebruikt | Termen Diensttype en Type inconsistent gebruikt, configuratiefout in weergave | [#357](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/357) | [ ] |
+| [376](issues/376.md) | Applicaties: labels wizard en tabel zijn anders | Labels in beheertabel wijken af van wizard en powerpoint | [#376](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/376) | [ ] |
+| [381](issues/381.md) | Applicaties: non-compliant vervangen door niet ondersteund | Tekst "non-compliant" moet "niet ondersteund" worden | [#381](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/381) | [ ] |
+| [410](issues/410.md) | Dashboard schrijfwijze softwarecatalogus | Schrijfwijze "softwarecatalogus" inconsistent, moet zonder hoofdletter | [#410](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/410) | [ ] |
+
+### Wens (11)
+
+| # | Issue | Analyse | GitHub | Checked |
+|---|-------|---------|--------|--------|
+| [6](issues/6.md) | Als aanbod-beheerder wil ik kunnen registreren welke standaarden door mijn pakket worden ondersteund en eventueel testrapporten beschikbaar stellen | Registreren standaarden bij pakketten en testrapporten beschikbaar stellen | [#6](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/6) | [ ] |
+| [85](issues/85.md) | (VNGR) Als ontwikkelaar wil ik via een veilige, publieke API toegang hebben tot aanbodinformatie uit de Softwarecatalogus ID-104 | Publieke API voor aanbodinformatie; documentatie en endpoints ontbreken nog | [#85](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/85) | [ ] |
+| [141](issues/141.md) | Als functioneel beheerder wil ik, naar aanleiding van gemeentelijke herindeling of een leveranciersovername, organisaties en al hun relaties (aanbod en/of gebruik) kunnen samenvoegen met een bestaande of nieuwe organisatie | Organisaties samenvoegen bij herindeling of overname, handleiding ontbreekt | [#141](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/141) | [ ] |
+| [148](issues/148.md) | (VNGR) De GEMMA-architectuur is opvraagbaar met een API | GEMMA-architectuur API verbeteren: meerdere modellen, minder id's, documentatie | [#148](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/148) | [ ] |
+| [155](issues/155.md) | (VNGR) Definities worden weergegeven via een interactieve optie binnen de softwarecatalogus | Glossary/begrippenlijst interactief weergeven binnen de softwarecatalogus | [#155](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/155) | [ ] |
+| [160](issues/160.md) | (VNGR) Performance plotten views tbv ID-77 | Performance verbeteren bij het plotten van grote ArchiMate views | [#160](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/160) | [ ] |
+| [306](issues/306.md) | Dienst: Overzicht controleren verbeteren | Overzicht controleren van dienst verbeteren, overbodige velden verwijderen | [#306](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/306) | [ ] |
+| [332](issues/332.md) | Voorpagina inrichten | Voorpagina inrichten met aanpasbare CMS-content | [#332](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/332) | [ ] |
+| [343](issues/343.md) | Zoeken: Filter 'Type koppeling' toevoegen. | Filter "Type koppeling" toevoegen aan zoekpagina | [#343](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/343) | [ ] |
+| [366](issues/366.md) | Contactpersonen: veld Rollen niet consistent | Veld Rollen verbergen bij leveranciers, tonen bij gemeenten met juiste waarden | [#366](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/366) | [ ] |
+| [370](issues/370.md) | Applicatie: teveel kolommen worden getoond | Technische kolommen verbergen in applicatie-overzicht | [#370](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/370) | [ ] |
+
+### Nog te bepalen (3)
+
+| # | Issue | Analyse | GitHub | Checked |
+|---|-------|---------|--------|--------|
+| [340](issues/340.md) | Bevindingen op tussenoplevering Zoeken | Meerdere bevindingen zoeken: performance, sortering, filters, tekst | [#340](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/340) | [ ] |
+| [391](issues/391.md) | Testen met een gebruiker van een bestaande organisatie | Testen met geimporteerde gebruiker/organisatie nog niet volledig mogelijk | [#391](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/391) | [ ] |
+| [402](issues/402.md) | Verschil tussen Edge en Chrome bij laden applicaties | Verschil tussen Edge en Chrome niet reproduceerbaar | [#402](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/402) | [ ] |
+
+---
+
+## Gesloten issues
+*74 issues*
+
+### Bug (47)
+
+| # | Issue | Analyse | GitHub | Checked |
+|---|-------|---------|--------|--------|
+| [66](issues/66.md) | Als aanbod-beheerder wil ik aanvullende informatie over mijn organisatie kunnen delen, een overzicht van de diensten, en links naar ondersteunende pagina's (zoals het support-portaal en handleidingen) | Organisatieformulier bevat fouten: verkeerde navigatie, publiceren werkt niet | [#66](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/66) | [ ] |
+| [172](issues/172.md) | Testresultaten Jeroen de Ruig 5/9/2025 acceptatietest | Diverse bugs bij wizard: cache-problemen, velden niet gevuld, verkeerde data | [#172](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/172) | [ ] |
+| [185](issues/185.md) | Detailpagina's | Detailpagina's missen beschrijvingen, tabs en naam; inconsistente vormgeving | [#185](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/185) | [ ] |
+| [189](issues/189.md) | Organisatie | Organisatiebeheer: te veel velden, logo-upload fout, gebruikers niet zichtbaar | [#189](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/189) | [ ] |
+| [190](issues/190.md) | Applicatie en diensten | Applicatie aanmelden geeft lege pagina, licentievorm-selectie klopt niet | [#190](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/190) | [ ] |
+| [191](issues/191.md) | Back-End | Gebruiker toevoegen via backend geeft foutmelding | [#191](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/191) | [ ] |
+| [264](issues/264.md) | Tekst aanleveren: 404-melding bij Niet ingelogd: onder een applicatie staat in het tabje gebruik de gemeenten | 404-melding bij klikken op gebruik-tab als niet-ingelogde bezoeker | [#264](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/264) | [ ] |
+| [265](issues/265.md) | Nieuwe gebruiker heeft software-catalog-users | Nieuwe gebruiker krijgt verkeerde rol (software-catalog-users ipv aanbod-beheerder) | [#265](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/265) | [ ] |
+| [266](issues/266.md) | Na inloggen: Mijn account & persoonlijke gegevens leeg? | Mijn account en persoonlijke gegevens leeg na inloggen | [#266](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/266) | [ ] |
+| [273](issues/273.md) | Wizard Dienst: Een zojuist opgevoerde applicatie wordt niet direct gevonden | Zojuist opgevoerde applicatie niet direct vindbaar in wizard Dienst | [#273](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/273) | [ ] |
+| [283](issues/283.md) | Zoeken > Applicatie: Tab gebruik zichtbaar & versies | Tab gebruik onterecht zichtbaar, versies tonen "onbekend" | [#283](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/283) | [ ] |
+| [284](issues/284.md) | Applicatie: toont standaarden ipv standaardversies | Standaarden getoond ipv standaardversies bij applicatie | [#284](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/284) | [ ] |
+| [285](issues/285.md) | Zoeken: zojuist aangemaakte organisatie wordt niet gevonden | Zojuist aangemaakte organisatie niet vindbaar in zoekresultaten | [#285](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/285) | [ ] |
+| [286](issues/286.md) | Aanmelden organisatie: 500-error bij wachtwoord wijzigen | 500-error bij wachtwoord wijzigen van gebruiker | [#286](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/286) | [ ] |
+| [287](issues/287.md) | Leverancier: tab met grafiek toont overige applicaties die onder Applicatie horen | Grafiek-tab toonde verkeerde applicaties door limiet van 30 resultaten | [#287](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/287) | [ ] |
+| [288](issues/288.md) | Beheer: Wizards voor een leverancier | Wizards voor leverancier werden niet correct getoond | [#288](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/288) | [ ] |
+| [289](issues/289.md) | Beheer: tabelvoorkeuren worden niet bewaard | Tabelvoorkeuren werden niet bewaard na sessie | [#289](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/289) | [ ] |
+| [290](issues/290.md) | Beheer: Contactpersonen zoeken werkt niet | Zoeken op contactpersonen werkte niet in beheer | [#290](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/290) | [ ] |
+| [291](issues/291.md) | Beheer: Organisatie bewerken via contactpersoon | Organisatie bewerken via contactpersoon werkte niet correct | [#291](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/291) | [ ] |
+| [294](issues/294.md) | Applicatie publiceren: uitlijning rechthoek om op te voeren. | Uitlijning rechthoek gaat niet goed bij referentiecomponent selectie | [#294](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/294) | [ ] |
+| [295](issues/295.md) | Applicatie publiceren: Koppeling veld is smal | Koppeling veld te smal in wizard applicatie publiceren | [#295](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/295) | [ ] |
+| [297](issues/297.md) | Applicatie publiceren: koppeling applicatie B niet te selecteren | Applicatie B niet selecteerbaar bij koppeling publiceren | [#297](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/297) | [ ] |
+| [299](issues/299.md) | Beheer: Applicatiedetail Diensten tab kaartje zonder tekst | Diensten tab toonde kaartje zonder tekst | [#299](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/299) | [ ] |
+| [300](issues/300.md) | Beheer: overzicht applicaties teveel applicaties | Beheer toonde teveel applicaties door ontbrekende RBAC-filtering | [#300](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/300) | [ ] |
+| [301](issues/301.md) | Beheer: overzicht applicaties sorteren werkt niet | Sorteren in applicatie-overzicht werkte niet | [#301](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/301) | [ ] |
+| [302](issues/302.md) | Beheer: applicatie bewerken (ophalen van gegevens is traag) | Ophalen applicatiegegevens in beheer was te traag | [#302](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/302) | [ ] |
+| [303](issues/303.md) | Beheer: applicatie bewerken dienst toevoegen ontbreekt | Knop "dienst toevoegen" ontbrak in applicatie bewerken | [#303](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/303) | [ ] |
+| [304](issues/304.md) | Dienst bewerken: formulier teveel velden | Formulier dienst bewerken toonde teveel velden | [#304](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/304) | [ ] |
+| [305](issues/305.md) | Dienst: multiselect diensttypen + label aanpassen | Diensttypen multiselect ontbrak, label was onjuist | [#305](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/305) | [ ] |
+| [307](issues/307.md) | Diensten overzicht: meer dienst bij organisatie dan er horen | Diensten overzicht toonde meer diensten dan bij organisatie horen | [#307](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/307) | [ ] |
+| [330](issues/330.md) | /Beheer pagina's rerouten | Beheer pagina's automatisch gegenereerd en ongewenst vindbaar | [#330](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/330) | [ ] |
+| [337](issues/337.md) | Applicatie melden wizard | Wizard applicatie melden toonde verkeerde data en laadde traag | [#337](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/337) | [ ] |
+| [353](issues/353.md) | Mijn account – Je “functie” wordt niet aangepast na bewerken en opslaan. Cache legen werkt ook niet | Functie niet opgeslagen na bewerken, zelfde oorzaak als #352 | [#353](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/353) | [ ] |
+| [356](issues/356.md) | Diensten: geen tussenvoegsel bij namen | Tussenvoegsel ontbreekt in contactpersoon-weergave | [#356](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/356) | [ ] |
+| [368](issues/368.md) | Applicatie publiceren: Zonder een richting aan te geven is de koppeling op te voeren | Koppeling kon worden aangemaakt zonder verplicht veld Richting, validatie ontbrak | [#368](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/368) | [ ] |
+| [369](issues/369.md) | Applicatie publiceren: de aangemaakte koppeling is niet zichtbaar | Aangemaakte koppeling niet zichtbaar door RBAC-bug | [#369](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/369) | [ ] |
+| [372](issues/372.md) | Applicaties: Kolom Contactpersoon toont geen tussenvoegsel | Tussenvoegsel ontbreekt in kolom Contactpersoon bij applicaties | [#372](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/372) | [ ] |
+| [380](issues/380.md) | Applicatie: compliance aantallen komen niet overeen | Compliance aantallen wizard en beheerpagina kwamen niet overeen | [#380](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/380) | [ ] |
+| [382](issues/382.md) | Applicatie: compliancy link werkt niet | Compliancy link zonder protocol wordt als relatieve URL behandeld | [#382](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/382) | [ ] |
+| [383](issues/383.md) | Applicatie: selectie vakken werken niet | Selectievakjes in applicatieoverzicht werkten niet meer (regressie) | [#383](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/383) | [ ] |
+| [389](issues/389.md) | Applicaties – Uw applicatie publiceren: link verdwijnt na klikken (2) | Link-onderstreping verdwijnt tijdelijk na klikken (CSS-gedrag) | [#389](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/389) | [ ] |
+| [395](issues/395.md) | Menu linkerkant verdwijnt | Linkermenu verdwijnt na F5/pagina herladen | [#395](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/395) | [ ] |
+| [397](issues/397.md) | Pagina aanmaken via CMS | CMS pagina's bewerken werkt niet meer (regressie door performance-wijziging) | [#397](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/397) | [ ] |
+| [400](issues/400.md) | Koppeling - Opslaan van een koppeling geeft een foutmelding | Opslaan van een koppeling geeft foutmelding | [#400](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/400) | [ ] |
+| [408](issues/408.md) | Tabblad beschrijving bij Dienst | Onterecht tabblad "Beschrijving" met getal bij nieuw aangemaakte dienst | [#408](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/408) | [ ] |
+| [409](issues/409.md) | Footer anders: inlog of uitgelogd | Footer verschilt tussen ingelogd en uitgelogd, links wijzen naar andere pagina's | [#409](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/409) | [ ] |
+| [225](issues/225.md) | Testresultaten 29-10-2025 | Ingelogde gebruiker ziet eigen applicaties niet, publiceren werkt niet correct | [#225](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/225) | [ ] |
+
+### Datakwaliteit (3)
+
+| # | Issue | Analyse | GitHub | Checked |
+|---|-------|---------|--------|--------|
+| [3](issues/3.md) | Als aanbod-raadpleger wil ik pakketten kunnen zoeken en filteren op ondersteuning van verplichte en aanbevolen standaarden | UUID's in standaarden-filter door niet-bestaande standaarden in importdata | [#3](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/3) | [ ] |
+| [292](issues/292.md) | Applicatie publiceren: lijst met onbekende contactpersonen | Onbekende contactpersonen in wizard door geimporteerde data | [#292](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/292) | [ ] |
+| [315](issues/315.md) | Hoge prioriteit: Zoekpagina toont deel van gemeentelijk applicatielandschap | Zoekpagina toont gemeenten als leverancier door fout in importdata | [#315](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/315) | [ ] |
+
+### Tekstueel (13)
+
+| # | Issue | Analyse | GitHub | Checked |
+|---|-------|---------|--------|--------|
+| [254](issues/254.md) | Beheer pagina's dashboard: wizard benamingen | Wizard benamingen op dashboard kloppen niet | [#254](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/254) | [ ] |
+| [267](issues/267.md) | Naam is softwarecatalogus i.p.v. VNG softwarecatalogus | Naam is "softwarecatalogus" in plaats van "VNG softwarecatalogus" | [#267](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/267) | [ ] |
+| [277](issues/277.md) | Beheer: Applicaties overzicht teksten aanpassen | Beheer applicaties overzicht bevat verkeerde teksten | [#277](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/277) | [ ] |
+| [359](issues/359.md) | Diensten wizard: Uw dienst publiceren - tekst aanpassen | Informatieteksten in wizard komen niet overeen met afgestemde powerpoint | [#359](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/359) | [ ] |
+| [360](issues/360.md) | Diensten wizard – Uw dienst publiceren: Meerdere i komen niet overeen met ppt | Informatieteksten stap 2 wizard komen niet overeen met powerpoint | [#360](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/360) | [ ] |
+| [361](issues/361.md) | Diensten wizard – Uw dienst publiceren: inconsistentie in labels | Labels controleformulier wijken af van invoervelden in wizard | [#361](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/361) | [ ] |
+| [362](issues/362.md) | Diensten wizard – Uw dienst publiceren: onlogische tekst bovenaan de aanmeld-stap | Onlogische tekst "Uw diensten publiceren" na succesvolle aanmelding | [#362](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/362) | [ ] |
+| [363](issues/363.md) | Diensten wizard – Uw dienst publiceren: catalogus i.p.v. softwarecatalogus | "Catalogus" moet "softwarecatalogus" zijn in bevestigingstekst | [#363](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/363) | [ ] |
+| [374](issues/374.md) | Applicaties: Standaarden, Standaarden GEMMA en Standaardversies? | Kolommen Standaarden/Standaarden GEMMA/Standaardversies verwarrend en dubbel | [#374](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/374) | [ ] |
+| [386](issues/386.md) | Applicaties – Uw applicatie publiceren: andere labels | Labels in wizard komen niet overeen met ontwerp (ppt) | [#386](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/386) | [ ] |
+| [387](issues/387.md) | Applicaties – Uw applicatie publiceren: i niet aanwezig | Informatie-iconen ontbreken bij labels in versie-wizard | [#387](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/387) | [ ] |
+| [390](issues/390.md) | Applicaties – Uw applicatie publiceren: labels komen niet overeen | Labels wizard en controleformulier komen niet overeen | [#390](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/390) | [ ] |
+| [403](issues/403.md) | Tekst verwijderen aanpassen | Verwijdertekst per objecttype moet aangepast worden | [#403](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/403) | [ ] |
+
+### Wens (10)
+
+| # | Issue | Analyse | GitHub | Checked |
+|---|-------|---------|--------|--------|
+| [4](issues/4.md) | Als aanbod-beheerder wil ik mijn pakketten eenmalig registreren en classificeren op basis van de referentiearchitecturen van de voor mij relevante sector(en) | Classificeren op meerdere sectorale referentiearchitecturen, buiten scope | [#4](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/4) | [ ] |
+| [298](issues/298.md) | Applicatie publiceren: Buitengemeentelijke voorzieningen herkenbaarder maken tussen applicaties | Buitengemeentelijke voorzieningen visueel onderscheiden van applicaties | [#298](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/298) | [ ] |
+| [308](issues/308.md) | Diensten overzicht: default kolommen + kolom verwijderen | Default kolommen aanpassen en kolom koppelingen verbergen bij diensten | [#308](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/308) | [ ] |
+| [350](issues/350.md) | De link achter de gebruikersnaam laten verwijzen naar Mij account | Link achter gebruikersnaam naar Mijn Account laten verwijzen | [#350](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/350) | [ ] |
+| [355](issues/355.md) | Diensten: Export geeft allerlei UUID's | Export toont UUID's, wens voor leesbare variant naast technische export | [#355](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/355) | [ ] |
+| [358](issues/358.md) | Diensten: De status "Concept" wordt nog op verschillende plekken getoond | Status "Concept" verwijderen, niet in PvE maar wel in datamodel | [#358](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/358) | [ ] |
+| [384](issues/384.md) | Applicaties: eenduidige manier van bewerken | Overal wizards gebruiken voor bewerken, niet meerdere manieren | [#384](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/384) | [ ] |
+| [385](issues/385.md) | Applicatie: Geen huidige versie in gebruik | "Huidige versie" verwijderen uit grijze blok, staat al in tabblad Versies | [#385](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/385) | [ ] |
+| [396](issues/396.md) | Verouderde NextCloud versie | Nextcloud versie upgraden naar ondersteunde versie | [#396](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/396) | [ ] |
+| [406](issues/406.md) | SiteImprove verwijderen | SiteImprove tracking script verwijderen uit template | [#406](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/406) | [ ] |
+
+### Nog te bepalen (1)
+
+| # | Issue | Analyse | GitHub | Checked |
+|---|-------|---------|--------|--------|
+| [334](issues/334.md) | Zoeken | Meerdere zoek-bevindingen: UUIDs, filters, tekstueel, RBAC-gevolgen | [#334](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/334) | [ ] |
+
diff --git a/add_csv_schemas.php b/add_csv_schemas.php
deleted file mode 100644
index 576c9368..00000000
--- a/add_csv_schemas.php
+++ /dev/null
@@ -1,220 +0,0 @@
- $header) {
- if (!preg_match('/^[_@]/', $header) && $header !== 'id') {
- $validHeaders[] = $header;
- $validIndices[] = $index;
- }
- }
-
- // Create properties
- $properties = [];
- foreach ($validHeaders as $headerIndex => $header) {
- $sampleValues = [];
- foreach ($sampleRows as $row) {
- if (isset($row[$validIndices[$headerIndex]])) {
- $sampleValues[] = $row[$validIndices[$headerIndex]];
- }
- }
-
- $type = determinePropertyType($sampleValues);
-
- $properties[$header] = [
- "description" => "",
- "type" => $type,
- "order" => $headerIndex + 1,
- "title" => ucfirst(str_replace('_', ' ', $header))
- ];
- }
-
- // Create schema
- $schema = [
- "uri" => null,
- "slug" => $schemaName,
- "title" => $schemaTitle,
- "description" => $schemaDescription,
- "version" => "0.0.1",
- "summary" => "",
- "icon" => null,
- "required" => [],
- "properties" => $properties,
- "archive" => [],
- "source" => "",
- "hardValidation" => false,
- "updated" => date('Y-m-d\TH:i:s+00:00'),
- "created" => date('Y-m-d\TH:i:s+00:00'),
- "maxDepth" => 0,
- "owner" => "system",
- "application" => null,
- "organisation" => "cb2bca24-40bf-4568-a138-454c63ab761c",
- "groups" => null,
- "authorization" => null,
- "deleted" => null,
- "configuration" => null
- ];
-
- return $schema;
-}
-
-// Define schemas to create
-$schemasToCreate = [
- [
- 'csvFile' => 'lib/Settings/koppeling.csv',
- 'schemaName' => 'koppeling',
- 'schemaTitle' => 'Koppeling',
- 'schemaDescription' => 'Schema voor koppelingen tussen modules en systemen'
- ],
- [
- 'csvFile' => 'lib/Settings/koppelingGebruik.csv',
- 'schemaName' => 'koppelingGebruik',
- 'schemaTitle' => 'Koppeling Gebruik',
- 'schemaDescription' => 'Schema voor het gebruik van koppelingen'
- ],
- [
- 'csvFile' => 'lib/Settings/compliancy.csv',
- 'schemaName' => 'compliancy',
- 'schemaTitle' => 'Compliancy',
- 'schemaDescription' => 'Schema voor compliancy en standaard ondersteuning'
- ],
- [
- 'csvFile' => 'lib/Settings/moduleGebruik.csv',
- 'schemaName' => 'moduleGebruik',
- 'schemaTitle' => 'Module Gebruik',
- 'schemaDescription' => 'Schema voor het gebruik van modules'
- ],
- [
- 'csvFile' => 'lib/Settings/moduleversie.csv',
- 'schemaName' => 'moduleversie',
- 'schemaTitle' => 'Module Versie',
- 'schemaDescription' => 'Schema voor module versies'
- ]
-];
-
-// Add schemas to the voorzieningen register
-$voorzieningenRegister = &$data['components']['registers']['voorzieningen'];
-
-// Add new schemas to the schemas list
-foreach ($schemasToCreate as $schemaConfig) {
- $schema = createSchemaFromCSV($schemaConfig['csvFile'], $schemaConfig['schemaName'], $schemaConfig['schemaTitle'], $schemaConfig['schemaDescription']);
-
- if ($schema) {
- // Add to schemas list if not already present
- if (!in_array($schemaConfig['schemaName'], $voorzieningenRegister['schemas'])) {
- $voorzieningenRegister['schemas'][] = $schemaConfig['schemaName'];
- }
-
- // Add to schemas object
- $data['components']['schemas'][$schemaConfig['schemaName']] = $schema;
-
- echo "Added schema: {$schemaConfig['schemaName']}\n";
- }
-}
-
-// Convert back to JSON with pretty formatting
-$updatedJson = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
-
-if ($updatedJson === false) {
- die("Error: Could not encode JSON\n");
-}
-
-// Create backup
-$backupFile = $jsonFile . '.backup.' . date('Y-m-d_H-i-s');
-if (copy($jsonFile, $backupFile)) {
- echo "Backup created: $backupFile\n";
-} else {
- echo "Warning: Could not create backup\n";
-}
-
-// Write the updated JSON back to file
-if (file_put_contents($jsonFile, $updatedJson) !== false) {
- echo "CSV schemas added successfully!\n";
- echo "Updated file: $jsonFile\n";
-} else {
- die("Error: Could not write to file $jsonFile\n");
-}
-
-echo "Done!\n";
diff --git a/appinfo/info.xml b/appinfo/info.xml
index be5951ae..bf641446 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -2,35 +2,61 @@
softwarecatalog
- Software Catalogus
- Easily manage and publish software catalogs in an open data ecosystem
- Software Catalogus
+ Software Catalogus
+ Manage your software portfolio with applications, modules, and connections
+ Beheer je softwareportfolio met applicaties, modules en koppelingen
+
+
- 0.1.138
+ 0.1.140
agpl
- organization
Conduction
SoftwareCatalog
+
+ https://github.com/ConductionNL/SoftwareCatalogus
+ https://github.com/ConductionNL/SoftwareCatalogus
+ https://github.com/ConductionNL/SoftwareCatalogus
+
+ organization
+ tools
+ integration
https://github.com/ConductionNL/SoftwareCatalogus
+ https://github.com/ConductionNL/SoftwareCatalogus/discussions
https://github.com/ConductionNL/SoftwareCatalogus/issues
https://github.com/ConductionNL/SoftwareCatalogus
+ https://raw.githubusercontent.com/ConductionNL/SoftwareCatalogus/main/img/app-store.svg
+
@@ -46,7 +72,7 @@ Submit a [feature request](https://github.com/OpenCatalogi/.github/issues/new/ch
- OCA\SoftwareCatalog\Cron\OrganizationSync
+ OCA\SoftwareCatalog\BackgroundJob\OrganizationContactSyncJob
diff --git a/composer.json b/composer.json
index 851827c8..a3cdaacb 100644
--- a/composer.json
+++ b/composer.json
@@ -1,7 +1,7 @@
{
"name": "conductionnl/softwarecatalog",
"description": "Quickly build data registers based on schema.json",
- "license": "AGPL-3.0-or-later",
+ "license": "EUPL-1.2",
"authors": [
{
"name": "Conduction b.v.",
@@ -26,15 +26,21 @@
"cs:fix": "./vendor/bin/phpcbf --standard=phpcs.xml",
"phpcs": "./vendor/bin/phpcs --standard=phpcs.xml",
"phpcs:fix": "./vendor/bin/phpcbf --standard=phpcs.xml",
+ "phpcs:output": "./vendor/bin/phpcs --standard=phpcs.xml --report=json lib/ 2>/dev/null | tail -1 > phpcs-output.json",
"phpmd": "phpmd lib text phpmd.xml || echo 'PHPMD not installed, skipping...'",
"phpmetrics": "./vendor/bin/phpmetrics --report-html=phpmetrics lib/",
- "psalm": "psalm --threads=1 --no-cache",
+ "phpmetrics:violations": "./vendor/bin/phpmetrics --violations-xml=phpmetrics/violations.xml lib/",
+ "psalm": "./vendor/bin/psalm --threads=1 --no-cache || echo 'Psalm not installed, skipping...'",
+ "phpstan": "./vendor/bin/phpstan analyse --memory-limit=1G || echo 'PHPStan not installed, skipping...'",
"test:unit": "phpunit tests -c tests/phpunit.xml --colors=always --fail-on-warning --fail-on-risky",
"test:all": "./vendor/bin/phpunit --colors=always || echo 'Tests require Nextcloud environment, skipping...'",
+ "check": "E=0; for CMD in lint phpcs psalm test:unit; do echo; echo \"=== $CMD ===\"; composer $CMD || E=1; done; echo; if [ $E -eq 0 ]; then echo \"ALL CHECKS PASSED\"; else echo \"SOME CHECKS FAILED (see above)\"; fi; exit $E",
+ "check:full": "E=0; for CMD in lint phpcs psalm phpstan test:all; do echo; echo \"=== $CMD ===\"; composer $CMD || E=1; done; echo; if [ $E -eq 0 ]; then echo \"ALL CHECKS PASSED\"; else echo \"SOME CHECKS FAILED (see above)\"; fi; exit $E",
+ "check:strict": "E=0; for CMD in lint phpcs phpmd psalm phpstan test:all; do echo; echo \"=== $CMD ===\"; composer $CMD || E=1; done; echo; if [ $E -eq 0 ]; then echo \"ALL CHECKS PASSED\"; else echo \"SOME CHECKS FAILED (see above)\"; fi; exit $E",
+ "fix": [
+ "@cs:fix"
+ ],
"openapi": "generate-spec",
- "grumphp": "./vendor/bin/grumphp run",
- "grumphp:init": "./vendor/bin/grumphp git:init",
- "grumphp:deinit": "./vendor/bin/grumphp git:deinit",
"phpqa": "./vendor/bin/phpqa --report --analyzedDirs lib --buildDir phpqa",
"phpqa:full": "./vendor/bin/phpqa --report --analyzedDirs lib --buildDir phpqa --tools phpcs:0,phpmd:0,phploc:0,phpmetrics,phpcpd:0,parallel-lint:0",
"phpqa:ci": "./vendor/bin/phpqa --report --analyzedDirs lib --buildDir phpqa --tools phpcs,phpmd,phploc,phpmetrics,phpcpd,parallel-lint",
@@ -43,6 +49,18 @@
],
"qa:full": [
"@phpqa:full"
+ ],
+ "test:coverage": "./vendor/bin/phpunit --coverage-html=coverage/html --coverage-clover=coverage/clover.xml --colors=always",
+ "coverage:check": "php -r \"\\$xml = simplexml_load_file('coverage/clover.xml'); \\$metrics = \\$xml->project->metrics; \\$statements = (int)\\$metrics['statements']; \\$covered = (int)\\$metrics['coveredstatements']; \\$percentage = \\$statements > 0 ? round((\\$covered / \\$statements) * 100, 2) : 0; echo 'Coverage: ' . \\$percentage . '%' . PHP_EOL; exit(\\$percentage < 75 ? 1 : 0);\"",
+ "quality:phpcs-score": "./vendor/bin/phpcs --standard=phpcs.xml --report=json lib/ | php -r \"\\$json = json_decode(file_get_contents('php://stdin'), true); \\$errors = \\$json['totals']['errors'] ?? 0; \\$warnings = \\$json['totals']['warnings'] ?? 0; \\$score = 1000 - \\$errors - (\\$warnings / 2); echo 'PHPCS Score: ' . \\$score . ' (Errors: ' . \\$errors . ', Warnings: ' . \\$warnings . ')' . PHP_EOL;\"",
+ "quality:phpmd-score": "phpmd lib/ json phpmd.xml | php -r \"\\$input = file_get_contents('php://stdin'); \\$json = json_decode(\\$input, true); \\$violations = count(\\$json['files'] ?? []); \\$score = 1000 - (\\$violations * 10); echo 'PHPMD Score: ' . \\$score . ' (Violations: ' . \\$violations . ')' . PHP_EOL;\" || echo 'PHPMD not available'",
+ "quality:psalm-score": "./vendor/bin/psalm --output-format=json --no-cache | php -r \"\\$input = file_get_contents('php://stdin'); \\$json = json_decode(\\$input, true); \\$errors = count(\\$json ?? []); \\$score = 1000 - (\\$errors * 5); echo 'Psalm Score: ' . \\$score . ' (Errors: ' . \\$errors . ')' . PHP_EOL;\" || echo 'Psalm not available'",
+ "quality:phpstan-score": "./vendor/bin/phpstan analyse --memory-limit=1G --error-format=json --no-progress | php -r \"\\$input = file_get_contents('php://stdin'); \\$json = json_decode(\\$input, true); \\$errors = \\$json['totals']['file_errors'] ?? 0; \\$score = 1000 - (\\$errors * 5); echo 'PHPStan Score: ' . \\$score . ' (Errors: ' . \\$errors . ')' . PHP_EOL;\" || echo 'PHPStan not available'",
+ "quality:score": [
+ "@quality:phpcs-score",
+ "@quality:phpmd-score",
+ "@quality:psalm-score",
+ "@quality:phpstan-score"
]
},
"require": {
@@ -57,20 +75,23 @@
"twig/twig": "^3.8"
},
"require-dev": {
- "edgedesign/phpqa": "^1.30",
+ "edgedesign/phpqa": "^1.27",
"guzzlehttp/guzzle": "^7.8",
+ "nextcloud/coding-standard": "^1.4",
+ "nextcloud/ocp": "^31.0",
+ "phpcsstandards/phpcsextra": "^1.4",
"phpmd/phpmd": "^2.15",
"phpmetrics/phpmetrics": "^2.8",
- "phpro/grumphp": "^2.9",
+ "phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.5",
"roave/security-advisories": "dev-latest",
- "squizlabs/php_codesniffer": "^3.9"
+ "squizlabs/php_codesniffer": "^3.9",
+ "vimeo/psalm": "^5.26"
},
"config": {
"allow-plugins": {
"bamarni/composer-bin-plugin": true,
"php-http/discovery": true,
- "phpro/grumphp": true,
"dealerdirect/phpcodesniffer-composer-installer": true
},
"optimize-autoloader": true,
diff --git a/composer.lock b/composer.lock
index 2575ce45..a961af8d 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "2885ec6fd5c8957ad5387d30123fd28c",
+ "content-hash": "5cc554989deac551468cfa7c24335c43",
"packages": [
{
"name": "adbario/php-dot-notation",
@@ -62,16 +62,16 @@
},
{
"name": "bamarni/composer-bin-plugin",
- "version": "1.8.2",
+ "version": "1.9.1",
"source": {
"type": "git",
"url": "https://github.com/bamarni/composer-bin-plugin.git",
- "reference": "92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880"
+ "reference": "641d0663f5ac270b1aeec4337b7856f76204df47"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/bamarni/composer-bin-plugin/zipball/92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880",
- "reference": "92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880",
+ "url": "https://api.github.com/repos/bamarni/composer-bin-plugin/zipball/641d0663f5ac270b1aeec4337b7856f76204df47",
+ "reference": "641d0663f5ac270b1aeec4337b7856f76204df47",
"shasum": ""
},
"require": {
@@ -79,12 +79,12 @@
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
- "composer/composer": "^2.0",
+ "composer/composer": "^2.2.26",
"ext-json": "*",
"phpstan/extension-installer": "^1.1",
- "phpstan/phpstan": "^1.8",
- "phpstan/phpstan-phpunit": "^1.1",
- "phpunit/phpunit": "^8.5 || ^9.5",
+ "phpstan/phpstan": "^1.8 || ^2.0",
+ "phpstan/phpstan-phpunit": "^1.1 || ^2.0",
+ "phpunit/phpunit": "^8.5 || ^9.6 || ^10.0",
"symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0",
"symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0",
"symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0"
@@ -113,9 +113,9 @@
],
"support": {
"issues": "https://github.com/bamarni/composer-bin-plugin/issues",
- "source": "https://github.com/bamarni/composer-bin-plugin/tree/1.8.2"
+ "source": "https://github.com/bamarni/composer-bin-plugin/tree/1.9.1"
},
- "time": "2022-10-31T08:38:03+00:00"
+ "time": "2026-02-04T10:18:12+00:00"
},
{
"name": "doctrine/lexer",
@@ -416,16 +416,16 @@
},
{
"name": "react/event-loop",
- "version": "v1.5.0",
+ "version": "v1.6.0",
"source": {
"type": "git",
"url": "https://github.com/reactphp/event-loop.git",
- "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354"
+ "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354",
- "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354",
+ "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a",
+ "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a",
"shasum": ""
},
"require": {
@@ -476,7 +476,7 @@
],
"support": {
"issues": "https://github.com/reactphp/event-loop/issues",
- "source": "https://github.com/reactphp/event-loop/tree/v1.5.0"
+ "source": "https://github.com/reactphp/event-loop/tree/v1.6.0"
},
"funding": [
{
@@ -484,7 +484,7 @@
"type": "open_collective"
}
],
- "time": "2023-11-13T13:48:05+00:00"
+ "time": "2025-11-17T20:46:25+00:00"
},
{
"name": "react/promise",
@@ -628,16 +628,16 @@
},
{
"name": "symfony/event-dispatcher",
- "version": "v6.4.25",
+ "version": "v6.4.32",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
- "reference": "b0cf3162020603587363f0551cd3be43958611ff"
+ "reference": "99d7e101826e6610606b9433248f80c1997cd20b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b0cf3162020603587363f0551cd3be43958611ff",
- "reference": "b0cf3162020603587363f0551cd3be43958611ff",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99d7e101826e6610606b9433248f80c1997cd20b",
+ "reference": "99d7e101826e6610606b9433248f80c1997cd20b",
"shasum": ""
},
"require": {
@@ -688,7 +688,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.25"
+ "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.32"
},
"funding": [
{
@@ -708,7 +708,7 @@
"type": "tidelift"
}
],
- "time": "2025-08-13T09:41:44+00:00"
+ "time": "2026-01-05T11:13:48+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
@@ -788,16 +788,16 @@
},
{
"name": "symfony/http-client",
- "version": "v6.4.26",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
- "reference": "6740cdc1a3bffa127966b6056e883b3fe3709849"
+ "reference": "0dc71f52e5d35bb045fd0f82b1a80c027971d551"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-client/zipball/6740cdc1a3bffa127966b6056e883b3fe3709849",
- "reference": "6740cdc1a3bffa127966b6056e883b3fe3709849",
+ "url": "https://api.github.com/repos/symfony/http-client/zipball/0dc71f52e5d35bb045fd0f82b1a80c027971d551",
+ "reference": "0dc71f52e5d35bb045fd0f82b1a80c027971d551",
"shasum": ""
},
"require": {
@@ -862,7 +862,7 @@
"http"
],
"support": {
- "source": "https://github.com/symfony/http-client/tree/v6.4.26"
+ "source": "https://github.com/symfony/http-client/tree/v6.4.34"
},
"funding": [
{
@@ -882,7 +882,7 @@
"type": "tidelift"
}
],
- "time": "2025-09-11T09:57:09+00:00"
+ "time": "2026-02-18T07:27:25+00:00"
},
{
"name": "symfony/http-client-contracts",
@@ -964,16 +964,16 @@
},
{
"name": "symfony/mailer",
- "version": "v6.4.26",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/mailer.git",
- "reference": "012185cd31689b799d39505bd706be6d3a57cd3f"
+ "reference": "01b846f48e53ee4096692a383637a1fa4d577301"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mailer/zipball/012185cd31689b799d39505bd706be6d3a57cd3f",
- "reference": "012185cd31689b799d39505bd706be6d3a57cd3f",
+ "url": "https://api.github.com/repos/symfony/mailer/zipball/01b846f48e53ee4096692a383637a1fa4d577301",
+ "reference": "01b846f48e53ee4096692a383637a1fa4d577301",
"shasum": ""
},
"require": {
@@ -1024,7 +1024,7 @@
"description": "Helps sending emails",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/mailer/tree/v6.4.26"
+ "source": "https://github.com/symfony/mailer/tree/v6.4.34"
},
"funding": [
{
@@ -1044,20 +1044,20 @@
"type": "tidelift"
}
],
- "time": "2025-09-11T09:57:09+00:00"
+ "time": "2026-02-24T09:34:36+00:00"
},
{
"name": "symfony/mailjet-mailer",
- "version": "v6.4.24",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/mailjet-mailer.git",
- "reference": "67836efb1775aa781dcca2876a4ad0a1d942806c"
+ "reference": "1e9916fccea1c1c5dc57e938b0fcc9d292d44193"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mailjet-mailer/zipball/67836efb1775aa781dcca2876a4ad0a1d942806c",
- "reference": "67836efb1775aa781dcca2876a4ad0a1d942806c",
+ "url": "https://api.github.com/repos/symfony/mailjet-mailer/zipball/1e9916fccea1c1c5dc57e938b0fcc9d292d44193",
+ "reference": "1e9916fccea1c1c5dc57e938b0fcc9d292d44193",
"shasum": ""
},
"require": {
@@ -1094,7 +1094,7 @@
"description": "Symfony Mailjet Mailer Bridge",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/mailjet-mailer/tree/v6.4.24"
+ "source": "https://github.com/symfony/mailjet-mailer/tree/v6.4.34"
},
"funding": [
{
@@ -1114,20 +1114,20 @@
"type": "tidelift"
}
],
- "time": "2025-07-10T08:14:14+00:00"
+ "time": "2026-02-05T07:56:34+00:00"
},
{
"name": "symfony/mime",
- "version": "v6.4.26",
+ "version": "v6.4.34",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
- "reference": "61ab9681cdfe315071eb4fa79b6ad6ab030a9235"
+ "reference": "2b32fbbe10b36a8379efab6e702ad8b917151839"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mime/zipball/61ab9681cdfe315071eb4fa79b6ad6ab030a9235",
- "reference": "61ab9681cdfe315071eb4fa79b6ad6ab030a9235",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/2b32fbbe10b36a8379efab6e702ad8b917151839",
+ "reference": "2b32fbbe10b36a8379efab6e702ad8b917151839",
"shasum": ""
},
"require": {
@@ -1183,7 +1183,7 @@
"mime-type"
],
"support": {
- "source": "https://github.com/symfony/mime/tree/v6.4.26"
+ "source": "https://github.com/symfony/mime/tree/v6.4.34"
},
"funding": [
{
@@ -1203,7 +1203,7 @@
"type": "tidelift"
}
],
- "time": "2025-09-16T08:22:30+00:00"
+ "time": "2026-02-02T17:01:23+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -1627,16 +1627,16 @@
},
{
"name": "symfony/service-contracts",
- "version": "v3.6.0",
+ "version": "v3.6.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
- "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4"
+ "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
- "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43",
+ "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43",
"shasum": ""
},
"require": {
@@ -1690,7 +1690,7 @@
"standards"
],
"support": {
- "source": "https://github.com/symfony/service-contracts/tree/v3.6.0"
+ "source": "https://github.com/symfony/service-contracts/tree/v3.6.1"
},
"funding": [
{
@@ -1701,25 +1701,29 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-04-25T09:37:31+00:00"
+ "time": "2025-07-15T11:30:57+00:00"
},
{
"name": "twig/twig",
- "version": "v3.21.1",
+ "version": "v3.23.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
- "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d"
+ "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/twigphp/Twig/zipball/285123877d4dd97dd7c11842ac5fb7e86e60d81d",
- "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d",
+ "url": "https://api.github.com/repos/twigphp/Twig/zipball/a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9",
+ "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9",
"shasum": ""
},
"require": {
@@ -1773,7 +1777,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
- "source": "https://github.com/twigphp/Twig/tree/v3.21.1"
+ "source": "https://github.com/twigphp/Twig/tree/v3.23.0"
},
"funding": [
{
@@ -1785,61 +1789,44 @@
"type": "tidelift"
}
],
- "time": "2025-05-03T07:21:55+00:00"
+ "time": "2026-01-23T21:00:41+00:00"
}
],
"packages-dev": [
{
- "name": "guzzlehttp/guzzle",
- "version": "7.10.0",
+ "name": "amphp/amp",
+ "version": "v2.6.5",
"source": {
"type": "git",
- "url": "https://github.com/guzzle/guzzle.git",
- "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4"
+ "url": "https://github.com/amphp/amp.git",
+ "reference": "d7dda98dae26e56f3f6fcfbf1c1f819c9a993207"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
- "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
+ "url": "https://api.github.com/repos/amphp/amp/zipball/d7dda98dae26e56f3f6fcfbf1c1f819c9a993207",
+ "reference": "d7dda98dae26e56f3f6fcfbf1c1f819c9a993207",
"shasum": ""
},
"require": {
- "ext-json": "*",
- "guzzlehttp/promises": "^2.3",
- "guzzlehttp/psr7": "^2.8",
- "php": "^7.2.5 || ^8.0",
- "psr/http-client": "^1.0",
- "symfony/deprecation-contracts": "^2.2 || ^3.0"
- },
- "provide": {
- "psr/http-client-implementation": "1.0"
+ "php": ">=7.1"
},
"require-dev": {
- "bamarni/composer-bin-plugin": "^1.8.2",
- "ext-curl": "*",
- "guzzle/client-integration-tests": "3.0.2",
- "php-http/message-factory": "^1.1",
- "phpunit/phpunit": "^8.5.39 || ^9.6.20",
- "psr/log": "^1.1 || ^2.0 || ^3.0"
- },
- "suggest": {
- "ext-curl": "Required for CURL handler support",
- "ext-intl": "Required for Internationalized Domain Name (IDN) support",
- "psr/log": "Required for using the Log middleware"
+ "amphp/php-cs-fixer-config": "dev-master",
+ "amphp/phpunit-util": "^1",
+ "ext-json": "*",
+ "jetbrains/phpstorm-stubs": "^2019.3",
+ "phpunit/phpunit": "^7 | ^8 | ^9",
+ "react/promise": "^2",
+ "vimeo/psalm": "^3.12"
},
"type": "library",
- "extra": {
- "bamarni-bin": {
- "bin-links": true,
- "forward-command": false
- }
- },
"autoload": {
"files": [
- "src/functions_include.php"
+ "lib/functions.php",
+ "lib/Internal/functions.php"
],
"psr-4": {
- "GuzzleHttp\\": "src/"
+ "Amp\\": "lib"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -1848,104 +1835,81 @@
],
"authors": [
{
- "name": "Graham Campbell",
- "email": "hello@gjcampbell.co.uk",
- "homepage": "https://github.com/GrahamCampbell"
- },
- {
- "name": "Michael Dowling",
- "email": "mtdowling@gmail.com",
- "homepage": "https://github.com/mtdowling"
- },
- {
- "name": "Jeremy Lindblom",
- "email": "jeremeamia@gmail.com",
- "homepage": "https://github.com/jeremeamia"
- },
- {
- "name": "George Mponos",
- "email": "gmponos@gmail.com",
- "homepage": "https://github.com/gmponos"
+ "name": "Daniel Lowrey",
+ "email": "rdlowrey@php.net"
},
{
- "name": "Tobias Nyholm",
- "email": "tobias.nyholm@gmail.com",
- "homepage": "https://github.com/Nyholm"
+ "name": "Aaron Piotrowski",
+ "email": "aaron@trowski.com"
},
{
- "name": "Márk Sági-Kazár",
- "email": "mark.sagikazar@gmail.com",
- "homepage": "https://github.com/sagikazarmark"
+ "name": "Bob Weinand",
+ "email": "bobwei9@hotmail.com"
},
{
- "name": "Tobias Schultze",
- "email": "webmaster@tubo-world.de",
- "homepage": "https://github.com/Tobion"
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
}
],
- "description": "Guzzle is a PHP HTTP client library",
+ "description": "A non-blocking concurrency framework for PHP applications.",
+ "homepage": "https://amphp.org/amp",
"keywords": [
- "client",
- "curl",
- "framework",
- "http",
- "http client",
- "psr-18",
- "psr-7",
- "rest",
- "web service"
+ "async",
+ "asynchronous",
+ "awaitable",
+ "concurrency",
+ "event",
+ "event-loop",
+ "future",
+ "non-blocking",
+ "promise"
],
"support": {
- "issues": "https://github.com/guzzle/guzzle/issues",
- "source": "https://github.com/guzzle/guzzle/tree/7.10.0"
+ "irc": "irc://irc.freenode.org/amphp",
+ "issues": "https://github.com/amphp/amp/issues",
+ "source": "https://github.com/amphp/amp/tree/v2.6.5"
},
"funding": [
{
- "url": "https://github.com/GrahamCampbell",
- "type": "github"
- },
- {
- "url": "https://github.com/Nyholm",
+ "url": "https://github.com/amphp",
"type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
- "type": "tidelift"
}
],
- "time": "2025-08-23T22:36:01+00:00"
+ "time": "2025-09-03T19:41:28+00:00"
},
{
- "name": "guzzlehttp/promises",
- "version": "2.3.0",
+ "name": "amphp/byte-stream",
+ "version": "v1.8.2",
"source": {
"type": "git",
- "url": "https://github.com/guzzle/promises.git",
- "reference": "481557b130ef3790cf82b713667b43030dc9c957"
+ "url": "https://github.com/amphp/byte-stream.git",
+ "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957",
- "reference": "481557b130ef3790cf82b713667b43030dc9c957",
+ "url": "https://api.github.com/repos/amphp/byte-stream/zipball/4f0e968ba3798a423730f567b1b50d3441c16ddc",
+ "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc",
"shasum": ""
},
"require": {
- "php": "^7.2.5 || ^8.0"
+ "amphp/amp": "^2",
+ "php": ">=7.1"
},
"require-dev": {
- "bamarni/composer-bin-plugin": "^1.8.2",
- "phpunit/phpunit": "^8.5.44 || ^9.6.25"
+ "amphp/php-cs-fixer-config": "dev-master",
+ "amphp/phpunit-util": "^1.4",
+ "friendsofphp/php-cs-fixer": "^2.3",
+ "jetbrains/phpstorm-stubs": "^2019.3",
+ "phpunit/phpunit": "^6 || ^7 || ^8",
+ "psalm/phar": "^3.11.4"
},
"type": "library",
- "extra": {
- "bamarni-bin": {
- "bin-links": true,
- "forward-command": false
- }
- },
"autoload": {
+ "files": [
+ "lib/functions.php"
+ ],
"psr-4": {
- "GuzzleHttp\\Promise\\": "src/"
+ "Amp\\ByteStream\\": "lib"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -1954,92 +1918,75 @@
],
"authors": [
{
- "name": "Graham Campbell",
- "email": "hello@gjcampbell.co.uk",
- "homepage": "https://github.com/GrahamCampbell"
- },
- {
- "name": "Michael Dowling",
- "email": "mtdowling@gmail.com",
- "homepage": "https://github.com/mtdowling"
- },
- {
- "name": "Tobias Nyholm",
- "email": "tobias.nyholm@gmail.com",
- "homepage": "https://github.com/Nyholm"
+ "name": "Aaron Piotrowski",
+ "email": "aaron@trowski.com"
},
{
- "name": "Tobias Schultze",
- "email": "webmaster@tubo-world.de",
- "homepage": "https://github.com/Tobion"
+ "name": "Niklas Keller",
+ "email": "me@kelunik.com"
}
],
- "description": "Guzzle promises library",
+ "description": "A stream abstraction to make working with non-blocking I/O simple.",
+ "homepage": "https://amphp.org/byte-stream",
"keywords": [
- "promise"
+ "amp",
+ "amphp",
+ "async",
+ "io",
+ "non-blocking",
+ "stream"
],
"support": {
- "issues": "https://github.com/guzzle/promises/issues",
- "source": "https://github.com/guzzle/promises/tree/2.3.0"
+ "issues": "https://github.com/amphp/byte-stream/issues",
+ "source": "https://github.com/amphp/byte-stream/tree/v1.8.2"
},
"funding": [
{
- "url": "https://github.com/GrahamCampbell",
- "type": "github"
- },
- {
- "url": "https://github.com/Nyholm",
+ "url": "https://github.com/amphp",
"type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
- "type": "tidelift"
}
],
- "time": "2025-08-22T14:34:08+00:00"
+ "time": "2024-04-13T18:00:56+00:00"
},
{
- "name": "guzzlehttp/psr7",
- "version": "2.8.0",
+ "name": "composer/pcre",
+ "version": "3.3.2",
"source": {
"type": "git",
- "url": "https://github.com/guzzle/psr7.git",
- "reference": "21dc724a0583619cd1652f673303492272778051"
+ "url": "https://github.com/composer/pcre.git",
+ "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051",
- "reference": "21dc724a0583619cd1652f673303492272778051",
+ "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+ "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"shasum": ""
},
"require": {
- "php": "^7.2.5 || ^8.0",
- "psr/http-factory": "^1.0",
- "psr/http-message": "^1.1 || ^2.0",
- "ralouphie/getallheaders": "^3.0"
+ "php": "^7.4 || ^8.0"
},
- "provide": {
- "psr/http-factory-implementation": "1.0",
- "psr/http-message-implementation": "1.0"
+ "conflict": {
+ "phpstan/phpstan": "<1.11.10"
},
"require-dev": {
- "bamarni/composer-bin-plugin": "^1.8.2",
- "http-interop/http-factory-tests": "0.9.0",
- "phpunit/phpunit": "^8.5.44 || ^9.6.25"
- },
- "suggest": {
- "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
+ "phpstan/phpstan": "^1.12 || ^2",
+ "phpstan/phpstan-strict-rules": "^1 || ^2",
+ "phpunit/phpunit": "^8 || ^9"
},
"type": "library",
"extra": {
- "bamarni-bin": {
- "bin-links": true,
- "forward-command": false
+ "phpstan": {
+ "includes": [
+ "extension.neon"
+ ]
+ },
+ "branch-alias": {
+ "dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
- "GuzzleHttp\\Psr7\\": "src/"
+ "Composer\\Pcre\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -2048,871 +1995,996 @@
],
"authors": [
{
- "name": "Graham Campbell",
- "email": "hello@gjcampbell.co.uk",
- "homepage": "https://github.com/GrahamCampbell"
- },
- {
- "name": "Michael Dowling",
- "email": "mtdowling@gmail.com",
- "homepage": "https://github.com/mtdowling"
- },
- {
- "name": "George Mponos",
- "email": "gmponos@gmail.com",
- "homepage": "https://github.com/gmponos"
- },
- {
- "name": "Tobias Nyholm",
- "email": "tobias.nyholm@gmail.com",
- "homepage": "https://github.com/Nyholm"
- },
- {
- "name": "Márk Sági-Kazár",
- "email": "mark.sagikazar@gmail.com",
- "homepage": "https://github.com/sagikazarmark"
- },
- {
- "name": "Tobias Schultze",
- "email": "webmaster@tubo-world.de",
- "homepage": "https://github.com/Tobion"
- },
- {
- "name": "Márk Sági-Kazár",
- "email": "mark.sagikazar@gmail.com",
- "homepage": "https://sagikazarmark.hu"
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
}
],
- "description": "PSR-7 message implementation that also provides common utility methods",
+ "description": "PCRE wrapping library that offers type-safe preg_* replacements.",
"keywords": [
- "http",
- "message",
- "psr-7",
- "request",
- "response",
- "stream",
- "uri",
- "url"
+ "PCRE",
+ "preg",
+ "regex",
+ "regular expression"
],
"support": {
- "issues": "https://github.com/guzzle/psr7/issues",
- "source": "https://github.com/guzzle/psr7/tree/2.8.0"
+ "issues": "https://github.com/composer/pcre/issues",
+ "source": "https://github.com/composer/pcre/tree/3.3.2"
},
"funding": [
{
- "url": "https://github.com/GrahamCampbell",
- "type": "github"
+ "url": "https://packagist.com",
+ "type": "custom"
},
{
- "url": "https://github.com/Nyholm",
+ "url": "https://github.com/composer",
"type": "github"
},
{
- "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
- "time": "2025-08-23T21:21:41+00:00"
+ "time": "2024-11-12T16:29:46+00:00"
},
{
- "name": "myclabs/deep-copy",
- "version": "1.13.4",
+ "name": "composer/semver",
+ "version": "3.4.4",
"source": {
"type": "git",
- "url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
+ "url": "https://github.com/composer/semver.git",
+ "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
- "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95",
+ "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95",
"shasum": ""
},
"require": {
- "php": "^7.1 || ^8.0"
- },
- "conflict": {
- "doctrine/collections": "<1.6.8",
- "doctrine/common": "<2.13.3 || >=3 <3.2.2"
+ "php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
- "doctrine/collections": "^1.6.8",
- "doctrine/common": "^2.13.3 || ^3.2.2",
- "phpspec/prophecy": "^1.10",
- "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+ "phpstan/phpstan": "^1.11",
+ "symfony/phpunit-bridge": "^3 || ^7"
},
"type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
"autoload": {
- "files": [
- "src/DeepCopy/deep_copy.php"
- ],
"psr-4": {
- "DeepCopy\\": "src/DeepCopy/"
+ "Composer\\Semver\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
- "description": "Create deep copies (clones) of your objects",
+ "authors": [
+ {
+ "name": "Nils Adermann",
+ "email": "naderman@naderman.de",
+ "homepage": "http://www.naderman.de"
+ },
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ },
+ {
+ "name": "Rob Bast",
+ "email": "rob.bast@gmail.com",
+ "homepage": "http://robbast.nl"
+ }
+ ],
+ "description": "Semver library that offers utilities, version constraint parsing and validation.",
"keywords": [
- "clone",
- "copy",
- "duplicate",
- "object",
- "object graph"
+ "semantic",
+ "semver",
+ "validation",
+ "versioning"
],
"support": {
- "issues": "https://github.com/myclabs/DeepCopy/issues",
- "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
+ "irc": "ircs://irc.libera.chat:6697/composer",
+ "issues": "https://github.com/composer/semver/issues",
+ "source": "https://github.com/composer/semver/tree/3.4.4"
},
"funding": [
{
- "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
- "type": "tidelift"
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
}
],
- "time": "2025-08-01T08:46:24+00:00"
+ "time": "2025-08-20T19:15:30+00:00"
},
{
- "name": "nikic/php-parser",
- "version": "v5.6.1",
+ "name": "composer/xdebug-handler",
+ "version": "3.0.5",
"source": {
"type": "git",
- "url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2"
+ "url": "https://github.com/composer/xdebug-handler.git",
+ "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2",
- "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2",
+ "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef",
+ "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef",
"shasum": ""
},
"require": {
- "ext-ctype": "*",
- "ext-json": "*",
- "ext-tokenizer": "*",
- "php": ">=7.4"
+ "composer/pcre": "^1 || ^2 || ^3",
+ "php": "^7.2.5 || ^8.0",
+ "psr/log": "^1 || ^2 || ^3"
},
"require-dev": {
- "ircmaxell/php-yacc": "^0.0.7",
- "phpunit/phpunit": "^9.0"
+ "phpstan/phpstan": "^1.0",
+ "phpstan/phpstan-strict-rules": "^1.1",
+ "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5"
},
- "bin": [
- "bin/php-parse"
- ],
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "5.x-dev"
- }
- },
"autoload": {
"psr-4": {
- "PhpParser\\": "lib/PhpParser"
+ "Composer\\XdebugHandler\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Nikita Popov"
+ "name": "John Stevenson",
+ "email": "john-stevenson@blueyonder.co.uk"
}
],
- "description": "A PHP parser written in PHP",
+ "description": "Restarts a process without Xdebug.",
"keywords": [
- "parser",
- "php"
+ "Xdebug",
+ "performance"
],
"support": {
- "issues": "https://github.com/nikic/PHP-Parser/issues",
- "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1"
+ "irc": "ircs://irc.libera.chat:6697/composer",
+ "issues": "https://github.com/composer/xdebug-handler/issues",
+ "source": "https://github.com/composer/xdebug-handler/tree/3.0.5"
},
- "time": "2025-08-13T20:13:15+00:00"
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-05-06T16:37:16+00:00"
},
{
- "name": "phar-io/manifest",
- "version": "2.0.4",
+ "name": "consolidation/annotated-command",
+ "version": "4.10.4",
"source": {
"type": "git",
- "url": "https://github.com/phar-io/manifest.git",
- "reference": "54750ef60c58e43759730615a392c31c80e23176"
+ "url": "https://github.com/consolidation/annotated-command.git",
+ "reference": "69d29da4acac31a43caa4cea13b6b948f4e5c56d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176",
- "reference": "54750ef60c58e43759730615a392c31c80e23176",
+ "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/69d29da4acac31a43caa4cea13b6b948f4e5c56d",
+ "reference": "69d29da4acac31a43caa4cea13b6b948f4e5c56d",
"shasum": ""
},
"require": {
- "ext-dom": "*",
- "ext-libxml": "*",
- "ext-phar": "*",
- "ext-xmlwriter": "*",
- "phar-io/version": "^3.0.1",
- "php": "^7.2 || ^8.0"
+ "consolidation/output-formatters": "^4.3.1",
+ "php": ">=7.1.3",
+ "psr/log": "^1 || ^2 || ^3",
+ "symfony/console": "^4.4.8 || ^5 || ^6 || ^7",
+ "symfony/event-dispatcher": "^4.4.8 || ^5 || ^6 || ^7",
+ "symfony/finder": "^4.4.8 || ^5 || ^6 || ^7"
+ },
+ "require-dev": {
+ "composer-runtime-api": "^2.0",
+ "phpunit/phpunit": "^7.5.20 || ^8 || ^9",
+ "squizlabs/php_codesniffer": "^3",
+ "yoast/phpunit-polyfills": "^0.2.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0.x-dev"
+ "dev-main": "4.x-dev"
}
},
"autoload": {
- "classmap": [
- "src/"
- ]
+ "psr-4": {
+ "Consolidation\\AnnotatedCommand\\": "src"
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Arne Blankerts",
- "email": "arne@blankerts.de",
- "role": "Developer"
- },
- {
- "name": "Sebastian Heuer",
- "email": "sebastian@phpeople.de",
- "role": "Developer"
- },
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de",
- "role": "Developer"
+ "name": "Greg Anderson",
+ "email": "greg.1.anderson@greenknowe.org"
}
],
- "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "description": "Initialize Symfony Console commands from annotated command class methods.",
"support": {
- "issues": "https://github.com/phar-io/manifest/issues",
- "source": "https://github.com/phar-io/manifest/tree/2.0.4"
+ "issues": "https://github.com/consolidation/annotated-command/issues",
+ "source": "https://github.com/consolidation/annotated-command/tree/4.10.4"
},
- "funding": [
- {
- "url": "https://github.com/theseer",
- "type": "github"
- }
- ],
- "time": "2024-03-03T12:33:53+00:00"
+ "time": "2025-11-14T22:57:49+00:00"
},
{
- "name": "phar-io/version",
- "version": "3.2.1",
+ "name": "consolidation/config",
+ "version": "2.1.2",
"source": {
"type": "git",
- "url": "https://github.com/phar-io/version.git",
- "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74"
+ "url": "https://github.com/consolidation/config.git",
+ "reference": "597f8d7fbeef801736250ec10c3e190569b1b0ae"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
- "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "url": "https://api.github.com/repos/consolidation/config/zipball/597f8d7fbeef801736250ec10c3e190569b1b0ae",
+ "reference": "597f8d7fbeef801736250ec10c3e190569b1b0ae",
"shasum": ""
},
"require": {
- "php": "^7.2 || ^8.0"
+ "dflydev/dot-access-data": "^1.1.0 || ^2 || ^3",
+ "grasmash/expander": "^2.0.1 || ^3",
+ "php": ">=7.1.3",
+ "symfony/event-dispatcher": "^4 || ^5 || ^6"
+ },
+ "require-dev": {
+ "ext-json": "*",
+ "phpunit/phpunit": ">=7.5.20",
+ "squizlabs/php_codesniffer": "^3",
+ "symfony/console": "^4 || ^5 || ^6",
+ "symfony/yaml": "^4 || ^5 || ^6",
+ "yoast/phpunit-polyfills": "^1"
+ },
+ "suggest": {
+ "symfony/event-dispatcher": "Required to inject configuration into Command options",
+ "symfony/yaml": "Required to use Consolidation\\Config\\Loader\\YamlConfigLoader"
},
"type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.x-dev"
+ }
+ },
"autoload": {
- "classmap": [
- "src/"
- ]
+ "psr-4": {
+ "Consolidation\\Config\\": "src"
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Arne Blankerts",
- "email": "arne@blankerts.de",
- "role": "Developer"
- },
- {
- "name": "Sebastian Heuer",
- "email": "sebastian@phpeople.de",
- "role": "Developer"
- },
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de",
- "role": "Developer"
+ "name": "Greg Anderson",
+ "email": "greg.1.anderson@greenknowe.org"
}
],
- "description": "Library for handling version information and constraints",
+ "description": "Provide configuration services for a commandline tool.",
"support": {
- "issues": "https://github.com/phar-io/version/issues",
- "source": "https://github.com/phar-io/version/tree/3.2.1"
+ "issues": "https://github.com/consolidation/config/issues",
+ "source": "https://github.com/consolidation/config/tree/2.1.2"
},
- "time": "2022-02-21T01:04:05+00:00"
+ "time": "2022-10-06T17:48:03+00:00"
},
{
- "name": "phpunit/php-code-coverage",
- "version": "10.1.16",
+ "name": "consolidation/log",
+ "version": "3.1.1",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "7e308268858ed6baedc8704a304727d20bc07c77"
+ "url": "https://github.com/consolidation/log.git",
+ "reference": "c1a87a94c01957697ec347fd67404d7f0030d1aa"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77",
- "reference": "7e308268858ed6baedc8704a304727d20bc07c77",
+ "url": "https://api.github.com/repos/consolidation/log/zipball/c1a87a94c01957697ec347fd67404d7f0030d1aa",
+ "reference": "c1a87a94c01957697ec347fd67404d7f0030d1aa",
"shasum": ""
},
"require": {
- "ext-dom": "*",
- "ext-libxml": "*",
- "ext-xmlwriter": "*",
- "nikic/php-parser": "^4.19.1 || ^5.1.0",
- "php": ">=8.1",
- "phpunit/php-file-iterator": "^4.1.0",
- "phpunit/php-text-template": "^3.0.1",
- "sebastian/code-unit-reverse-lookup": "^3.0.0",
- "sebastian/complexity": "^3.2.0",
- "sebastian/environment": "^6.1.0",
- "sebastian/lines-of-code": "^2.0.2",
- "sebastian/version": "^4.0.1",
- "theseer/tokenizer": "^1.2.3"
+ "php": ">=8.0.0",
+ "psr/log": "^3",
+ "symfony/console": "^5 || ^6 || ^7"
},
"require-dev": {
- "phpunit/phpunit": "^10.1"
- },
- "suggest": {
- "ext-pcov": "PHP extension that provides line coverage",
- "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ "phpunit/phpunit": "^7.5.20 || ^8 || ^9",
+ "squizlabs/php_codesniffer": "^3",
+ "yoast/phpunit-polyfills": "^0.2.0"
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-main": "10.1.x-dev"
+ "platform": {
+ "php": "8.2.17"
}
},
"autoload": {
- "classmap": [
- "src/"
- ]
+ "psr-4": {
+ "Consolidation\\Log\\": "src"
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de",
- "role": "lead"
+ "name": "Greg Anderson",
+ "email": "greg.1.anderson@greenknowe.org"
}
],
- "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
- "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
- "keywords": [
- "coverage",
- "testing",
- "xunit"
- ],
+ "description": "Improved Psr-3 / Psr\\Log logger based on Symfony Console components.",
"support": {
- "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
- "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
- "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16"
+ "issues": "https://github.com/consolidation/log/issues",
+ "source": "https://github.com/consolidation/log/tree/3.1.1"
},
- "funding": [
- {
- "url": "https://github.com/sebastianbergmann",
- "type": "github"
- }
- ],
- "time": "2024-08-22T04:31:57+00:00"
+ "time": "2025-11-14T21:11:00+00:00"
},
{
- "name": "phpunit/php-file-iterator",
- "version": "4.1.0",
+ "name": "consolidation/output-formatters",
+ "version": "4.7.0",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
- "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c"
+ "url": "https://github.com/consolidation/output-formatters.git",
+ "reference": "dfc464c4d4a47594cac5eac01ce265e04b70cb94"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c",
- "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c",
+ "url": "https://api.github.com/repos/consolidation/output-formatters/zipball/dfc464c4d4a47594cac5eac01ce265e04b70cb94",
+ "reference": "dfc464c4d4a47594cac5eac01ce265e04b70cb94",
"shasum": ""
},
"require": {
- "php": ">=8.1"
+ "dflydev/dot-access-data": "^1.1.0 || ^2 || ^3",
+ "php": ">=7.1.3",
+ "symfony/console": "^4 || ^5 || ^6 || ^7",
+ "symfony/finder": "^4 || ^5 || ^6 || ^7"
},
"require-dev": {
- "phpunit/phpunit": "^10.0"
+ "php-coveralls/php-coveralls": "^2.4.2",
+ "phpunit/phpunit": "^7 || ^8 || ^9",
+ "squizlabs/php_codesniffer": "^3",
+ "symfony/var-dumper": "^4 || ^5 || ^6 || ^7",
+ "symfony/yaml": "^4 || ^5 || ^6 || ^7",
+ "yoast/phpunit-polyfills": "^1"
},
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-main": "4.0-dev"
- }
+ "suggest": {
+ "symfony/var-dumper": "For using the var_dump formatter"
},
+ "type": "library",
"autoload": {
- "classmap": [
- "src/"
- ]
+ "psr-4": {
+ "Consolidation\\OutputFormatters\\": "src"
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de",
- "role": "lead"
+ "name": "Greg Anderson",
+ "email": "greg.1.anderson@greenknowe.org"
}
],
- "description": "FilterIterator implementation that filters files based on a list of suffixes.",
- "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
- "keywords": [
- "filesystem",
- "iterator"
- ],
+ "description": "Format text by applying transformations provided by plug-in formatters.",
"support": {
- "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
- "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy",
- "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0"
+ "issues": "https://github.com/consolidation/output-formatters/issues",
+ "source": "https://github.com/consolidation/output-formatters/tree/4.7.0"
},
- "funding": [
- {
- "url": "https://github.com/sebastianbergmann",
- "type": "github"
- }
- ],
- "time": "2023-08-31T06:24:48+00:00"
+ "time": "2025-11-14T21:06:10+00:00"
},
{
- "name": "phpunit/php-invoker",
- "version": "4.0.0",
+ "name": "consolidation/robo",
+ "version": "4.0.6",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/php-invoker.git",
- "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7"
+ "url": "https://github.com/consolidation/robo.git",
+ "reference": "55a272370940607649e5c46eb173c5c54f7c166d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7",
- "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7",
+ "url": "https://api.github.com/repos/consolidation/robo/zipball/55a272370940607649e5c46eb173c5c54f7c166d",
+ "reference": "55a272370940607649e5c46eb173c5c54f7c166d",
"shasum": ""
},
"require": {
- "php": ">=8.1"
+ "consolidation/annotated-command": "^4.8.1",
+ "consolidation/config": "^2.0.1",
+ "consolidation/log": "^2.0.2 || ^3",
+ "consolidation/output-formatters": "^4.1.2",
+ "consolidation/self-update": "^2.0",
+ "league/container": "^3.3.1 || ^4.0",
+ "php": ">=8.0",
+ "phpowermove/docblock": "^4.0",
+ "symfony/console": "^6",
+ "symfony/event-dispatcher": "^6",
+ "symfony/filesystem": "^6",
+ "symfony/finder": "^6",
+ "symfony/process": "^6",
+ "symfony/yaml": "^6"
+ },
+ "conflict": {
+ "codegyre/robo": "*"
},
"require-dev": {
- "ext-pcntl": "*",
- "phpunit/phpunit": "^10.0"
+ "natxet/cssmin": "3.0.4",
+ "patchwork/jsqueeze": "^2",
+ "pear/archive_tar": "^1.4.4",
+ "phpunit/phpunit": "^7.5.20 || ^8",
+ "squizlabs/php_codesniffer": "^3.6",
+ "yoast/phpunit-polyfills": "^0.2.0"
},
"suggest": {
- "ext-pcntl": "*"
+ "natxet/cssmin": "For minifying CSS files in taskMinify",
+ "patchwork/jsqueeze": "For minifying JS files in taskMinify",
+ "pear/archive_tar": "Allows tar archives to be created and extracted in taskPack and taskExtract, respectively.",
+ "totten/lurkerlite": "For monitoring filesystem changes in taskWatch"
},
+ "bin": [
+ "robo"
+ ],
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-main": "4.0-dev"
- }
- },
"autoload": {
- "classmap": [
- "src/"
- ]
+ "psr-4": {
+ "Robo\\": "src"
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de",
- "role": "lead"
+ "name": "Davert",
+ "email": "davert.php@resend.cc"
}
],
- "description": "Invoke callables with a timeout",
- "homepage": "https://github.com/sebastianbergmann/php-invoker/",
- "keywords": [
- "process"
- ],
+ "description": "Modern task runner",
"support": {
- "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
- "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0"
+ "issues": "https://github.com/consolidation/robo/issues",
+ "source": "https://github.com/consolidation/robo/tree/4.0.6"
},
- "funding": [
- {
- "url": "https://github.com/sebastianbergmann",
- "type": "github"
- }
- ],
- "time": "2023-02-03T06:56:09+00:00"
+ "time": "2023-04-30T21:49:04+00:00"
},
{
- "name": "phpunit/php-text-template",
- "version": "3.0.1",
+ "name": "consolidation/self-update",
+ "version": "2.2.0",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/php-text-template.git",
- "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748"
+ "url": "https://github.com/consolidation/self-update.git",
+ "reference": "972a1016761c9b63314e040836a12795dff6953a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748",
- "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748",
+ "url": "https://api.github.com/repos/consolidation/self-update/zipball/972a1016761c9b63314e040836a12795dff6953a",
+ "reference": "972a1016761c9b63314e040836a12795dff6953a",
"shasum": ""
},
"require": {
- "php": ">=8.1"
- },
- "require-dev": {
- "phpunit/phpunit": "^10.0"
+ "composer/semver": "^3.2",
+ "php": ">=5.5.0",
+ "symfony/console": "^2.8 || ^3 || ^4 || ^5 || ^6",
+ "symfony/filesystem": "^2.5 || ^3 || ^4 || ^5 || ^6"
},
+ "bin": [
+ "scripts/release"
+ ],
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "3.0-dev"
+ "dev-main": "2.x-dev"
}
},
"autoload": {
- "classmap": [
- "src/"
- ]
+ "psr-4": {
+ "SelfUpdate\\": "src"
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de",
- "role": "lead"
+ "name": "Alexander Menk",
+ "email": "menk@mestrona.net"
+ },
+ {
+ "name": "Greg Anderson",
+ "email": "greg.1.anderson@greenknowe.org"
}
],
- "description": "Simple template engine.",
- "homepage": "https://github.com/sebastianbergmann/php-text-template/",
- "keywords": [
- "template"
- ],
+ "description": "Provides a self:update command for Symfony Console applications.",
"support": {
- "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
- "security": "https://github.com/sebastianbergmann/php-text-template/security/policy",
- "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1"
+ "issues": "https://github.com/consolidation/self-update/issues",
+ "source": "https://github.com/consolidation/self-update/tree/2.2.0"
},
- "funding": [
- {
- "url": "https://github.com/sebastianbergmann",
- "type": "github"
- }
- ],
- "time": "2023-08-31T14:07:24+00:00"
+ "time": "2023-03-18T01:37:41+00:00"
},
{
- "name": "phpunit/php-timer",
- "version": "6.0.0",
+ "name": "dealerdirect/phpcodesniffer-composer-installer",
+ "version": "v1.2.0",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/php-timer.git",
- "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d"
+ "url": "https://github.com/PHPCSStandards/composer-installer.git",
+ "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d",
- "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d",
+ "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1",
+ "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1",
"shasum": ""
},
"require": {
- "php": ">=8.1"
+ "composer-plugin-api": "^2.2",
+ "php": ">=5.4",
+ "squizlabs/php_codesniffer": "^3.1.0 || ^4.0"
},
"require-dev": {
- "phpunit/phpunit": "^10.0"
+ "composer/composer": "^2.2",
+ "ext-json": "*",
+ "ext-zip": "*",
+ "php-parallel-lint/php-parallel-lint": "^1.4.0",
+ "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev",
+ "yoast/phpunit-polyfills": "^1.0"
},
- "type": "library",
+ "type": "composer-plugin",
"extra": {
- "branch-alias": {
- "dev-main": "6.0-dev"
- }
+ "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin"
},
"autoload": {
- "classmap": [
- "src/"
- ]
+ "psr-4": {
+ "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/"
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de",
- "role": "lead"
+ "name": "Franck Nijhof",
+ "email": "opensource@frenck.dev",
+ "homepage": "https://frenck.dev",
+ "role": "Open source developer"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors"
}
],
- "description": "Utility class for timing",
- "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "description": "PHP_CodeSniffer Standards Composer Installer Plugin",
"keywords": [
- "timer"
+ "PHPCodeSniffer",
+ "PHP_CodeSniffer",
+ "code quality",
+ "codesniffer",
+ "composer",
+ "installer",
+ "phpcbf",
+ "phpcs",
+ "plugin",
+ "qa",
+ "quality",
+ "standard",
+ "standards",
+ "style guide",
+ "stylecheck",
+ "tests"
],
"support": {
- "issues": "https://github.com/sebastianbergmann/php-timer/issues",
- "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0"
+ "issues": "https://github.com/PHPCSStandards/composer-installer/issues",
+ "security": "https://github.com/PHPCSStandards/composer-installer/security/policy",
+ "source": "https://github.com/PHPCSStandards/composer-installer"
},
"funding": [
{
- "url": "https://github.com/sebastianbergmann",
+ "url": "https://github.com/PHPCSStandards",
"type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/phpcsstandards",
+ "type": "thanks_dev"
}
],
- "time": "2023-02-03T06:57:52+00:00"
+ "time": "2025-11-11T04:32:07+00:00"
},
{
- "name": "phpunit/phpunit",
- "version": "10.5.58",
+ "name": "dflydev/dot-access-data",
+ "version": "v3.0.3",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca"
+ "url": "https://github.com/dflydev/dflydev-dot-access-data.git",
+ "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e24fb46da450d8e6a5788670513c1af1424f16ca",
- "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca",
+ "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f",
+ "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f",
"shasum": ""
},
"require": {
- "ext-dom": "*",
- "ext-json": "*",
- "ext-libxml": "*",
- "ext-mbstring": "*",
- "ext-xml": "*",
- "ext-xmlwriter": "*",
- "myclabs/deep-copy": "^1.13.4",
- "phar-io/manifest": "^2.0.4",
- "phar-io/version": "^3.2.1",
- "php": ">=8.1",
- "phpunit/php-code-coverage": "^10.1.16",
- "phpunit/php-file-iterator": "^4.1.0",
- "phpunit/php-invoker": "^4.0.0",
- "phpunit/php-text-template": "^3.0.1",
- "phpunit/php-timer": "^6.0.0",
- "sebastian/cli-parser": "^2.0.1",
- "sebastian/code-unit": "^2.0.0",
- "sebastian/comparator": "^5.0.4",
- "sebastian/diff": "^5.1.1",
- "sebastian/environment": "^6.1.0",
- "sebastian/exporter": "^5.1.4",
- "sebastian/global-state": "^6.0.2",
- "sebastian/object-enumerator": "^5.0.0",
- "sebastian/recursion-context": "^5.0.1",
- "sebastian/type": "^4.0.0",
- "sebastian/version": "^4.0.1"
+ "php": "^7.1 || ^8.0"
},
- "suggest": {
- "ext-soap": "To be able to generate mocks based on WSDL files"
+ "require-dev": {
+ "phpstan/phpstan": "^0.12.42",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3",
+ "scrutinizer/ocular": "1.6.0",
+ "squizlabs/php_codesniffer": "^3.5",
+ "vimeo/psalm": "^4.0.0"
},
- "bin": [
- "phpunit"
- ],
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "10.5-dev"
+ "dev-main": "3.x-dev"
}
},
"autoload": {
- "files": [
- "src/Framework/Assert/Functions.php"
- ],
- "classmap": [
- "src/"
- ]
+ "psr-4": {
+ "Dflydev\\DotAccessData\\": "src/"
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de",
- "role": "lead"
- }
- ],
- "description": "The PHP Unit Testing framework.",
- "homepage": "https://phpunit.de/",
- "keywords": [
- "phpunit",
- "testing",
- "xunit"
- ],
- "support": {
- "issues": "https://github.com/sebastianbergmann/phpunit/issues",
- "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.58"
- },
- "funding": [
- {
- "url": "https://phpunit.de/sponsors.html",
- "type": "custom"
- },
- {
- "url": "https://github.com/sebastianbergmann",
- "type": "github"
+ "name": "Dragonfly Development Inc.",
+ "email": "info@dflydev.com",
+ "homepage": "http://dflydev.com"
},
{
- "url": "https://liberapay.com/sebastianbergmann",
- "type": "liberapay"
+ "name": "Beau Simensen",
+ "email": "beau@dflydev.com",
+ "homepage": "http://beausimensen.com"
},
{
- "url": "https://thanks.dev/u/gh/sebastianbergmann",
- "type": "thanks_dev"
+ "name": "Carlos Frutos",
+ "email": "carlos@kiwing.it",
+ "homepage": "https://github.com/cfrutos"
},
{
- "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
- "type": "tidelift"
+ "name": "Colin O'Dell",
+ "email": "colinodell@gmail.com",
+ "homepage": "https://www.colinodell.com"
}
],
- "time": "2025-09-28T12:04:46+00:00"
+ "description": "Given a deep data structure, access data by dot notation.",
+ "homepage": "https://github.com/dflydev/dflydev-dot-access-data",
+ "keywords": [
+ "access",
+ "data",
+ "dot",
+ "notation"
+ ],
+ "support": {
+ "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues",
+ "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3"
+ },
+ "time": "2024-07-08T12:26:09+00:00"
},
{
- "name": "psr/http-client",
- "version": "1.0.3",
+ "name": "dnoegel/php-xdg-base-dir",
+ "version": "v0.1.1",
"source": {
"type": "git",
- "url": "https://github.com/php-fig/http-client.git",
- "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+ "url": "https://github.com/dnoegel/php-xdg-base-dir.git",
+ "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
- "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd",
+ "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd",
"shasum": ""
},
"require": {
- "php": "^7.0 || ^8.0",
- "psr/http-message": "^1.0 || ^2.0"
+ "php": ">=5.3.2"
},
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
+ "require-dev": {
+ "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35"
},
+ "type": "library",
"autoload": {
"psr-4": {
- "Psr\\Http\\Client\\": "src/"
+ "XdgBaseDir\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
- "authors": [
- {
- "name": "PHP-FIG",
- "homepage": "https://www.php-fig.org/"
- }
- ],
- "description": "Common interface for HTTP clients",
- "homepage": "https://github.com/php-fig/http-client",
- "keywords": [
- "http",
- "http-client",
- "psr",
- "psr-18"
- ],
+ "description": "implementation of xdg base directory specification for php",
"support": {
- "source": "https://github.com/php-fig/http-client"
+ "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues",
+ "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1"
},
- "time": "2023-09-23T14:17:50+00:00"
+ "time": "2019-12-04T15:06:13+00:00"
},
{
- "name": "psr/http-factory",
- "version": "1.1.0",
+ "name": "doctrine/deprecations",
+ "version": "1.1.6",
"source": {
"type": "git",
- "url": "https://github.com/php-fig/http-factory.git",
- "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
+ "url": "https://github.com/doctrine/deprecations.git",
+ "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
- "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
+ "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
"shasum": ""
},
"require": {
- "php": ">=7.1",
- "psr/http-message": "^1.0 || ^2.0"
+ "php": "^7.1 || ^8.0"
},
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
+ "conflict": {
+ "phpunit/phpunit": "<=7.5 || >=14"
},
+ "require-dev": {
+ "doctrine/coding-standard": "^9 || ^12 || ^14",
+ "phpstan/phpstan": "1.4.10 || 2.1.30",
+ "phpstan/phpstan-phpunit": "^1.0 || ^2",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0",
+ "psr/log": "^1 || ^2 || ^3"
+ },
+ "suggest": {
+ "psr/log": "Allows logging deprecations via PSR-3 logger implementation"
+ },
+ "type": "library",
"autoload": {
"psr-4": {
- "Psr\\Http\\Message\\": "src/"
+ "Doctrine\\Deprecations\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
- "authors": [
- {
- "name": "PHP-FIG",
- "homepage": "https://www.php-fig.org/"
- }
- ],
- "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
- "keywords": [
- "factory",
- "http",
- "message",
- "psr",
- "psr-17",
- "psr-7",
- "request",
- "response"
- ],
+ "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
+ "homepage": "https://www.doctrine-project.org/",
"support": {
- "source": "https://github.com/php-fig/http-factory"
+ "issues": "https://github.com/doctrine/deprecations/issues",
+ "source": "https://github.com/doctrine/deprecations/tree/1.1.6"
},
- "time": "2024-04-15T12:06:14+00:00"
+ "time": "2026-02-07T07:09:04+00:00"
},
{
- "name": "psr/http-message",
- "version": "2.0",
+ "name": "edgedesign/phpqa",
+ "version": "v1.27.2",
"source": {
"type": "git",
- "url": "https://github.com/php-fig/http-message.git",
- "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+ "url": "https://github.com/EdgedesignCZ/phpqa.git",
+ "reference": "ad97e48c8dfe406f3fab5f6a6fb0604035f7a3ea"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
- "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "url": "https://api.github.com/repos/EdgedesignCZ/phpqa/zipball/ad97e48c8dfe406f3fab5f6a6fb0604035f7a3ea",
+ "reference": "ad97e48c8dfe406f3fab5f6a6fb0604035f7a3ea",
"shasum": ""
},
"require": {
- "php": "^7.2 || ^8.0"
+ "consolidation/robo": "~0.5|>=1",
+ "ext-xsl": "*",
+ "php": ">=5.4",
+ "twig/twig": "~1.38|~2.7|>=3"
+ },
+ "require-dev": {
+ "hamcrest/hamcrest-php": ">=2.0.1",
+ "phpunit/phpunit": ">=4.8.28"
+ },
+ "suggest": {
+ "deptrac/deptrac": "Enforce rules for dependencies between software layers",
+ "enlightn/security-checker": "Check composer.lock for known security issues",
+ "friendsofphp/php-cs-fixer": "A tool to automatically fix PHP coding standards issues",
+ "pdepend/pdepend": "Analyze you the quality of your design in terms of extensibility, reusability and maintainability",
+ "php-parallel-lint/php-console-highlighter": "Colored output in parallel-lint",
+ "php-parallel-lint/php-parallel-lint": "Check PHP syntax",
+ "phploc/phploc": "Abandoned measuring the size of a PHP project",
+ "phpmd/phpmd": "user friendly metrics from pdepend",
+ "phpmetrics/phpmetrics": "Metrics about PHP project and classes in HTML report",
+ "phpstan/phpstan": "PHP Static Analysis Tool - discover bugs in your code without running it!",
+ "phpunit/phpunit": "The PHP Unit Testing framework",
+ "psalm/phar": "A static analysis tool for finding errors in PHP applications",
+ "sebastian/phpcpd": "Abandoned copy-paste detector",
+ "squizlabs/php_codesniffer": "Detect coding standard violation (phpcs) and fix them (phpcbf)"
+ },
+ "bin": [
+ "phpqa"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/report.php",
+ "src/paths.php"
+ ],
+ "psr-4": {
+ "Edge\\QA\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Zdenek Drahos",
+ "email": "drahoszdenek@gmail.com"
+ }
+ ],
+ "description": "Analyze PHP code with one command.",
+ "keywords": [
+ "code analysis",
+ "qa",
+ "static analysis"
+ ],
+ "support": {
+ "docs": "https://edgedesigncz.github.io/phpqa/",
+ "issues": "https://github.com/EdgedesignCZ/phpqa/issues",
+ "source": "https://github.com/EdgedesignCZ/phpqa"
+ },
+ "time": "2025-11-22T07:41:40+00:00"
+ },
+ {
+ "name": "felixfbecker/advanced-json-rpc",
+ "version": "v3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git",
+ "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/b5f37dbff9a8ad360ca341f3240dc1c168b45447",
+ "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447",
+ "shasum": ""
+ },
+ "require": {
+ "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0",
+ "php": "^7.1 || ^8.0",
+ "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7.0 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "AdvancedJsonRpc\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "ISC"
+ ],
+ "authors": [
+ {
+ "name": "Felix Becker",
+ "email": "felix.b@outlook.com"
+ }
+ ],
+ "description": "A more advanced JSONRPC implementation",
+ "support": {
+ "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues",
+ "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.1"
+ },
+ "time": "2021-06-11T22:34:44+00:00"
+ },
+ {
+ "name": "felixfbecker/language-server-protocol",
+ "version": "v1.5.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/felixfbecker/php-language-server-protocol.git",
+ "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/a9e113dbc7d849e35b8776da39edaf4313b7b6c9",
+ "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "*",
+ "squizlabs/php_codesniffer": "^3.1",
+ "vimeo/psalm": "^4.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0.x-dev"
+ "dev-master": "1.x-dev"
}
},
"autoload": {
"psr-4": {
- "Psr\\Http\\Message\\": "src/"
+ "LanguageServerProtocol\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "ISC"
+ ],
+ "authors": [
+ {
+ "name": "Felix Becker",
+ "email": "felix.b@outlook.com"
+ }
+ ],
+ "description": "PHP classes for the Language Server Protocol",
+ "keywords": [
+ "language",
+ "microsoft",
+ "php",
+ "server"
+ ],
+ "support": {
+ "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues",
+ "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.3"
+ },
+ "time": "2024-04-30T00:40:11+00:00"
+ },
+ {
+ "name": "fidry/cpu-core-counter",
+ "version": "1.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theofidry/cpu-core-counter.git",
+ "reference": "db9508f7b1474469d9d3c53b86f817e344732678"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678",
+ "reference": "db9508f7b1474469d9d3c53b86f817e344732678",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "require-dev": {
+ "fidry/makefile": "^0.2.0",
+ "fidry/php-cs-fixer-config": "^1.1.2",
+ "phpstan/extension-installer": "^1.2.0",
+ "phpstan/phpstan": "^2.0",
+ "phpstan/phpstan-deprecation-rules": "^2.0.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^8.5.31 || ^9.5.26",
+ "webmozarts/strict-phpunit": "^7.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Fidry\\CpuCoreCounter\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -2921,51 +2993,62 @@
],
"authors": [
{
- "name": "PHP-FIG",
- "homepage": "https://www.php-fig.org/"
+ "name": "Théo FIDRY",
+ "email": "theo.fidry@gmail.com"
}
],
- "description": "Common interface for HTTP messages",
- "homepage": "https://github.com/php-fig/http-message",
+ "description": "Tiny utility to get the number of CPU cores.",
"keywords": [
- "http",
- "http-message",
- "psr",
- "psr-7",
- "request",
- "response"
+ "CPU",
+ "core"
],
"support": {
- "source": "https://github.com/php-fig/http-message/tree/2.0"
+ "issues": "https://github.com/theofidry/cpu-core-counter/issues",
+ "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0"
},
- "time": "2023-04-04T09:54:51+00:00"
+ "funding": [
+ {
+ "url": "https://github.com/theofidry",
+ "type": "github"
+ }
+ ],
+ "time": "2025-08-14T07:29:31+00:00"
},
{
- "name": "ralouphie/getallheaders",
- "version": "3.0.3",
+ "name": "grasmash/expander",
+ "version": "3.0.1",
"source": {
"type": "git",
- "url": "https://github.com/ralouphie/getallheaders.git",
- "reference": "120b605dfeb996808c31b6477290a714d356e822"
+ "url": "https://github.com/grasmash/expander.git",
+ "reference": "eea11b9afb0c32483b18b9009f4ca07b770e39f4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
- "reference": "120b605dfeb996808c31b6477290a714d356e822",
+ "url": "https://api.github.com/repos/grasmash/expander/zipball/eea11b9afb0c32483b18b9009f4ca07b770e39f4",
+ "reference": "eea11b9afb0c32483b18b9009f4ca07b770e39f4",
"shasum": ""
},
"require": {
- "php": ">=5.6"
+ "dflydev/dot-access-data": "^3.0.0",
+ "php": ">=8.0",
+ "psr/log": "^2 | ^3"
},
"require-dev": {
- "php-coveralls/php-coveralls": "^2.1",
- "phpunit/phpunit": "^5 || ^6.5"
+ "greg-1-anderson/composer-test-scenarios": "^1",
+ "php-coveralls/php-coveralls": "^2.5",
+ "phpunit/phpunit": "^9",
+ "squizlabs/php_codesniffer": "^3.3"
},
"type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
"autoload": {
- "files": [
- "src/getallheaders.php"
- ]
+ "psr-4": {
+ "Grasmash\\Expander\\": "src/"
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@@ -2973,159 +3056,2563 @@
],
"authors": [
{
- "name": "Ralph Khattar",
- "email": "ralph.khattar@gmail.com"
+ "name": "Matthew Grasmick"
}
],
- "description": "A polyfill for getallheaders.",
+ "description": "Expands internal property references in PHP arrays file.",
"support": {
- "issues": "https://github.com/ralouphie/getallheaders/issues",
- "source": "https://github.com/ralouphie/getallheaders/tree/develop"
+ "issues": "https://github.com/grasmash/expander/issues",
+ "source": "https://github.com/grasmash/expander/tree/3.0.1"
},
- "time": "2019-03-08T08:55:37+00:00"
+ "time": "2024-11-25T23:28:05+00:00"
},
{
- "name": "roave/security-advisories",
- "version": "dev-latest",
+ "name": "guzzlehttp/guzzle",
+ "version": "7.10.0",
"source": {
"type": "git",
- "url": "https://github.com/Roave/SecurityAdvisories.git",
- "reference": "4c2208a685ebaa31a0b6ce592e2ee2735542b0fc"
+ "url": "https://github.com/guzzle/guzzle.git",
+ "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/4c2208a685ebaa31a0b6ce592e2ee2735542b0fc",
- "reference": "4c2208a685ebaa31a0b6ce592e2ee2735542b0fc",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
+ "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4",
"shasum": ""
},
- "conflict": {
- "3f/pygmentize": "<1.2",
- "adaptcms/adaptcms": "<=1.3",
- "admidio/admidio": "<4.3.12",
- "adodb/adodb-php": "<=5.22.9",
- "aheinze/cockpit": "<2.2",
- "aimeos/ai-admin-graphql": ">=2022.04.1,<2022.10.10|>=2023.04.1,<2023.10.6|>=2024.04.1,<2024.07.2",
- "aimeos/ai-admin-jsonadm": "<2020.10.13|>=2021.04.1,<2021.10.6|>=2022.04.1,<2022.10.3|>=2023.04.1,<2023.10.4|==2024.04.1",
- "aimeos/ai-client-html": ">=2020.04.1,<2020.10.27|>=2021.04.1,<2021.10.22|>=2022.04.1,<2022.10.13|>=2023.04.1,<2023.10.15|>=2024.04.1,<2024.04.7",
- "aimeos/ai-controller-frontend": "<2020.10.15|>=2021.04.1,<2021.10.8|>=2022.04.1,<2022.10.8|>=2023.04.1,<2023.10.9|==2024.04.1",
- "aimeos/aimeos-core": ">=2022.04.1,<2022.10.17|>=2023.04.1,<2023.10.17|>=2024.04.1,<2024.04.7",
- "aimeos/aimeos-typo3": "<19.10.12|>=20,<20.10.5",
- "airesvsg/acf-to-rest-api": "<=3.1",
- "akaunting/akaunting": "<2.1.13",
- "akeneo/pim-community-dev": "<5.0.119|>=6,<6.0.53",
- "alextselegidis/easyappointments": "<1.5.2.0-beta1",
- "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1",
- "amazing/media2click": ">=1,<1.3.3",
- "ameos/ameos_tarteaucitron": "<1.2.23",
- "amphp/artax": "<1.0.6|>=2,<2.0.6",
- "amphp/http": "<=1.7.2|>=2,<=2.1",
- "amphp/http-client": ">=4,<4.4",
- "anchorcms/anchor-cms": "<=0.12.7",
- "andreapollastri/cipi": "<=3.1.15",
- "andrewhaine/silverstripe-form-capture": ">=0.2,<=0.2.3|>=1,<1.0.2|>=2,<2.2.5",
- "aoe/restler": "<1.7.1",
- "apache-solr-for-typo3/solr": "<2.8.3",
- "apereo/phpcas": "<1.6",
- "api-platform/core": "<3.4.17|>=4,<4.0.22|>=4.1,<4.1.5",
- "api-platform/graphql": "<3.4.17|>=4,<4.0.22|>=4.1,<4.1.5",
- "appwrite/server-ce": "<=1.2.1",
- "arc/web": "<3",
- "area17/twill": "<1.2.5|>=2,<2.5.3",
- "artesaos/seotools": "<0.17.2",
- "asymmetricrypt/asymmetricrypt": "<9.9.99",
- "athlon1600/php-proxy": "<=5.1",
- "athlon1600/php-proxy-app": "<=3",
- "athlon1600/youtube-downloader": "<=4",
- "austintoddj/canvas": "<=3.4.2",
- "auth0/auth0-php": ">=3.3,<=8.16",
- "auth0/login": "<=7.18",
- "auth0/symfony": "<=5.4.1",
- "auth0/wordpress": "<=5.3",
- "automad/automad": "<2.0.0.0-alpha5",
- "automattic/jetpack": "<9.8",
- "awesome-support/awesome-support": "<=6.0.7",
- "aws/aws-sdk-php": "<3.288.1",
- "azuracast/azuracast": "<0.18.3",
- "b13/seo_basics": "<0.8.2",
- "backdrop/backdrop": "<1.27.3|>=1.28,<1.28.2",
- "backpack/crud": "<3.4.9",
- "backpack/filemanager": "<2.0.2|>=3,<3.0.9",
- "bacula-web/bacula-web": "<9.7.1",
- "badaso/core": "<=2.9.11",
- "bagisto/bagisto": "<2.1",
- "barrelstrength/sprout-base-email": "<1.2.7",
- "barrelstrength/sprout-forms": "<3.9",
- "barryvdh/laravel-translation-manager": "<0.6.8",
- "barzahlen/barzahlen-php": "<2.0.1",
- "baserproject/basercms": "<=5.1.1",
- "bassjobsen/bootstrap-3-typeahead": ">4.0.2",
- "bbpress/bbpress": "<2.6.5",
- "bcit-ci/codeigniter": "<3.1.3",
- "bcosca/fatfree": "<3.7.2",
- "bedita/bedita": "<4",
- "bednee/cooluri": "<1.0.30",
- "bigfork/silverstripe-form-capture": ">=3,<3.1.1",
- "billz/raspap-webgui": "<3.3.6",
- "binarytorch/larecipe": "<2.8.1",
- "bk2k/bootstrap-package": ">=7.1,<7.1.2|>=8,<8.0.8|>=9,<9.0.4|>=9.1,<9.1.3|>=10,<10.0.10|>=11,<11.0.3",
- "blueimp/jquery-file-upload": "==6.4.4",
- "bmarshall511/wordpress_zero_spam": "<5.2.13",
- "bolt/bolt": "<3.7.2",
- "bolt/core": "<=4.2",
- "born05/craft-twofactorauthentication": "<3.3.4",
- "bottelet/flarepoint": "<2.2.1",
- "bref/bref": "<2.1.17",
- "brightlocal/phpwhois": "<=4.2.5",
- "brotkrueml/codehighlight": "<2.7",
- "brotkrueml/schema": "<1.13.1|>=2,<2.5.1",
- "brotkrueml/typo3-matomo-integration": "<1.3.2",
- "buddypress/buddypress": "<7.2.1",
- "bugsnag/bugsnag-laravel": ">=2,<2.0.2",
- "bvbmedia/multishop": "<2.0.39",
- "bytefury/crater": "<6.0.2",
- "cachethq/cachet": "<2.5.1",
- "cakephp/cakephp": "<3.10.3|>=4,<4.0.10|>=4.1,<4.1.4|>=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10",
- "cakephp/database": ">=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10",
- "cardgate/magento2": "<2.0.33",
- "cardgate/woocommerce": "<=3.1.15",
- "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4",
- "cart2quote/module-quotation-encoded": ">=4.1.6,<=4.4.5|>=5,<5.4.4",
- "cartalyst/sentry": "<=2.1.6",
- "catfan/medoo": "<1.7.5",
- "causal/oidc": "<4",
- "cecil/cecil": "<7.47.1",
- "centreon/centreon": "<22.10.15",
- "cesnet/simplesamlphp-module-proxystatistics": "<3.1",
- "chriskacerguis/codeigniter-restserver": "<=2.7.1",
- "chrome-php/chrome": "<1.14",
- "civicrm/civicrm-core": ">=4.2,<4.2.9|>=4.3,<4.3.3",
- "ckeditor/ckeditor": "<4.25",
- "clickstorm/cs-seo": ">=6,<6.8|>=7,<7.5|>=8,<8.4|>=9,<9.3",
- "co-stack/fal_sftp": "<0.2.6",
- "cockpit-hq/cockpit": "<2.11.4",
- "codeception/codeception": "<3.1.3|>=4,<4.1.22",
- "codeigniter/framework": "<3.1.10",
- "codeigniter4/framework": "<4.6.2",
- "codeigniter4/shield": "<1.0.0.0-beta8",
- "codiad/codiad": "<=2.8.4",
- "codingms/additional-tca": ">=1.7,<1.15.17|>=1.16,<1.16.9",
- "commerceteam/commerce": ">=0.9.6,<0.9.9",
- "components/jquery": ">=1.0.3,<3.5",
- "composer/composer": "<1.10.27|>=2,<2.2.24|>=2.3,<2.7.7",
- "concrete5/concrete5": "<9.4.3",
- "concrete5/core": "<8.5.8|>=9,<9.1",
- "contao-components/mediaelement": ">=2.14.2,<2.21.1",
- "contao/comments-bundle": ">=2,<4.13.40|>=5.0.0.0-RC1-dev,<5.3.4",
- "contao/contao": ">=3,<3.5.37|>=4,<4.4.56|>=4.5,<4.13.56|>=5,<5.3.38|>=5.4.0.0-RC1-dev,<5.6.1",
+ "require": {
+ "ext-json": "*",
+ "guzzlehttp/promises": "^2.3",
+ "guzzlehttp/psr7": "^2.8",
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-client": "^1.0",
+ "symfony/deprecation-contracts": "^2.2 || ^3.0"
+ },
+ "provide": {
+ "psr/http-client-implementation": "1.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "ext-curl": "*",
+ "guzzle/client-integration-tests": "3.0.2",
+ "php-http/message-factory": "^1.1",
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20",
+ "psr/log": "^1.1 || ^2.0 || ^3.0"
+ },
+ "suggest": {
+ "ext-curl": "Required for CURL handler support",
+ "ext-intl": "Required for Internationalized Domain Name (IDN) support",
+ "psr/log": "Required for using the Log middleware"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "GuzzleHttp\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Jeremy Lindblom",
+ "email": "jeremeamia@gmail.com",
+ "homepage": "https://github.com/jeremeamia"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle is a PHP HTTP client library",
+ "keywords": [
+ "client",
+ "curl",
+ "framework",
+ "http",
+ "http client",
+ "psr-18",
+ "psr-7",
+ "rest",
+ "web service"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/guzzle/issues",
+ "source": "https://github.com/guzzle/guzzle/tree/7.10.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-23T22:36:01+00:00"
+ },
+ {
+ "name": "guzzlehttp/promises",
+ "version": "2.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/promises.git",
+ "reference": "481557b130ef3790cf82b713667b43030dc9c957"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957",
+ "reference": "481557b130ef3790cf82b713667b43030dc9c957",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "phpunit/phpunit": "^8.5.44 || ^9.6.25"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Promise\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle promises library",
+ "keywords": [
+ "promise"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/promises/issues",
+ "source": "https://github.com/guzzle/promises/tree/2.3.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-22T14:34:08+00:00"
+ },
+ {
+ "name": "guzzlehttp/psr7",
+ "version": "2.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/psr7.git",
+ "reference": "21dc724a0583619cd1652f673303492272778051"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051",
+ "reference": "21dc724a0583619cd1652f673303492272778051",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.1 || ^2.0",
+ "ralouphie/getallheaders": "^3.0"
+ },
+ "provide": {
+ "psr/http-factory-implementation": "1.0",
+ "psr/http-message-implementation": "1.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "http-interop/http-factory-tests": "0.9.0",
+ "phpunit/phpunit": "^8.5.44 || ^9.6.25"
+ },
+ "suggest": {
+ "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Psr7\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://sagikazarmark.hu"
+ }
+ ],
+ "description": "PSR-7 message implementation that also provides common utility methods",
+ "keywords": [
+ "http",
+ "message",
+ "psr-7",
+ "request",
+ "response",
+ "stream",
+ "uri",
+ "url"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/psr7/issues",
+ "source": "https://github.com/guzzle/psr7/tree/2.8.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-23T21:21:41+00:00"
+ },
+ {
+ "name": "kubawerlos/php-cs-fixer-custom-fixers",
+ "version": "v3.36.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers.git",
+ "reference": "e1f97f6463f0b2a22e0dd320948a04132ff9c501"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/kubawerlos/php-cs-fixer-custom-fixers/zipball/e1f97f6463f0b2a22e0dd320948a04132ff9c501",
+ "reference": "e1f97f6463f0b2a22e0dd320948a04132ff9c501",
+ "shasum": ""
+ },
+ "require": {
+ "ext-filter": "*",
+ "ext-tokenizer": "*",
+ "friendsofphp/php-cs-fixer": "^3.87",
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.6.24 || ^10.5.51 || ^11.5.44"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PhpCsFixerCustomFixers\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Kuba Werłos",
+ "email": "werlos@gmail.com"
+ }
+ ],
+ "description": "A set of custom fixers for PHP CS Fixer",
+ "support": {
+ "issues": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers/issues",
+ "source": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers/tree/v3.36.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/kubawerlos",
+ "type": "github"
+ }
+ ],
+ "time": "2026-01-31T07:02:11+00:00"
+ },
+ {
+ "name": "league/container",
+ "version": "4.2.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/container.git",
+ "reference": "d3cebb0ff4685ff61c749e54b27db49319e2ec00"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/container/zipball/d3cebb0ff4685ff61c749e54b27db49319e2ec00",
+ "reference": "d3cebb0ff4685ff61c749e54b27db49319e2ec00",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0",
+ "psr/container": "^1.1 || ^2.0"
+ },
+ "provide": {
+ "psr/container-implementation": "^1.0"
+ },
+ "replace": {
+ "orno/di": "~2.0"
+ },
+ "require-dev": {
+ "nette/php-generator": "^3.4",
+ "nikic/php-parser": "^4.10",
+ "phpstan/phpstan": "^0.12.47",
+ "phpunit/phpunit": "^8.5.17",
+ "roave/security-advisories": "dev-latest",
+ "scrutinizer/ocular": "^1.8",
+ "squizlabs/php_codesniffer": "^3.6"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-1.x": "1.x-dev",
+ "dev-2.x": "2.x-dev",
+ "dev-3.x": "3.x-dev",
+ "dev-4.x": "4.x-dev",
+ "dev-master": "4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "League\\Container\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Phil Bennett",
+ "email": "mail@philbennett.co.uk",
+ "role": "Developer"
+ }
+ ],
+ "description": "A fast and intuitive dependency injection container.",
+ "homepage": "https://github.com/thephpleague/container",
+ "keywords": [
+ "container",
+ "dependency",
+ "di",
+ "injection",
+ "league",
+ "provider",
+ "service"
+ ],
+ "support": {
+ "issues": "https://github.com/thephpleague/container/issues",
+ "source": "https://github.com/thephpleague/container/tree/4.2.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/philipobenito",
+ "type": "github"
+ }
+ ],
+ "time": "2025-05-20T12:55:37+00:00"
+ },
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.13.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.6.8",
+ "doctrine/common": "<2.13.3 || >=3 <3.2.2"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.6.8",
+ "doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpspec/prophecy": "^1.10",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ],
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-01T08:46:24+00:00"
+ },
+ {
+ "name": "netresearch/jsonmapper",
+ "version": "v4.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/cweiske/jsonmapper.git",
+ "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8e76efb98ee8b6afc54687045e1b8dba55ac76e5",
+ "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "ext-pcre": "*",
+ "ext-reflection": "*",
+ "ext-spl": "*",
+ "php": ">=7.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0 || ~10.0",
+ "squizlabs/php_codesniffer": "~3.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "JsonMapper": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "OSL-3.0"
+ ],
+ "authors": [
+ {
+ "name": "Christian Weiske",
+ "email": "cweiske@cweiske.de",
+ "homepage": "http://github.com/cweiske/jsonmapper/",
+ "role": "Developer"
+ }
+ ],
+ "description": "Map nested JSON structures onto PHP classes",
+ "support": {
+ "email": "cweiske@cweiske.de",
+ "issues": "https://github.com/cweiske/jsonmapper/issues",
+ "source": "https://github.com/cweiske/jsonmapper/tree/v4.5.0"
+ },
+ "time": "2024-09-08T10:13:13+00:00"
+ },
+ {
+ "name": "nextcloud/coding-standard",
+ "version": "v1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nextcloud/coding-standard.git",
+ "reference": "8e06808c1423e9208d63d1bd205b9a38bd400011"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nextcloud/coding-standard/zipball/8e06808c1423e9208d63d1bd205b9a38bd400011",
+ "reference": "8e06808c1423e9208d63d1bd205b9a38bd400011",
+ "shasum": ""
+ },
+ "require": {
+ "kubawerlos/php-cs-fixer-custom-fixers": "^3.22",
+ "php": "^8.0",
+ "php-cs-fixer/shim": "^3.17"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Nextcloud\\CodingStandard\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christoph Wurst",
+ "email": "christoph@winzerhof-wurst.at"
+ }
+ ],
+ "description": "Nextcloud coding standards for the php cs fixer",
+ "keywords": [
+ "dev"
+ ],
+ "support": {
+ "issues": "https://github.com/nextcloud/coding-standard/issues",
+ "source": "https://github.com/nextcloud/coding-standard/tree/v1.4.0"
+ },
+ "time": "2025-06-19T12:27:27+00:00"
+ },
+ {
+ "name": "nextcloud/ocp",
+ "version": "v31.0.9",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nextcloud-deps/ocp.git",
+ "reference": "abd32429d794ede1d92b7b0a88a1070371c907b5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/abd32429d794ede1d92b7b0a88a1070371c907b5",
+ "reference": "abd32429d794ede1d92b7b0a88a1070371c907b5",
+ "shasum": ""
+ },
+ "require": {
+ "php": "~8.1 || ~8.2 || ~8.3 || ~8.4",
+ "psr/clock": "^1.0",
+ "psr/container": "^2.0.2",
+ "psr/event-dispatcher": "^1.0",
+ "psr/log": "^3.0.2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-stable31": "31.0.0-dev"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "AGPL-3.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Christoph Wurst",
+ "email": "christoph@winzerhof-wurst.at"
+ },
+ {
+ "name": "Joas Schilling",
+ "email": "coding@schilljs.com"
+ }
+ ],
+ "description": "Composer package containing Nextcloud's public OCP API and the unstable NCU API",
+ "support": {
+ "issues": "https://github.com/nextcloud-deps/ocp/issues",
+ "source": "https://github.com/nextcloud-deps/ocp/tree/v31.0.9"
+ },
+ "time": "2025-07-31T00:57:37+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v4.19.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "51bd93cc741b7fc3d63d20b6bdcd99fdaa359837"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/51bd93cc741b7fc3d63d20b6bdcd99fdaa359837",
+ "reference": "51bd93cc741b7fc3d63d20b6bdcd99fdaa359837",
+ "shasum": ""
+ },
+ "require": {
+ "ext-tokenizer": "*",
+ "php": ">=7.1"
+ },
+ "require-dev": {
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PhpParser\\": "lib/PhpParser"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov"
+ }
+ ],
+ "description": "A PHP parser written in PHP",
+ "keywords": [
+ "parser",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/PHP-Parser/issues",
+ "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.5"
+ },
+ "time": "2025-12-06T11:45:25+00:00"
+ },
+ {
+ "name": "pdepend/pdepend",
+ "version": "2.16.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/pdepend/pdepend.git",
+ "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/pdepend/pdepend/zipball/f942b208dc2a0868454d01b29f0c75bbcfc6ed58",
+ "reference": "f942b208dc2a0868454d01b29f0c75bbcfc6ed58",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.7",
+ "symfony/config": "^2.3.0|^3|^4|^5|^6.0|^7.0",
+ "symfony/dependency-injection": "^2.3.0|^3|^4|^5|^6.0|^7.0",
+ "symfony/filesystem": "^2.3.0|^3|^4|^5|^6.0|^7.0",
+ "symfony/polyfill-mbstring": "^1.19"
+ },
+ "require-dev": {
+ "easy-doc/easy-doc": "0.0.0|^1.2.3",
+ "gregwar/rst": "^1.0",
+ "squizlabs/php_codesniffer": "^2.0.0"
+ },
+ "bin": [
+ "src/bin/pdepend"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PDepend\\": "src/main/php/PDepend"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Official version of pdepend to be handled with Composer",
+ "keywords": [
+ "PHP Depend",
+ "PHP_Depend",
+ "dev",
+ "pdepend"
+ ],
+ "support": {
+ "issues": "https://github.com/pdepend/pdepend/issues",
+ "source": "https://github.com/pdepend/pdepend/tree/2.16.2"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/pdepend/pdepend",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-12-17T18:09:59+00:00"
+ },
+ {
+ "name": "phar-io/manifest",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/manifest.git",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-phar": "*",
+ "ext-xmlwriter": "*",
+ "phar-io/version": "^3.0.1",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "support": {
+ "issues": "https://github.com/phar-io/manifest/issues",
+ "source": "https://github.com/phar-io/manifest/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:33:53+00:00"
+ },
+ {
+ "name": "phar-io/version",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Library for handling version information and constraints",
+ "support": {
+ "issues": "https://github.com/phar-io/version/issues",
+ "source": "https://github.com/phar-io/version/tree/3.2.1"
+ },
+ "time": "2022-02-21T01:04:05+00:00"
+ },
+ {
+ "name": "phootwork/collection",
+ "version": "v3.2.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phootwork/collection.git",
+ "reference": "46dde20420fba17766c89200bc3ff91d3e58eafa"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phootwork/collection/zipball/46dde20420fba17766c89200bc3ff91d3e58eafa",
+ "reference": "46dde20420fba17766c89200bc3ff91d3e58eafa",
+ "shasum": ""
+ },
+ "require": {
+ "phootwork/lang": "^3.0",
+ "php": ">=8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "phootwork\\collection\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Thomas Gossmann",
+ "homepage": "http://gos.si"
+ }
+ ],
+ "description": "The phootwork library fills gaps in the php language and provides better solutions than the existing ones php offers.",
+ "homepage": "https://phootwork.github.io/collection/",
+ "keywords": [
+ "Array object",
+ "Text object",
+ "collection",
+ "collections",
+ "json",
+ "list",
+ "map",
+ "queue",
+ "set",
+ "stack",
+ "xml"
+ ],
+ "support": {
+ "issues": "https://github.com/phootwork/phootwork/issues",
+ "source": "https://github.com/phootwork/collection/tree/v3.2.3"
+ },
+ "time": "2022-08-27T12:51:24+00:00"
+ },
+ {
+ "name": "phootwork/lang",
+ "version": "v3.2.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phootwork/lang.git",
+ "reference": "52ec8cce740ce1c424eef02f43b43d5ddfec7b5e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phootwork/lang/zipball/52ec8cce740ce1c424eef02f43b43d5ddfec7b5e",
+ "reference": "52ec8cce740ce1c424eef02f43b43d5ddfec7b5e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0",
+ "symfony/polyfill-mbstring": "^1.12",
+ "symfony/polyfill-php81": "^1.22"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "phootwork\\lang\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Thomas Gossmann",
+ "homepage": "http://gos.si"
+ }
+ ],
+ "description": "Missing PHP language constructs",
+ "homepage": "https://phootwork.github.io/lang/",
+ "keywords": [
+ "array",
+ "comparator",
+ "comparison",
+ "string"
+ ],
+ "support": {
+ "issues": "https://github.com/phootwork/phootwork/issues",
+ "source": "https://github.com/phootwork/lang/tree/v3.2.3"
+ },
+ "time": "2024-10-03T13:43:19+00:00"
+ },
+ {
+ "name": "php-cs-fixer/shim",
+ "version": "v3.94.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHP-CS-Fixer/shim.git",
+ "reference": "80fd29f44a736136a2f05bae5464816a444b91d1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHP-CS-Fixer/shim/zipball/80fd29f44a736136a2f05bae5464816a444b91d1",
+ "reference": "80fd29f44a736136a2f05bae5464816a444b91d1",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "ext-tokenizer": "*",
+ "php": "^7.4 || ^8.0"
+ },
+ "replace": {
+ "friendsofphp/php-cs-fixer": "self.version"
+ },
+ "suggest": {
+ "ext-dom": "For handling output formats in XML",
+ "ext-mbstring": "For handling non-UTF8 characters."
+ },
+ "bin": [
+ "php-cs-fixer",
+ "php-cs-fixer.phar"
+ ],
+ "type": "application",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Dariusz Rumiński",
+ "email": "dariusz.ruminski@gmail.com"
+ }
+ ],
+ "description": "A tool to automatically fix PHP code style",
+ "support": {
+ "issues": "https://github.com/PHP-CS-Fixer/shim/issues",
+ "source": "https://github.com/PHP-CS-Fixer/shim/tree/v3.94.2"
+ },
+ "time": "2026-02-20T16:14:17+00:00"
+ },
+ {
+ "name": "phpcsstandards/phpcsextra",
+ "version": "1.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPCSStandards/PHPCSExtra.git",
+ "reference": "b598aa890815b8df16363271b659d73280129101"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/b598aa890815b8df16363271b659d73280129101",
+ "reference": "b598aa890815b8df16363271b659d73280129101",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4",
+ "phpcsstandards/phpcsutils": "^1.2.0",
+ "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1"
+ },
+ "require-dev": {
+ "php-parallel-lint/php-console-highlighter": "^1.0",
+ "php-parallel-lint/php-parallel-lint": "^1.4.0",
+ "phpcsstandards/phpcsdevcs": "^1.2.0",
+ "phpcsstandards/phpcsdevtools": "^1.2.1",
+ "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4"
+ },
+ "type": "phpcodesniffer-standard",
+ "extra": {
+ "branch-alias": {
+ "dev-stable": "1.x-dev",
+ "dev-develop": "1.x-dev"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-3.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Juliette Reinders Folmer",
+ "homepage": "https://github.com/jrfnl",
+ "role": "lead"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors"
+ }
+ ],
+ "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.",
+ "keywords": [
+ "PHP_CodeSniffer",
+ "phpcbf",
+ "phpcodesniffer-standard",
+ "phpcs",
+ "standards",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues",
+ "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy",
+ "source": "https://github.com/PHPCSStandards/PHPCSExtra"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/PHPCSStandards",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/phpcsstandards",
+ "type": "thanks_dev"
+ }
+ ],
+ "time": "2025-11-12T23:06:57+00:00"
+ },
+ {
+ "name": "phpcsstandards/phpcsutils",
+ "version": "1.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPCSStandards/PHPCSUtils.git",
+ "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/c216317e96c8b3f5932808f9b0f1f7a14e3bbf55",
+ "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55",
+ "shasum": ""
+ },
+ "require": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0",
+ "php": ">=5.4",
+ "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1"
+ },
+ "require-dev": {
+ "ext-filter": "*",
+ "php-parallel-lint/php-console-highlighter": "^1.0",
+ "php-parallel-lint/php-parallel-lint": "^1.4.0",
+ "phpcsstandards/phpcsdevcs": "^1.2.0",
+ "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0"
+ },
+ "type": "phpcodesniffer-standard",
+ "extra": {
+ "branch-alias": {
+ "dev-stable": "1.x-dev",
+ "dev-develop": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "PHPCSUtils/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-3.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Juliette Reinders Folmer",
+ "homepage": "https://github.com/jrfnl",
+ "role": "lead"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors"
+ }
+ ],
+ "description": "A suite of utility functions for use with PHP_CodeSniffer",
+ "homepage": "https://phpcsutils.com/",
+ "keywords": [
+ "PHP_CodeSniffer",
+ "phpcbf",
+ "phpcodesniffer-standard",
+ "phpcs",
+ "phpcs3",
+ "phpcs4",
+ "standards",
+ "static analysis",
+ "tokens",
+ "utility"
+ ],
+ "support": {
+ "docs": "https://phpcsutils.com/",
+ "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues",
+ "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy",
+ "source": "https://github.com/PHPCSStandards/PHPCSUtils"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/PHPCSStandards",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/phpcsstandards",
+ "type": "thanks_dev"
+ }
+ ],
+ "time": "2025-12-08T14:27:58+00:00"
+ },
+ {
+ "name": "phpdocumentor/reflection-common",
+ "version": "2.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
+ "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
+ "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-2.x": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jaap van Otterdijk",
+ "email": "opensource@ijaap.nl"
+ }
+ ],
+ "description": "Common reflection classes used by phpdocumentor to reflect the code structure",
+ "homepage": "http://www.phpdoc.org",
+ "keywords": [
+ "FQSEN",
+ "phpDocumentor",
+ "phpdoc",
+ "reflection",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
+ "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
+ },
+ "time": "2020-06-27T09:03:43+00:00"
+ },
+ {
+ "name": "phpdocumentor/reflection-docblock",
+ "version": "5.6.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
+ "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/5cee1d3dfc2d2aa6599834520911d246f656bcb8",
+ "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/deprecations": "^1.1",
+ "ext-filter": "*",
+ "php": "^7.4 || ^8.0",
+ "phpdocumentor/reflection-common": "^2.2",
+ "phpdocumentor/type-resolver": "^1.7",
+ "phpstan/phpdoc-parser": "^1.7|^2.0",
+ "webmozart/assert": "^1.9.1 || ^2"
+ },
+ "require-dev": {
+ "mockery/mockery": "~1.3.5 || ~1.6.0",
+ "phpstan/extension-installer": "^1.1",
+ "phpstan/phpstan": "^1.8",
+ "phpstan/phpstan-mockery": "^1.1",
+ "phpstan/phpstan-webmozart-assert": "^1.2",
+ "phpunit/phpunit": "^9.5",
+ "psalm/phar": "^5.26"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ "email": "me@mikevanriel.com"
+ },
+ {
+ "name": "Jaap van Otterdijk",
+ "email": "opensource@ijaap.nl"
+ }
+ ],
+ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
+ "support": {
+ "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
+ "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.6"
+ },
+ "time": "2025-12-22T21:13:58+00:00"
+ },
+ {
+ "name": "phpdocumentor/type-resolver",
+ "version": "1.12.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/TypeResolver.git",
+ "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195",
+ "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/deprecations": "^1.0",
+ "php": "^7.3 || ^8.0",
+ "phpdocumentor/reflection-common": "^2.0",
+ "phpstan/phpdoc-parser": "^1.18|^2.0"
+ },
+ "require-dev": {
+ "ext-tokenizer": "*",
+ "phpbench/phpbench": "^1.2",
+ "phpstan/extension-installer": "^1.1",
+ "phpstan/phpstan": "^1.8",
+ "phpstan/phpstan-phpunit": "^1.1",
+ "phpunit/phpunit": "^9.5",
+ "rector/rector": "^0.13.9",
+ "vimeo/psalm": "^4.25"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-1.x": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ "email": "me@mikevanriel.com"
+ }
+ ],
+ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
+ "support": {
+ "issues": "https://github.com/phpDocumentor/TypeResolver/issues",
+ "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0"
+ },
+ "time": "2025-11-21T15:09:14+00:00"
+ },
+ {
+ "name": "phpmd/phpmd",
+ "version": "2.15.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpmd/phpmd.git",
+ "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpmd/phpmd/zipball/74a1f56e33afad4128b886e334093e98e1b5e7c0",
+ "reference": "74a1f56e33afad4128b886e334093e98e1b5e7c0",
+ "shasum": ""
+ },
+ "require": {
+ "composer/xdebug-handler": "^1.0 || ^2.0 || ^3.0",
+ "ext-xml": "*",
+ "pdepend/pdepend": "^2.16.1",
+ "php": ">=5.3.9"
+ },
+ "require-dev": {
+ "easy-doc/easy-doc": "0.0.0 || ^1.3.2",
+ "ext-json": "*",
+ "ext-simplexml": "*",
+ "gregwar/rst": "^1.0",
+ "mikey179/vfsstream": "^1.6.8",
+ "squizlabs/php_codesniffer": "^2.9.2 || ^3.7.2"
+ },
+ "bin": [
+ "src/bin/phpmd"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "PHPMD\\": "src/main/php"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Manuel Pichler",
+ "email": "github@manuel-pichler.de",
+ "homepage": "https://github.com/manuelpichler",
+ "role": "Project Founder"
+ },
+ {
+ "name": "Marc Würth",
+ "email": "ravage@bluewin.ch",
+ "homepage": "https://github.com/ravage84",
+ "role": "Project Maintainer"
+ },
+ {
+ "name": "Other contributors",
+ "homepage": "https://github.com/phpmd/phpmd/graphs/contributors",
+ "role": "Contributors"
+ }
+ ],
+ "description": "PHPMD is a spin-off project of PHP Depend and aims to be a PHP equivalent of the well known Java tool PMD.",
+ "homepage": "https://phpmd.org/",
+ "keywords": [
+ "dev",
+ "mess detection",
+ "mess detector",
+ "pdepend",
+ "phpmd",
+ "pmd"
+ ],
+ "support": {
+ "irc": "irc://irc.freenode.org/phpmd",
+ "issues": "https://github.com/phpmd/phpmd/issues",
+ "source": "https://github.com/phpmd/phpmd/tree/2.15.0"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpmd/phpmd",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-12-11T08:22:20+00:00"
+ },
+ {
+ "name": "phpmetrics/phpmetrics",
+ "version": "v2.9.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpmetrics/PhpMetrics.git",
+ "reference": "e2e68ddd1543bc3f44402c383f7bccb62de1ece3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpmetrics/PhpMetrics/zipball/e2e68ddd1543bc3f44402c383f7bccb62de1ece3",
+ "reference": "e2e68ddd1543bc3f44402c383f7bccb62de1ece3",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-tokenizer": "*",
+ "nikic/php-parser": "^3|^4|^5"
+ },
+ "replace": {
+ "halleck45/php-metrics": "*",
+ "halleck45/phpmetrics": "*"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "*"
+ },
+ "bin": [
+ "bin/phpmetrics"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "./src/functions.php"
+ ],
+ "psr-0": {
+ "Hal\\": "./src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jean-François Lépine",
+ "email": "lepinejeanfrancois@yahoo.fr",
+ "homepage": "http://www.lepine.pro",
+ "role": "Copyright Holder"
+ }
+ ],
+ "description": "Static analyzer tool for PHP : Coupling, Cyclomatic complexity, Maintainability Index, Halstead's metrics... and more !",
+ "homepage": "http://www.phpmetrics.org",
+ "keywords": [
+ "analysis",
+ "qa",
+ "quality",
+ "testing"
+ ],
+ "support": {
+ "issues": "https://github.com/PhpMetrics/PhpMetrics/issues",
+ "source": "https://github.com/phpmetrics/PhpMetrics/tree/v2.9.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Halleck45",
+ "type": "github"
+ }
+ ],
+ "time": "2025-09-25T05:21:02+00:00"
+ },
+ {
+ "name": "phpowermove/docblock",
+ "version": "v4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpowermove/docblock.git",
+ "reference": "a73f6e17b7d4e1b92ca5378c248c952c9fae7826"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpowermove/docblock/zipball/a73f6e17b7d4e1b92ca5378c248c952c9fae7826",
+ "reference": "a73f6e17b7d4e1b92ca5378c248c952c9fae7826",
+ "shasum": ""
+ },
+ "require": {
+ "phootwork/collection": "^3.0",
+ "phootwork/lang": "^3.0",
+ "php": ">=8.0"
+ },
+ "require-dev": {
+ "phootwork/php-cs-fixer-config": "^0.4",
+ "phpunit/phpunit": "^9.0",
+ "psalm/phar": "^4.3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "phpowermove\\docblock\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Thomas Gossmann",
+ "homepage": "http://gos.si"
+ }
+ ],
+ "description": "PHP Docblock parser and generator. An API to read and write Docblocks.",
+ "keywords": [
+ "docblock",
+ "generator",
+ "parser"
+ ],
+ "support": {
+ "issues": "https://github.com/phpowermove/docblock/issues",
+ "source": "https://github.com/phpowermove/docblock/tree/v4.0"
+ },
+ "time": "2021-09-22T16:57:06+00:00"
+ },
+ {
+ "name": "phpstan/phpdoc-parser",
+ "version": "2.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpdoc-parser.git",
+ "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a",
+ "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/annotations": "^2.0",
+ "nikic/php-parser": "^5.3.0",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/extension-installer": "^1.0",
+ "phpstan/phpstan": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^9.6",
+ "symfony/process": "^5.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\PhpDocParser\\": [
+ "src/"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPDoc parser with support for nullable, intersection and generic types",
+ "support": {
+ "issues": "https://github.com/phpstan/phpdoc-parser/issues",
+ "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2"
+ },
+ "time": "2026-01-25T14:56:51+00:00"
+ },
+ {
+ "name": "phpstan/phpstan",
+ "version": "1.12.33",
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/37982d6fc7cbb746dda7773530cda557cdf119e1",
+ "reference": "37982d6fc7cbb746dda7773530cda557cdf119e1",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2|^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan-shim": "*"
+ },
+ "bin": [
+ "phpstan",
+ "phpstan.phar"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPStan - PHP Static Analysis Tool",
+ "keywords": [
+ "dev",
+ "static analysis"
+ ],
+ "support": {
+ "docs": "https://phpstan.org/user-guide/getting-started",
+ "forum": "https://github.com/phpstan/phpstan/discussions",
+ "issues": "https://github.com/phpstan/phpstan/issues",
+ "security": "https://github.com/phpstan/phpstan/security/policy",
+ "source": "https://github.com/phpstan/phpstan-src"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/ondrejmirtes",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/phpstan",
+ "type": "github"
+ }
+ ],
+ "time": "2026-02-28T20:30:03+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "10.1.16",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "7e308268858ed6baedc8704a304727d20bc07c77"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77",
+ "reference": "7e308268858ed6baedc8704a304727d20bc07c77",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-xmlwriter": "*",
+ "nikic/php-parser": "^4.19.1 || ^5.1.0",
+ "php": ">=8.1",
+ "phpunit/php-file-iterator": "^4.1.0",
+ "phpunit/php-text-template": "^3.0.1",
+ "sebastian/code-unit-reverse-lookup": "^3.0.0",
+ "sebastian/complexity": "^3.2.0",
+ "sebastian/environment": "^6.1.0",
+ "sebastian/lines-of-code": "^2.0.2",
+ "sebastian/version": "^4.0.1",
+ "theseer/tokenizer": "^1.2.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.1"
+ },
+ "suggest": {
+ "ext-pcov": "PHP extension that provides line coverage",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "10.1.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+ "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-08-22T04:31:57+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "4.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c",
+ "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+ "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-08-31T06:24:48+00:00"
+ },
+ {
+ "name": "phpunit/php-invoker",
+ "version": "4.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-invoker.git",
+ "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7",
+ "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^10.0"
+ },
+ "suggest": {
+ "ext-pcntl": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Invoke callables with a timeout",
+ "homepage": "https://github.com/sebastianbergmann/php-invoker/",
+ "keywords": [
+ "process"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:56:09+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "3.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748",
+ "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+ "security": "https://github.com/sebastianbergmann/php-text-template/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-08-31T14:07:24+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "6.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d",
+ "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:57:52+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "10.5.63",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "33198268dad71e926626b618f3ec3966661e4d90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90",
+ "reference": "33198268dad71e926626b618f3ec3966661e4d90",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xml": "*",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.13.4",
+ "phar-io/manifest": "^2.0.4",
+ "phar-io/version": "^3.2.1",
+ "php": ">=8.1",
+ "phpunit/php-code-coverage": "^10.1.16",
+ "phpunit/php-file-iterator": "^4.1.0",
+ "phpunit/php-invoker": "^4.0.0",
+ "phpunit/php-text-template": "^3.0.1",
+ "phpunit/php-timer": "^6.0.0",
+ "sebastian/cli-parser": "^2.0.1",
+ "sebastian/code-unit": "^2.0.0",
+ "sebastian/comparator": "^5.0.5",
+ "sebastian/diff": "^5.1.1",
+ "sebastian/environment": "^6.1.0",
+ "sebastian/exporter": "^5.1.4",
+ "sebastian/global-state": "^6.0.2",
+ "sebastian/object-enumerator": "^5.0.0",
+ "sebastian/recursion-context": "^5.0.1",
+ "sebastian/type": "^4.0.0",
+ "sebastian/version": "^4.0.1"
+ },
+ "suggest": {
+ "ext-soap": "To be able to generate mocks based on WSDL files"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "10.5-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Framework/Assert/Functions.php"
+ ],
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+ "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63"
+ },
+ "funding": [
+ {
+ "url": "https://phpunit.de/sponsors.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-27T05:48:37+00:00"
+ },
+ {
+ "name": "psr/clock",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/clock.git",
+ "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d",
+ "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Psr\\Clock\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for reading the clock.",
+ "homepage": "https://github.com/php-fig/clock",
+ "keywords": [
+ "clock",
+ "now",
+ "psr",
+ "psr-20",
+ "time"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/clock/issues",
+ "source": "https://github.com/php-fig/clock/tree/1.0.0"
+ },
+ "time": "2022-11-25T14:36:26+00:00"
+ },
+ {
+ "name": "psr/http-client",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-client.git",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP clients",
+ "homepage": "https://github.com/php-fig/http-client",
+ "keywords": [
+ "http",
+ "http-client",
+ "psr",
+ "psr-18"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-client"
+ },
+ "time": "2023-09-23T14:17:50+00:00"
+ },
+ {
+ "name": "psr/http-factory",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-factory.git",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "psr",
+ "psr-17",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-factory"
+ },
+ "time": "2024-04-15T12:06:14+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/2.0"
+ },
+ "time": "2023-04-04T09:54:51+00:00"
+ },
+ {
+ "name": "ralouphie/getallheaders",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ralouphie/getallheaders.git",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.1",
+ "phpunit/phpunit": "^5 || ^6.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/getallheaders.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ralph Khattar",
+ "email": "ralph.khattar@gmail.com"
+ }
+ ],
+ "description": "A polyfill for getallheaders.",
+ "support": {
+ "issues": "https://github.com/ralouphie/getallheaders/issues",
+ "source": "https://github.com/ralouphie/getallheaders/tree/develop"
+ },
+ "time": "2019-03-08T08:55:37+00:00"
+ },
+ {
+ "name": "roave/security-advisories",
+ "version": "dev-latest",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Roave/SecurityAdvisories.git",
+ "reference": "5a17df9a478a03dff7428a3172cf7204d4d017a8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/5a17df9a478a03dff7428a3172cf7204d4d017a8",
+ "reference": "5a17df9a478a03dff7428a3172cf7204d4d017a8",
+ "shasum": ""
+ },
+ "conflict": {
+ "3f/pygmentize": "<1.2",
+ "adaptcms/adaptcms": "<=1.3",
+ "admidio/admidio": "<=4.3.16",
+ "adodb/adodb-php": "<=5.22.9",
+ "aheinze/cockpit": "<2.2",
+ "aimeos/ai-admin-graphql": ">=2022.04.1,<2022.10.10|>=2023.04.1,<2023.10.6|>=2024.04.1,<2024.07.2",
+ "aimeos/ai-admin-jsonadm": "<2020.10.13|>=2021.04.1,<2021.10.6|>=2022.04.1,<2022.10.3|>=2023.04.1,<2023.10.4|==2024.04.1",
+ "aimeos/ai-client-html": ">=2020.04.1,<2020.10.27|>=2021.04.1,<2021.10.22|>=2022.04.1,<2022.10.13|>=2023.04.1,<2023.10.15|>=2024.04.1,<2024.04.7",
+ "aimeos/ai-cms-grapesjs": ">=2021.04.1,<2021.10.8|>=2022.04.1,<2022.10.9|>=2023.04.1,<2023.10.15|>=2024.04.1,<2024.10.8|>=2025.04.1,<2025.10.2",
+ "aimeos/ai-controller-frontend": "<2020.10.15|>=2021.04.1,<2021.10.8|>=2022.04.1,<2022.10.8|>=2023.04.1,<2023.10.9|==2024.04.1",
+ "aimeos/aimeos-core": ">=2022.04.1,<2022.10.17|>=2023.04.1,<2023.10.17|>=2024.04.1,<2024.04.7",
+ "aimeos/aimeos-laravel": "==2021.10",
+ "aimeos/aimeos-typo3": "<19.10.12|>=20,<20.10.5",
+ "airesvsg/acf-to-rest-api": "<=3.1",
+ "akaunting/akaunting": "<2.1.13",
+ "akeneo/pim-community-dev": "<5.0.119|>=6,<6.0.53",
+ "alextselegidis/easyappointments": "<=1.5.2",
+ "alexusmai/laravel-file-manager": "<=3.3.1",
+ "algolia/algoliasearch-magento-2": "<=3.16.1|>=3.17.0.0-beta1,<=3.17.1",
+ "alt-design/alt-redirect": "<1.6.4",
+ "altcha-org/altcha": "<1.3.1",
+ "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1",
+ "amazing/media2click": ">=1,<1.3.3",
+ "ameos/ameos_tarteaucitron": "<1.2.23",
+ "amphp/artax": "<1.0.6|>=2,<2.0.6",
+ "amphp/http": "<=1.7.2|>=2,<=2.1",
+ "amphp/http-client": ">=4,<4.4",
+ "amphp/http-server": ">=2.0.0.0-RC1-dev,<2.1.10|>=3.0.0.0-beta1,<3.4.4",
+ "anchorcms/anchor-cms": "<=0.12.7",
+ "andreapollastri/cipi": "<=3.1.15",
+ "andrewhaine/silverstripe-form-capture": ">=0.2,<=0.2.3|>=1,<1.0.2|>=2,<2.2.5",
+ "aoe/restler": "<1.7.1",
+ "apache-solr-for-typo3/solr": "<2.8.3",
+ "apereo/phpcas": "<1.6",
+ "api-platform/core": "<3.4.17|>=4,<4.0.22|>=4.1,<4.1.5",
+ "api-platform/graphql": "<3.4.17|>=4,<4.0.22|>=4.1,<4.1.5",
+ "appwrite/server-ce": "<=1.2.1",
+ "arc/web": "<3",
+ "area17/twill": "<1.2.5|>=2,<2.5.3",
+ "artesaos/seotools": "<0.17.2",
+ "asymmetricrypt/asymmetricrypt": "<9.9.99",
+ "athlon1600/php-proxy": "<=5.1",
+ "athlon1600/php-proxy-app": "<=3",
+ "athlon1600/youtube-downloader": "<=4",
+ "austintoddj/canvas": "<=3.4.2",
+ "auth0/auth0-php": ">=3.3,<8.18",
+ "auth0/login": "<7.20",
+ "auth0/symfony": "<=5.5",
+ "auth0/wordpress": "<=5.4",
+ "automad/automad": "<2.0.0.0-alpha5",
+ "automattic/jetpack": "<9.8",
+ "awesome-support/awesome-support": "<=6.0.7",
+ "aws/aws-sdk-php": "<3.368",
+ "azuracast/azuracast": "<=0.23.1",
+ "b13/seo_basics": "<0.8.2",
+ "backdrop/backdrop": "<=1.32",
+ "backpack/crud": "<3.4.9",
+ "backpack/filemanager": "<2.0.2|>=3,<3.0.9",
+ "bacula-web/bacula-web": "<9.7.1",
+ "badaso/core": "<=2.9.11",
+ "bagisto/bagisto": "<2.3.10",
+ "barrelstrength/sprout-base-email": "<1.2.7",
+ "barrelstrength/sprout-forms": "<3.9",
+ "barryvdh/laravel-translation-manager": "<0.6.8",
+ "barzahlen/barzahlen-php": "<2.0.1",
+ "baserproject/basercms": "<=5.1.1",
+ "bassjobsen/bootstrap-3-typeahead": ">4.0.2",
+ "bbpress/bbpress": "<2.6.5",
+ "bcit-ci/codeigniter": "<3.1.3",
+ "bcosca/fatfree": "<3.7.2",
+ "bedita/bedita": "<4",
+ "bednee/cooluri": "<1.0.30",
+ "bigfork/silverstripe-form-capture": ">=3,<3.1.1",
+ "billz/raspap-webgui": "<3.3.6",
+ "binarytorch/larecipe": "<2.8.1",
+ "bk2k/bootstrap-package": ">=7.1,<7.1.2|>=8,<8.0.8|>=9,<9.0.4|>=9.1,<9.1.3|>=10,<10.0.10|>=11,<11.0.3",
+ "blueimp/jquery-file-upload": "==6.4.4",
+ "bmarshall511/wordpress_zero_spam": "<5.2.13",
+ "bolt/bolt": "<3.7.2",
+ "bolt/core": "<=4.2",
+ "born05/craft-twofactorauthentication": "<3.3.4",
+ "bottelet/flarepoint": "<2.2.1",
+ "bref/bref": "<2.1.17",
+ "brightlocal/phpwhois": "<=4.2.5",
+ "brotkrueml/codehighlight": "<2.7",
+ "brotkrueml/schema": "<1.13.1|>=2,<2.5.1",
+ "brotkrueml/typo3-matomo-integration": "<1.3.2",
+ "buddypress/buddypress": "<7.2.1",
+ "bugsnag/bugsnag-laravel": ">=2,<2.0.2",
+ "bvbmedia/multishop": "<2.0.39",
+ "bytefury/crater": "<6.0.2",
+ "cachethq/cachet": "<2.5.1",
+ "cadmium-org/cadmium-cms": "<=0.4.9",
+ "cakephp/cakephp": "<3.10.3|>=4,<4.0.10|>=4.1,<4.1.4|>=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10|>=5.2.10,<5.2.12|==5.3",
+ "cakephp/database": ">=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10",
+ "cardgate/magento2": "<2.0.33",
+ "cardgate/woocommerce": "<=3.1.15",
+ "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4",
+ "cart2quote/module-quotation-encoded": ">=4.1.6,<=4.4.5|>=5,<5.4.4",
+ "cartalyst/sentry": "<=2.1.6",
+ "catfan/medoo": "<1.7.5",
+ "causal/oidc": "<4",
+ "cecil/cecil": "<7.47.1",
+ "centreon/centreon": "<22.10.15",
+ "cesargb/laravel-magiclink": ">=2,<2.25.1",
+ "cesnet/simplesamlphp-module-proxystatistics": "<3.1",
+ "chriskacerguis/codeigniter-restserver": "<=2.7.1",
+ "chrome-php/chrome": "<1.14",
+ "ci4-cms-erp/ci4ms": "<0.28.5",
+ "civicrm/civicrm-core": ">=4.2,<4.2.9|>=4.3,<4.3.3",
+ "ckeditor/ckeditor": "<4.25",
+ "clickstorm/cs-seo": ">=6,<6.8|>=7,<7.5|>=8,<8.4|>=9,<9.3",
+ "co-stack/fal_sftp": "<0.2.6",
+ "cockpit-hq/cockpit": "<2.11.4",
+ "code16/sharp": "<9.11.1",
+ "codeception/codeception": "<3.1.3|>=4,<4.1.22",
+ "codeigniter/framework": "<3.1.10",
+ "codeigniter4/framework": "<4.6.2",
+ "codeigniter4/shield": "<1.0.0.0-beta8",
+ "codiad/codiad": "<=2.8.4",
+ "codingms/additional-tca": ">=1.7,<1.15.17|>=1.16,<1.16.9",
+ "codingms/modules": "<4.3.11|>=5,<5.7.4|>=6,<6.4.2|>=7,<7.5.5",
+ "commerceteam/commerce": ">=0.9.6,<0.9.9",
+ "components/jquery": ">=1.0.3,<3.5",
+ "composer/composer": "<1.10.27|>=2,<2.2.26|>=2.3,<2.9.3",
+ "concrete5/concrete5": "<9.4.3",
+ "concrete5/core": "<8.5.8|>=9,<9.1",
+ "contao-components/mediaelement": ">=2.14.2,<2.21.1",
+ "contao/comments-bundle": ">=2,<4.13.40|>=5.0.0.0-RC1-dev,<5.3.4",
+ "contao/contao": ">=3,<3.5.37|>=4,<4.4.56|>=4.5,<4.13.56|>=5,<5.3.38|>=5.4.0.0-RC1-dev,<5.6.1",
"contao/core": "<3.5.39",
- "contao/core-bundle": "<4.13.56|>=5,<5.3.38|>=5.4,<5.6.1",
+ "contao/core-bundle": "<4.13.57|>=5,<5.3.42|>=5.4,<5.6.5",
"contao/listing-bundle": ">=3,<=3.5.30|>=4,<4.4.8",
"contao/managed-edition": "<=1.5",
+ "coreshop/core-shop": "<4.1.9",
"corveda/phpsandbox": "<1.3.5",
"cosenary/instagram": "<=2.3",
"couleurcitron/tarteaucitron-wp": "<0.3",
- "craftcms/cms": "<=4.16.5|>=5,<=5.8.6",
- "croogo/croogo": "<4",
+ "cpsit/typo3-mailqueue": "<0.4.3|>=0.5,<0.5.1",
+ "craftcms/cms": "<4.17.0.0-beta1|>=5,<5.9.0.0-beta1",
+ "craftcms/commerce": ">=4.0.0.0-RC1-dev,<=4.10|>=5,<=5.5.1",
+ "craftcms/composer": ">=4.0.0.0-RC1-dev,<=4.10|>=5.0.0.0-RC1-dev,<=5.5.1",
+ "craftcms/craft": ">=3.5,<=4.16.17|>=5.0.0.0-RC1-dev,<=5.8.21",
+ "croogo/croogo": "<=4.0.7",
"cuyz/valinor": "<0.12",
"czim/file-handling": "<1.5|>=2,<2.3",
"czproject/git-php": "<4.0.3",
@@ -3142,9 +5629,11 @@
"derhansen/sf_event_mgt": "<4.3.1|>=5,<5.1.1|>=7,<7.4",
"desperado/xml-bundle": "<=0.1.7",
"dev-lancer/minecraft-motd-parser": "<=1.0.5",
+ "devcode-it/openstamanager": "<=2.9.8",
"devgroup/dotplant": "<2020.09.14-dev",
"digimix/wp-svg-upload": "<=1",
"directmailteam/direct-mail": "<6.0.3|>=7,<7.0.3|>=8,<9.5.2",
+ "directorytree/imapengine": "<1.22.3",
"dl/yag": "<3.0.1",
"dmk/webkitpdf": "<1.1.4",
"dnadesign/silverstripe-elemental": "<5.3.12",
@@ -3160,36 +5649,48 @@
"dolibarr/dolibarr": "<21.0.3",
"dompdf/dompdf": "<2.0.4",
"doublethreedigital/guest-entries": "<3.1.2",
+ "drupal-pattern-lab/unified-twig-extensions": "<=0.1",
+ "drupal/access_code": "<2.0.5",
+ "drupal/acquia_dam": "<1.1.5",
"drupal/admin_audit_trail": "<1.0.5",
"drupal/ai": "<1.0.5",
"drupal/alogin": "<2.0.6",
"drupal/cache_utility": "<1.2.1",
+ "drupal/civictheme": "<1.12",
"drupal/commerce_alphabank_redirect": "<1.0.3",
"drupal/commerce_eurobank_redirect": "<2.1.1",
"drupal/config_split": "<1.10|>=2,<2.0.2",
- "drupal/core": ">=6,<6.38|>=7,<7.102|>=8,<10.3.14|>=10.4,<10.4.5|>=11,<11.0.13|>=11.1,<11.1.5",
+ "drupal/core": ">=6,<6.38|>=7,<7.103|>=8,<10.4.9|>=10.5,<10.5.6|>=11,<11.1.9|>=11.2,<11.2.8",
"drupal/core-recommended": ">=7,<7.102|>=8,<10.2.11|>=10.3,<10.3.9|>=11,<11.0.8",
+ "drupal/currency": "<3.5",
"drupal/drupal": ">=5,<5.11|>=6,<6.38|>=7,<7.102|>=8,<10.2.11|>=10.3,<10.3.9|>=11,<11.0.8",
+ "drupal/email_tfa": "<2.0.6",
"drupal/formatter_suite": "<2.1",
"drupal/gdpr": "<3.0.1|>=3.1,<3.1.2",
"drupal/google_tag": "<1.8|>=2,<2.0.8",
"drupal/ignition": "<1.0.4",
+ "drupal/json_field": "<1.5",
"drupal/lightgallery": "<1.6",
"drupal/link_field_display_mode_formatter": "<1.6",
"drupal/matomo": "<1.24",
"drupal/oauth2_client": "<4.1.3",
"drupal/oauth2_server": "<2.1",
"drupal/obfuscate": "<2.0.1",
+ "drupal/plausible_tracking": "<1.0.2",
"drupal/quick_node_block": "<2",
"drupal/rapidoc_elements_field_formatter": "<1.0.1",
+ "drupal/reverse_proxy_header": "<1.1.2",
+ "drupal/simple_multistep": "<2",
+ "drupal/simple_oauth": ">=6,<6.0.7",
"drupal/spamspan": "<3.2.1",
"drupal/tfa": "<1.10",
+ "drupal/umami_analytics": "<1.0.1",
"duncanmcclean/guest-entries": "<3.1.2",
"dweeves/magmi": "<=0.7.24",
"ec-cube/ec-cube": "<2.4.4|>=2.11,<=2.17.1|>=3,<=3.0.18.0-patch4|>=4,<=4.1.2",
"ecodev/newsletter": "<=4",
"ectouch/ectouch": "<=2.7.2",
- "egroupware/egroupware": "<23.1.20240624",
+ "egroupware/egroupware": "<23.1.20260113|>=26.0.20251208,<26.0.20260113",
"elefant/cms": "<2.0.7",
"elgg/elgg": "<3.3.24|>=4,<4.0.5",
"elijaa/phpmemcacheadmin": "<=1.3",
@@ -3208,33 +5709,34 @@
"ezsystems/ezdemo-ls-extension": ">=5.4,<5.4.2.1-dev",
"ezsystems/ezfind-ls": ">=5.3,<5.3.6.1-dev|>=5.4,<5.4.11.1-dev|>=2017.12,<2017.12.0.1-dev",
"ezsystems/ezplatform": "<=1.13.6|>=2,<=2.5.24",
- "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.6|>=1.5,<1.5.29|>=2.3,<2.3.38|>=3.3,<3.3.39",
+ "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.6|>=1.5,<1.5.29|>=2.3,<2.3.39|>=3.3,<3.3.39",
"ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2.1|>=5,<5.0.1|>=5.1,<5.1.1|>=5.3.0.0-beta1,<5.3.5",
"ezsystems/ezplatform-graphql": ">=1.0.0.0-RC1-dev,<1.0.13|>=2.0.0.0-beta1,<2.3.12",
"ezsystems/ezplatform-http-cache": "<2.3.16",
- "ezsystems/ezplatform-kernel": "<1.2.5.1-dev|>=1.3,<1.3.35",
+ "ezsystems/ezplatform-kernel": "<=1.2.5|>=1.3,<1.3.35",
"ezsystems/ezplatform-rest": ">=1.2,<=1.2.2|>=1.3,<1.3.8",
"ezsystems/ezplatform-richtext": ">=2.3,<2.3.26|>=3.3,<3.3.40",
"ezsystems/ezplatform-solr-search-engine": ">=1.7,<1.7.12|>=2,<2.0.2|>=3.3,<3.3.15",
"ezsystems/ezplatform-user": ">=1,<1.0.1",
- "ezsystems/ezpublish-kernel": "<6.13.8.2-dev|>=7,<7.5.31",
+ "ezsystems/ezpublish-kernel": "<=6.13.8.1|>=7,<7.5.31",
"ezsystems/ezpublish-legacy": "<=2017.12.7.3|>=2018.6,<=2019.03.5.1",
"ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3",
"ezsystems/repository-forms": ">=2.3,<2.3.2.1-dev|>=2.5,<2.5.15",
"ezyang/htmlpurifier": "<=4.2",
"facade/ignition": "<1.16.15|>=2,<2.4.2|>=2.5,<2.5.2",
- "facturascripts/facturascripts": "<=2022.08",
+ "facturascripts/facturascripts": "<2025.81",
"fastly/magento2": "<1.2.26",
"feehi/cms": "<=2.1.1",
"feehi/feehicms": "<=2.1.1",
"fenom/fenom": "<=2.12.1",
"filament/actions": ">=3.2,<3.2.123",
+ "filament/filament": ">=4,<4.3.1",
"filament/infolists": ">=3,<3.2.115",
"filament/tables": ">=3,<3.2.115",
"filegator/filegator": "<7.8",
"filp/whoops": "<2.1.13",
"fineuploader/php-traditional-server": "<=1.2.2",
- "firebase/php-jwt": "<6",
+ "firebase/php-jwt": "<7",
"fisharebest/webtrees": "<=2.1.18",
"fixpunkt/fp-masterquiz": "<2.2.1|>=3,<3.5.2",
"fixpunkt/fp-newsletter": "<1.1.1|>=1.2,<2.1.2|>=2.2,<3.2.6",
@@ -3247,6 +5749,7 @@
"floriangaerber/magnesium": "<0.3.1",
"fluidtypo3/vhs": "<5.1.1",
"fof/byobu": ">=0.3.0.0-beta2,<1.1.7",
+ "fof/pretty-mail": "<=1.1.2",
"fof/upload": "<1.2.3",
"foodcoopshop/foodcoopshop": ">=3.2,<3.6.1",
"fooman/tcpdf": "<6.2.22",
@@ -3262,17 +5765,18 @@
"friendsoftypo3/mediace": ">=7.6.2,<7.6.5",
"friendsoftypo3/openid": ">=4.5,<4.5.31|>=4.7,<4.7.16|>=6,<6.0.11|>=6.1,<6.1.6",
"froala/wysiwyg-editor": "<=4.3",
- "froxlor/froxlor": "<=2.2.5",
+ "frosh/adminer-platform": "<2.2.1",
+ "froxlor/froxlor": "<=2.3.3",
"frozennode/administrator": "<=5.0.12",
"fuel/core": "<1.8.1",
- "funadmin/funadmin": "<=5.0.2",
+ "funadmin/funadmin": "<=7.1.0.0-RC4",
"gaoming13/wechat-php-sdk": "<=1.10.2",
"genix/cms": "<=1.1.11",
"georgringer/news": "<1.3.3",
"geshi/geshi": "<=1.0.9.1",
- "getformwork/formwork": "<1.13.1|>=2.0.0.0-beta1,<2.0.0.0-beta4",
- "getgrav/grav": "<1.7.46",
- "getkirby/cms": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1",
+ "getformwork/formwork": "<=2.3.3",
+ "getgrav/grav": "<1.11.0.0-beta1",
+ "getkirby/cms": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1|>=5,<=5.2.1",
"getkirby/kirby": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1",
"getkirby/panel": "<2.5.14",
"getkirby/starterkit": "<=3.7.0.2",
@@ -3302,17 +5806,17 @@
"hov/jobfair": "<1.0.13|>=2,<2.0.2",
"httpsoft/http-message": "<1.0.12",
"hyn/multi-tenant": ">=5.6,<5.7.2",
- "ibexa/admin-ui": ">=4.2,<4.2.3|>=4.6,<4.6.21",
+ "ibexa/admin-ui": ">=4.2,<4.2.3|>=4.6,<4.6.25|>=5,<5.0.3",
"ibexa/admin-ui-assets": ">=4.6.0.0-alpha1,<4.6.21",
"ibexa/core": ">=4,<4.0.7|>=4.1,<4.1.4|>=4.2,<4.2.3|>=4.5,<4.5.6|>=4.6,<4.6.2",
- "ibexa/fieldtype-richtext": ">=4.6,<4.6.21",
+ "ibexa/fieldtype-richtext": ">=4.6,<4.6.25|>=5,<5.0.3",
"ibexa/graphql": ">=2.5,<2.5.31|>=3.3,<3.3.28|>=4.2,<4.2.3",
"ibexa/http-cache": ">=4.6,<4.6.14",
"ibexa/post-install": "<1.0.16|>=4.6,<4.6.14",
"ibexa/solr": ">=4.5,<4.5.4",
- "ibexa/user": ">=4,<4.4.3",
+ "ibexa/user": ">=4,<4.4.3|>=5,<5.0.4",
"icecoder/icecoder": "<=8.1",
- "idno/known": "<=1.3.1",
+ "idno/known": "<1.6.4",
"ilicmiljan/secure-props": ">=1.2,<1.2.2",
"illuminate/auth": "<5.5.10",
"illuminate/cookie": ">=4,<=4.0.11|>=4.1,<6.18.31|>=7,<7.22.4",
@@ -3362,7 +5866,7 @@
"kelvinmo/simplexrd": "<3.1.1",
"kevinpapst/kimai2": "<1.16.7",
"khodakhah/nodcms": "<=3",
- "kimai/kimai": "<=2.20.1",
+ "kimai/kimai": "<2.46",
"kitodo/presentation": "<3.2.3|>=3.3,<3.3.4",
"klaviyo/magento2-extension": ">=1,<3",
"knplabs/knp-snappy": "<=1.4.2",
@@ -3381,10 +5885,10 @@
"laravel/framework": "<10.48.29|>=11,<11.44.1|>=12,<12.1.1",
"laravel/laravel": ">=5.4,<5.4.22",
"laravel/pulse": "<1.3.1",
- "laravel/reverb": "<1.4",
+ "laravel/reverb": "<1.7",
"laravel/socialite": ">=1,<2.0.10",
"latte/latte": "<2.10.8",
- "lavalite/cms": "<=9|==10.1",
+ "lavalite/cms": "<=10.1",
"lavitto/typo3-form-to-database": "<2.2.5|>=3,<3.2.2|>=4,<4.2.3|>=5,<5.0.2",
"lcobucci/jwt": ">=3.4,<3.4.6|>=4,<4.0.4|>=4.1,<4.1.5",
"league/commonmark": "<2.7",
@@ -3393,11 +5897,12 @@
"leantime/leantime": "<3.3",
"lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3",
"libreform/libreform": ">=2,<=2.0.8",
- "librenms/librenms": "<2017.08.18",
+ "librenms/librenms": "<26.2",
"liftkit/database": "<2.13.2",
"lightsaml/lightsaml": "<1.3.5",
"limesurvey/limesurvey": "<6.5.12",
"livehelperchat/livehelperchat": "<=3.91",
+ "livewire-filemanager/filemanager": "<=1.0.4",
"livewire/livewire": "<2.12.7|>=3.0.0.0-beta1,<3.6.4",
"livewire/volt": "<1.7",
"lms/routes": "<2.1.1",
@@ -3407,7 +5912,7 @@
"luyadev/yii-helpers": "<1.2.1",
"macropay-solutions/laravel-crud-wizard-free": "<3.4.17",
"maestroerror/php-heic-to-jpg": "<1.0.5",
- "magento/community-edition": "<=2.4.5.0-patch14|==2.4.6|>=2.4.6.0-patch1,<=2.4.6.0-patch12|>=2.4.7.0-beta1,<=2.4.7.0-patch7|>=2.4.8.0-beta1,<=2.4.8.0-patch2|>=2.4.9.0-alpha1,<=2.4.9.0-alpha2|==2.4.9",
+ "magento/community-edition": "<2.4.6.0-patch13|>=2.4.7.0-beta1,<2.4.7.0-patch8|>=2.4.8.0-beta1,<2.4.8.0-patch3|>=2.4.9.0-alpha1,<2.4.9.0-alpha3|==2.4.9",
"magento/core": "<=1.9.4.5",
"magento/magento1ce": "<1.9.4.3-dev",
"magento/magento1ee": ">=1,<1.14.4.3-dev",
@@ -3418,49 +5923,53 @@
"maikuolan/phpmussel": ">=1,<1.6",
"mainwp/mainwp": "<=4.4.3.3",
"manogi/nova-tiptap": "<=3.2.6",
- "mantisbt/mantisbt": "<=2.26.3",
+ "mantisbt/mantisbt": "<2.27.2",
"marcwillmann/turn": "<0.3.3",
"marshmallow/nova-tiptap": "<5.7",
"matomo/matomo": "<1.11",
"matyhtf/framework": "<3.0.6",
- "mautic/core": "<5.2.8|>=6.0.0.0-alpha,<6.0.5",
+ "mautic/core": "<5.2.10|>=6,<6.0.8|>=7.0.0.0-alpha,<7.0.1",
"mautic/core-lib": ">=1.0.0.0-beta,<4.4.13|>=5.0.0.0-alpha,<5.1.1",
+ "mautic/grapes-js-builder-bundle": ">=4,<4.4.18|>=5,<5.2.9|>=6,<6.0.7",
"maximebf/debugbar": "<1.19",
"mdanter/ecc": "<2",
"mediawiki/abuse-filter": "<1.39.9|>=1.40,<1.41.3|>=1.42,<1.42.2",
- "mediawiki/cargo": "<3.6.1",
+ "mediawiki/cargo": "<3.8.3",
"mediawiki/core": "<1.39.5|==1.40",
"mediawiki/data-transfer": ">=1.39,<1.39.11|>=1.41,<1.41.3|>=1.42,<1.42.2",
"mediawiki/matomo": "<2.4.3",
"mediawiki/semantic-media-wiki": "<4.0.2",
"mehrwert/phpmyadmin": "<3.2",
"melisplatform/melis-asset-manager": "<5.0.1",
- "melisplatform/melis-cms": "<5.0.1",
+ "melisplatform/melis-cms": "<5.3.4",
+ "melisplatform/melis-cms-slider": "<5.3.1",
+ "melisplatform/melis-core": "<5.3.11",
"melisplatform/melis-front": "<5.0.1",
"mezzio/mezzio-swoole": "<3.7|>=4,<4.3",
"mgallegos/laravel-jqgrid": "<=1.3",
"microsoft/microsoft-graph": ">=1.16,<1.109.1|>=2,<2.0.1",
"microsoft/microsoft-graph-beta": "<2.0.1",
"microsoft/microsoft-graph-core": "<2.0.2",
- "microweber/microweber": "<=2.0.19",
+ "microweber/microweber": "<2.0.20",
"mikehaertl/php-shellcommand": "<1.6.1",
+ "mineadmin/mineadmin": "<=3.0.9",
"miniorange/miniorange-saml": "<1.4.3",
"mittwald/typo3_forum": "<1.2.1",
"mobiledetect/mobiledetectlib": "<2.8.32",
"modx/revolution": "<=3.1",
"mojo42/jirafeau": "<4.4",
"mongodb/mongodb": ">=1,<1.9.2",
+ "mongodb/mongodb-extension": "<1.21.2",
"monolog/monolog": ">=1.8,<1.12",
- "moodle/moodle": "<4.3.12|>=4.4,<4.4.8|>=4.5.0.0-beta,<4.5.4",
+ "moodle/moodle": "<4.5.9|>=5.0.0.0-beta,<5.0.5|>=5.1.0.0-beta,<5.1.2",
"moonshine/moonshine": "<=3.12.5",
"mos/cimage": "<0.7.19",
"movim/moxl": ">=0.8,<=0.10",
"movingbytes/social-network": "<=1.2.1",
"mpdf/mpdf": "<=7.1.7",
- "munkireport/comment": "<4.1",
+ "munkireport/comment": "<4",
"munkireport/managedinstalls": "<2.6",
"munkireport/munki_facts": "<1.5",
- "munkireport/munkireport": ">=2.5.3,<5.6.3",
"munkireport/reportdata": "<3.5",
"munkireport/softwareupdate": "<1.6",
"mustache/mustache": ">=2,<2.14.1",
@@ -3480,13 +5989,14 @@
"netgen/tagsbundle": ">=3.4,<3.4.11|>=4,<4.0.15",
"nette/application": ">=2,<2.0.19|>=2.1,<2.1.13|>=2.2,<2.2.10|>=2.3,<2.3.14|>=2.4,<2.4.16|>=3,<3.0.6",
"nette/nette": ">=2,<2.0.19|>=2.1,<2.1.13",
+ "neuron-core/neuron-ai": "<=2.8.11",
"nilsteampassnet/teampass": "<3.1.3.1-dev",
"nitsan/ns-backup": "<13.0.1",
"nonfiction/nterchange": "<4.1.1",
"notrinos/notrinos-erp": "<=0.7",
"noumo/easyii": "<=0.9",
"novaksolutions/infusionsoft-php-sdk": "<1",
- "novosga/novosga": "<=2.2.9",
+ "novosga/novosga": "<=2.2.12",
"nukeviet/nukeviet": "<4.5.02",
"nyholm/psr7": "<1.6.1",
"nystudio107/craft-seomatic": "<3.4.12",
@@ -3496,15 +6006,15 @@
"october/cms": "<1.0.469|==1.0.469|==1.0.471|==1.1.1",
"october/october": "<3.7.5",
"october/rain": "<1.0.472|>=1.1,<1.1.2",
- "october/system": "<3.7.5",
+ "october/system": "<=3.7.12|>=4,<=4.0.11",
"oliverklee/phpunit": "<3.5.15",
"omeka/omeka-s": "<4.0.3",
- "onelogin/php-saml": "<2.10.4",
+ "onelogin/php-saml": "<2.21.1|>=3,<3.8.1|>=4,<4.3.1",
"oneup/uploader-bundle": ">=1,<1.9.3|>=2,<2.1.5",
- "open-web-analytics/open-web-analytics": "<1.7.4",
+ "open-web-analytics/open-web-analytics": "<1.8.1",
"opencart/opencart": ">=0",
"openid/php-openid": "<2.3",
- "openmage/magento-lts": "<20.12.3",
+ "openmage/magento-lts": "<20.16.1",
"opensolutions/vimbadmin": "<=3.0.15",
"opensource-workshop/connect-cms": "<1.8.7|>=2,<2.4.7",
"orchid/platform": ">=8,<14.43",
@@ -3523,6 +6033,7 @@
"pagekit/pagekit": "<=1.0.18",
"paragonie/ecc": "<2.0.1",
"paragonie/random_compat": "<2",
+ "paragonie/sodium_compat": "<1.24|>=2,<2.5",
"passbolt/passbolt_api": "<4.6.2",
"paypal/adaptivepayments-sdk-php": "<=3.9.2",
"paypal/invoice-sdk-php": "<=3.9",
@@ -3535,6 +6046,7 @@
"pear/pear": "<=1.10.1",
"pegasus/google-for-jobs": "<1.5.1|>=2,<2.1.1",
"personnummer/personnummer": "<3.0.2",
+ "ph7software/ph7builder": "<=17.9.1",
"phanan/koel": "<5.1.4",
"phenx/php-svg-lib": "<0.5.2",
"php-censor/php-censor": "<2.0.13|>=2.1,<2.1.5",
@@ -3545,27 +6057,30 @@
"phpmailer/phpmailer": "<6.5",
"phpmussel/phpmussel": ">=1,<1.6",
"phpmyadmin/phpmyadmin": "<5.2.2",
- "phpmyfaq/phpmyfaq": "<3.2.5|==3.2.5|>=3.2.10,<=4.0.1",
+ "phpmyfaq/phpmyfaq": "<=4.0.16",
"phpoffice/common": "<0.2.9",
"phpoffice/math": "<=0.2",
"phpoffice/phpexcel": "<=1.8.2",
"phpoffice/phpspreadsheet": "<1.30|>=2,<2.1.12|>=2.2,<2.4|>=3,<3.10|>=4,<5",
+ "phppgadmin/phppgadmin": "<=7.13",
"phpseclib/phpseclib": "<2.0.47|>=3,<3.0.36",
"phpservermon/phpservermon": "<3.6",
"phpsysinfo/phpsysinfo": "<3.4.3",
- "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3",
+ "phpunit/phpunit": "<8.5.52|>=9,<9.6.33|>=10,<10.5.62|>=11,<11.5.50|>=12,<12.5.8",
"phpwhois/phpwhois": "<=4.2.5",
"phpxmlrpc/extras": "<0.6.1",
"phpxmlrpc/phpxmlrpc": "<4.9.2",
+ "phraseanet/phraseanet": "==4.0.3",
"pi/pi": "<=2.5",
- "pimcore/admin-ui-classic-bundle": "<1.7.6",
+ "pimcore/admin-ui-classic-bundle": "<=1.7.15|>=2.0.0.0-RC1-dev,<=2.2.2",
"pimcore/customer-management-framework-bundle": "<4.2.1",
"pimcore/data-hub": "<1.2.4",
"pimcore/data-importer": "<1.8.9|>=1.9,<1.9.3",
"pimcore/demo": "<10.3",
"pimcore/ecommerce-framework-bundle": "<1.0.10",
"pimcore/perspective-editor": "<1.5.1",
- "pimcore/pimcore": "<11.5.4",
+ "pimcore/pimcore": "<=11.5.14.1|>=12,<12.3.3",
+ "pimcore/web2print-tools-bundle": "<=5.2.1|>=6.0.0.0-RC1-dev,<=6.1",
"piwik/piwik": "<1.11",
"pixelfed/pixelfed": "<0.12.5",
"plotly/plotly.js": "<2.25.2",
@@ -3578,17 +6093,19 @@
"prestashop/blockwishlist": ">=2,<2.1.1",
"prestashop/contactform": ">=1.0.1,<4.3",
"prestashop/gamification": "<2.3.2",
- "prestashop/prestashop": "<8.2.3",
+ "prestashop/prestashop": "<8.2.4|>=9.0.0.0-alpha1,<9.0.3",
"prestashop/productcomments": "<5.0.2",
+ "prestashop/ps_checkout": "<4.4.1|>=5,<5.0.5",
"prestashop/ps_contactinfo": "<=3.3.2",
"prestashop/ps_emailsubscription": "<2.6.1",
"prestashop/ps_facetedsearch": "<3.4.1",
"prestashop/ps_linklist": "<3.1",
- "privatebin/privatebin": "<1.4|>=1.5,<1.7.4",
- "processwire/processwire": "<=3.0.229",
+ "privatebin/privatebin": "<1.4|>=1.5,<1.7.4|>=1.7.7,<2.0.3",
+ "processwire/processwire": "<=3.0.246",
"propel/propel": ">=2.0.0.0-alpha1,<=2.0.0.0-alpha7",
"propel/propel1": ">=1,<=1.7.1",
- "pterodactyl/panel": "<=1.11.10",
+ "psy/psysh": "<=0.11.22|>=0.12,<=0.12.18",
+ "pterodactyl/panel": "<1.12.1",
"ptheofan/yii2-statemachine": ">=2.0.0.0-RC1-dev,<=2",
"ptrofimov/beanstalk_console": "<1.7.14",
"pubnub/pubnub": "<6.1",
@@ -3606,13 +6123,13 @@
"rap2hpoutre/laravel-log-viewer": "<0.13",
"react/http": ">=0.7,<1.9",
"really-simple-plugins/complianz-gdpr": "<6.4.2",
- "redaxo/source": "<5.18.3",
+ "redaxo/source": "<=5.20.1",
"remdex/livehelperchat": "<4.29",
"renolit/reint-downloadmanager": "<4.0.2|>=5,<5.0.1",
"reportico-web/reportico": "<=8.1",
"rhukster/dom-sanitizer": "<1.0.7",
"rmccue/requests": ">=1.6,<1.8",
- "robrichards/xmlseclibs": ">=1,<3.0.4",
+ "robrichards/xmlseclibs": "<=3.1.3",
"roots/soil": "<4.1",
"roundcube/roundcubemail": "<1.5.10|>=1.6,<1.6.11",
"rudloff/alltube": "<3.0.3",
@@ -3628,11 +6145,11 @@
"setasign/fpdi": "<2.6.4",
"sfroemken/url_redirect": "<=1.2.1",
"sheng/yiicms": "<1.2.1",
- "shopware/core": "<6.5.8.18-dev|>=6.6,<6.6.10.3-dev|>=6.7,<6.7.2.1-dev",
- "shopware/platform": "<=6.6.10.4|>=6.7.0.0-RC1-dev,<6.7.0.0-RC2-dev",
+ "shopware/core": "<6.6.10.9-dev|>=6.7,<6.7.6.1-dev",
+ "shopware/platform": "<6.6.10.7-dev|>=6.7,<6.7.3.1-dev",
"shopware/production": "<=6.3.5.2",
- "shopware/shopware": "<=5.7.17|>=6.7,<6.7.2.1-dev",
- "shopware/storefront": "<=6.4.8.1|>=6.5.8,<6.5.8.7-dev",
+ "shopware/shopware": "<=5.7.17|>=6.4.6,<6.6.10.10-dev|>=6.7,<6.7.6.1-dev",
+ "shopware/storefront": "<6.6.10.10-dev|>=6.7,<6.7.5.1-dev",
"shopxo/shopxo": "<=6.4",
"showdoc/showdoc": "<2.10.4",
"shuchkin/simplexlsx": ">=1.0.12,<1.1.13",
@@ -3673,24 +6190,25 @@
"slim/slim": "<2.6",
"slub/slub-events": "<3.0.3",
"smarty/smarty": "<4.5.3|>=5,<5.1.1",
- "snipe/snipe-it": "<8.1.18",
+ "snipe/snipe-it": "<=8.3.4",
"socalnick/scn-social-auth": "<1.15.2",
"socialiteproviders/steam": "<1.1",
- "solspace/craft-freeform": ">=5,<5.10.16",
+ "solspace/craft-freeform": "<4.1.29|>=5,<=5.14.6",
"soosyze/soosyze": "<=2",
"spatie/browsershot": "<5.0.5",
"spatie/image-optimizer": "<1.7.3",
"spencer14420/sp-php-email-handler": "<1",
"spipu/html2pdf": "<5.2.8",
+ "spiral/roadrunner": "<2025.1",
"spoon/library": "<1.4.1",
"spoonity/tcpdf": "<6.2.22",
"squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1",
"ssddanbrown/bookstack": "<24.05.1",
- "starcitizentools/citizen-skin": ">=1.9.4,<3.4",
+ "starcitizentools/citizen-skin": ">=1.9.4,<3.9",
"starcitizentools/short-description": ">=4,<4.0.1",
"starcitizentools/tabber-neue": ">=1.9.1,<2.7.2|>=3,<3.1.1",
"starcitizenwiki/embedvideo": "<=4",
- "statamic/cms": "<=5.16",
+ "statamic/cms": "<5.73.11|>=6,<6.4",
"stormpath/sdk": "<9.9.99",
"studio-42/elfinder": "<=2.1.64",
"studiomitte/friendlycaptcha": "<0.1.4",
@@ -3721,7 +6239,7 @@
"symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1",
"symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7|>=5.3.14,<5.3.15|>=5.4.3,<5.4.4|>=6.0.3,<6.0.4",
"symfony/http-client": ">=4.3,<5.4.47|>=6,<6.4.15|>=7,<7.1.8",
- "symfony/http-foundation": "<5.4.46|>=6,<6.4.14|>=7,<7.1.7",
+ "symfony/http-foundation": "<5.4.50|>=6,<6.4.29|>=7,<7.3.7",
"symfony/http-kernel": ">=2,<4.4.50|>=5,<5.4.20|>=6,<6.0.20|>=6.1,<6.1.12|>=6.2,<6.2.6",
"symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13",
"symfony/maker-bundle": ">=1.27,<1.29.2|>=1.30,<1.31.1",
@@ -3729,7 +6247,7 @@
"symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
"symfony/polyfill": ">=1,<1.10",
"symfony/polyfill-php55": ">=1,<1.10",
- "symfony/process": "<5.4.46|>=6,<6.4.14|>=7,<7.1.7",
+ "symfony/process": "<5.4.51|>=6,<6.4.33|>=7,<7.1.7|>=7.3,<7.3.11|>=7.4,<7.4.5|>=8,<8.0.5",
"symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
"symfony/routing": ">=2,<2.0.19",
"symfony/runtime": ">=5.3,<5.4.46|>=6,<6.4.14|>=7,<7.1.7",
@@ -3740,7 +6258,7 @@
"symfony/security-guard": ">=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8",
"symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7|>=5.1,<5.2.8|>=5.3,<5.4.47|>=6,<6.4.15|>=7,<7.1.8",
"symfony/serializer": ">=2,<2.0.11|>=4.1,<4.4.35|>=5,<5.3.12",
- "symfony/symfony": "<5.4.47|>=6,<6.4.15|>=7,<7.1.8",
+ "symfony/symfony": "<5.4.51|>=6,<6.4.33|>=7,<7.3.11|>=7.4,<7.4.5|>=8,<8.0.5",
"symfony/translation": ">=2,<2.0.17",
"symfony/twig-bridge": ">=2,<4.4.51|>=5,<5.4.31|>=6,<6.3.8",
"symfony/ux-autocomplete": "<2.11.2",
@@ -3764,7 +6282,7 @@
"thelia/thelia": ">=2.1,<2.1.3",
"theonedemon/phpwhois": "<=4.2.5",
"thinkcmf/thinkcmf": "<6.0.8",
- "thorsten/phpmyfaq": "<=4.0.1",
+ "thorsten/phpmyfaq": "<4.0.18|>=4.1.0.0-alpha,<=4.1.0.0-beta2",
"tikiwiki/tiki-manager": "<=17.1",
"timber/timber": ">=0.16.6,<1.23.1|>=1.24,<1.24.1|>=2,<2.1",
"tinymce/tinymce": "<7.2",
@@ -3775,18 +6293,19 @@
"topthink/framework": "<6.0.17|>=6.1,<=8.0.4",
"topthink/think": "<=6.1.1",
"topthink/thinkphp": "<=3.2.3|>=6.1.3,<=8.0.4",
- "torrentpier/torrentpier": "<=2.4.3",
+ "torrentpier/torrentpier": "<=2.8.8",
"tpwd/ke_search": "<4.0.3|>=4.1,<4.6.6|>=5,<5.0.2",
"tribalsystems/zenario": "<=9.7.61188",
"truckersmp/phpwhois": "<=4.3.1",
"ttskch/pagination-service-provider": "<1",
- "twbs/bootstrap": "<3.4.1|>=4,<=4.6.2",
+ "twbs/bootstrap": "<3.4.1|>=4,<4.3.1",
"twig/twig": "<3.11.2|>=3.12,<3.14.1|>=3.16,<3.19",
+ "typicms/core": "<16.1.7",
"typo3/cms": "<9.5.29|>=10,<10.4.35|>=11,<11.5.23|>=12,<12.2",
- "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18",
+ "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1",
"typo3/cms-belog": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2",
"typo3/cms-beuser": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18",
- "typo3/cms-core": "<=8.7.56|>=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18",
+ "typo3/cms-core": "<=8.7.56|>=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1",
"typo3/cms-dashboard": ">=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18",
"typo3/cms-extbase": "<6.2.24|>=7,<7.6.8|==8.1.1",
"typo3/cms-extensionmanager": ">=10,<=10.4.47|>=11,<=11.5.41|>=12,<=12.4.24|>=13,<=13.4.2",
@@ -3798,7 +6317,8 @@
"typo3/cms-install": "<4.1.14|>=4.2,<4.2.16|>=4.3,<4.3.9|>=4.4,<4.4.5|>=12.2,<12.4.8|==13.4.2",
"typo3/cms-lowlevel": ">=11,<=11.5.41",
"typo3/cms-recordlist": ">=11,<11.5.48",
- "typo3/cms-recycler": ">=9,<9.5.55|>=10,<10.4.54|>=11,<11.5.48|>=12,<12.4.37|>=13,<13.4.18",
+ "typo3/cms-recycler": ">=9,<9.5.55|>=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1",
+ "typo3/cms-redirects": ">=10,<=10.4.54|>=11,<=11.5.48|>=12,<=12.4.40|>=13,<=13.4.22|>=14,<=14.0.1",
"typo3/cms-rte-ckeditor": ">=9.5,<9.5.42|>=10,<10.4.39|>=11,<11.5.30",
"typo3/cms-scheduler": ">=11,<=11.5.41",
"typo3/cms-setup": ">=9,<=9.5.50|>=10,<=10.4.49|>=11,<=11.5.43|>=12,<=12.4.30|>=13,<=13.4.11",
@@ -3828,7 +6348,7 @@
"vertexvaar/falsftp": "<0.2.6",
"villagedefrance/opencart-overclocked": "<=1.11.1",
"vova07/yii2-fileapi-widget": "<0.1.9",
- "vrana/adminer": "<=4.8.1",
+ "vrana/adminer": "<5.4.2",
"vufind/vufind": ">=2,<9.1.1",
"waldhacker/hcaptcha": "<2.1.2",
"wallabag/tcpdf": "<6.2.22",
@@ -3844,11 +6364,12 @@
"webklex/laravel-imap": "<5.3",
"webklex/php-imap": "<5.3",
"webpa/webpa": "<3.1.2",
+ "webreinvent/vaahcms": "<=2.3.1",
"wikibase/wikibase": "<=1.39.3",
"wikimedia/parsoid": "<0.12.2",
"willdurand/js-translation-bundle": "<2.1.1",
"winter/wn-backend-module": "<1.2.4",
- "winter/wn-cms-module": "<1.0.476|>=1.1,<1.1.11|>=1.2,<1.2.7",
+ "winter/wn-cms-module": "<=1.2.9",
"winter/wn-dusk-plugin": "<2.1",
"winter/wn-system-module": "<1.2.4",
"wintercms/winter": "<=1.2.3",
@@ -3860,7 +6381,7 @@
"wpanel/wpanel4-cms": "<=4.3.1",
"wpcloud/wp-stateless": "<3.2",
"wpglobus/wpglobus": "<=1.9.6",
- "wwbn/avideo": "<14.3",
+ "wwbn/avideo": "<=21",
"xataface/xataface": "<3",
"xpressengine/xpressengine": "<3.0.15",
"yab/quarx": "<2.4.5",
@@ -3879,8 +6400,9 @@
"yiisoft/yii2-redis": "<2.0.20",
"yikesinc/yikes-inc-easy-mailchimp-extender": "<6.8.6",
"yoast-seo-for-typo3/yoast_seo": "<7.2.3",
- "yourls/yourls": "<=1.8.2",
+ "yourls/yourls": "<=1.10.2",
"yuan1994/tpadmin": "<=1.3.12",
+ "yungifez/skuul": "<=2.6.5",
"z-push/z-push-dev": "<2.7.6",
"zencart/zencart": "<=1.5.7.0-beta",
"zendesk/zendesk_api_client_php": "<2.2.11",
@@ -3918,58 +6440,903 @@
"zf-commons/zfc-user": "<1.2.2",
"zfcampus/zf-apigility-doctrine": ">=1,<1.0.3",
"zfr/zfr-oauth2-server-module": "<0.1.2",
- "zoujingli/thinkadmin": "<=6.1.53"
+ "zoujingli/thinkadmin": "<=6.1.53",
+ "zumba/json-serializer": "<3.2.3"
+ },
+ "default-branch": true,
+ "type": "metapackage",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com",
+ "role": "maintainer"
+ },
+ {
+ "name": "Ilya Tribusean",
+ "email": "slash3b@gmail.com",
+ "role": "maintainer"
+ }
+ ],
+ "description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it",
+ "keywords": [
+ "dev"
+ ],
+ "support": {
+ "issues": "https://github.com/Roave/SecurityAdvisories/issues",
+ "source": "https://github.com/Roave/SecurityAdvisories/tree/latest"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Ocramius",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/roave/security-advisories",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-03-03T18:18:18+00:00"
+ },
+ {
+ "name": "sebastian/cli-parser",
+ "version": "2.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/cli-parser.git",
+ "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084",
+ "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for parsing CLI options",
+ "homepage": "https://github.com/sebastianbergmann/cli-parser",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
+ "security": "https://github.com/sebastianbergmann/cli-parser/security/policy",
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T07:12:49+00:00"
+ },
+ {
+ "name": "sebastian/code-unit",
+ "version": "2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit.git",
+ "reference": "a81fee9eef0b7a76af11d121767abc44c104e503"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503",
+ "reference": "a81fee9eef0b7a76af11d121767abc44c104e503",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/code-unit",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:58:43+00:00"
+ },
+ {
+ "name": "sebastian/code-unit-reverse-lookup",
+ "version": "3.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+ "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d",
+ "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Looks up which function or method a line of code belongs to",
+ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T06:59:15+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "5.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d",
+ "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-mbstring": "*",
+ "php": ">=8.1",
+ "sebastian/diff": "^5.0",
+ "sebastian/exporter": "^5.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "https://github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/comparator/issues",
+ "security": "https://github.com/sebastianbergmann/comparator/security/policy",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-24T09:25:16+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "3.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git",
+ "reference": "68ff824baeae169ec9f2137158ee529584553799"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799",
+ "reference": "68ff824baeae169ec9f2137158ee529584553799",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for calculating the complexity of PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/complexity",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/complexity/issues",
+ "security": "https://github.com/sebastianbergmann/complexity/security/policy",
+ "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-21T08:37:17+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "5.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e",
+ "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0",
+ "symfony/process": "^6.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues",
+ "security": "https://github.com/sebastianbergmann/diff/security/policy",
+ "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T07:15:17+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "6.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "8074dbcd93529b357029f5cc5058fd3e43666984"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984",
+ "reference": "8074dbcd93529b357029f5cc5058fd3e43666984",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "suggest": {
+ "ext-posix": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "https://github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/environment/issues",
+ "security": "https://github.com/sebastianbergmann/environment/security/policy",
+ "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-23T08:47:14+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "5.1.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "0735b90f4da94969541dac1da743446e276defa6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6",
+ "reference": "0735b90f4da94969541dac1da743446e276defa6",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": ">=8.1",
+ "sebastian/recursion-context": "^5.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "https://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/exporter/issues",
+ "security": "https://github.com/sebastianbergmann/exporter/security/policy",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-09-24T06:09:11+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "6.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9",
+ "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "sebastian/object-reflector": "^3.0",
+ "sebastian/recursion-context": "^5.0"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "https://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/global-state/issues",
+ "security": "https://github.com/sebastianbergmann/global-state/security/policy",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T07:19:19+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0",
+ "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for counting the lines of code in PHP source code",
+ "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
+ "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy",
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-12-21T08:38:20+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "5.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906",
+ "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "sebastian/object-reflector": "^3.0",
+ "sebastian/recursion-context": "^5.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T07:08:32+00:00"
+ },
+ {
+ "name": "sebastian/object-reflector",
+ "version": "3.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-reflector.git",
+ "reference": "24ed13d98130f0e7122df55d06c5c4942a577957"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957",
+ "reference": "24ed13d98130f0e7122df55d06c5c4942a577957",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Allows reflection of object attributes, including inherited and non-public ones",
+ "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2023-02-03T07:06:18+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "5.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a",
+ "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
},
- "default-branch": true,
- "type": "metapackage",
"notification-url": "https://packagist.org/downloads/",
"license": [
- "MIT"
+ "BSD-3-Clause"
],
"authors": [
{
- "name": "Marco Pivetta",
- "email": "ocramius@gmail.com",
- "role": "maintainer"
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
},
{
- "name": "Ilya Tribusean",
- "email": "slash3b@gmail.com",
- "role": "maintainer"
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
}
],
- "description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it",
- "keywords": [
- "dev"
- ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "https://github.com/sebastianbergmann/recursion-context",
"support": {
- "issues": "https://github.com/Roave/SecurityAdvisories/issues",
- "source": "https://github.com/Roave/SecurityAdvisories/tree/latest"
+ "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+ "security": "https://github.com/sebastianbergmann/recursion-context/security/policy",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1"
},
"funding": [
{
- "url": "https://github.com/Ocramius",
+ "url": "https://github.com/sebastianbergmann",
"type": "github"
},
{
- "url": "https://tidelift.com/funding/github/packagist/roave/security-advisories",
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context",
"type": "tidelift"
}
],
- "time": "2025-10-01T22:05:21+00:00"
+ "time": "2025-08-10T07:50:56+00:00"
},
{
- "name": "sebastian/cli-parser",
- "version": "2.0.1",
+ "name": "sebastian/type",
+ "version": "4.0.0",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/cli-parser.git",
- "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084"
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "462699a16464c3944eefc02ebdd77882bd3925bf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084",
- "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf",
+ "reference": "462699a16464c3944eefc02ebdd77882bd3925bf",
"shasum": ""
},
"require": {
@@ -3981,7 +7348,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "2.0-dev"
+ "dev-main": "4.0-dev"
}
},
"autoload": {
@@ -4000,12 +7367,11 @@
"role": "lead"
}
],
- "description": "Library for parsing CLI options",
- "homepage": "https://github.com/sebastianbergmann/cli-parser",
+ "description": "Collection of value objects that represent the types of the PHP type system",
+ "homepage": "https://github.com/sebastianbergmann/type",
"support": {
- "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
- "security": "https://github.com/sebastianbergmann/cli-parser/security/policy",
- "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1"
+ "issues": "https://github.com/sebastianbergmann/type/issues",
+ "source": "https://github.com/sebastianbergmann/type/tree/4.0.0"
},
"funding": [
{
@@ -4013,32 +7379,29 @@
"type": "github"
}
],
- "time": "2024-03-02T07:12:49+00:00"
+ "time": "2023-02-03T07:10:45+00:00"
},
{
- "name": "sebastian/code-unit",
- "version": "2.0.0",
+ "name": "sebastian/version",
+ "version": "4.0.1",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/code-unit.git",
- "reference": "a81fee9eef0b7a76af11d121767abc44c104e503"
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503",
- "reference": "a81fee9eef0b7a76af11d121767abc44c104e503",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17",
+ "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
- "require-dev": {
- "phpunit/phpunit": "^10.0"
- },
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "2.0-dev"
+ "dev-main": "4.0-dev"
}
},
"autoload": {
@@ -4057,11 +7420,11 @@
"role": "lead"
}
],
- "description": "Collection of value objects that represent the PHP code units",
- "homepage": "https://github.com/sebastianbergmann/code-unit",
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
"support": {
- "issues": "https://github.com/sebastianbergmann/code-unit/issues",
- "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0"
+ "issues": "https://github.com/sebastianbergmann/version/issues",
+ "source": "https://github.com/sebastianbergmann/version/tree/4.0.1"
},
"funding": [
{
@@ -4069,765 +7432,1049 @@
"type": "github"
}
],
- "time": "2023-02-03T06:58:43+00:00"
+ "time": "2023-02-07T11:34:05+00:00"
},
{
- "name": "sebastian/code-unit-reverse-lookup",
- "version": "3.0.0",
+ "name": "spatie/array-to-xml",
+ "version": "3.4.4",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
- "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d"
+ "url": "https://github.com/spatie/array-to-xml.git",
+ "reference": "88b2f3852a922dd73177a68938f8eb2ec70c7224"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d",
- "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d",
+ "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/88b2f3852a922dd73177a68938f8eb2ec70c7224",
+ "reference": "88b2f3852a922dd73177a68938f8eb2ec70c7224",
"shasum": ""
},
"require": {
- "php": ">=8.1"
+ "ext-dom": "*",
+ "php": "^8.0"
},
"require-dev": {
- "phpunit/phpunit": "^10.0"
+ "mockery/mockery": "^1.2",
+ "pestphp/pest": "^1.21",
+ "spatie/pest-plugin-snapshots": "^1.1"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "3.0-dev"
+ "dev-main": "3.x-dev"
}
},
"autoload": {
- "classmap": [
- "src/"
+ "psr-4": {
+ "Spatie\\ArrayToXml\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Freek Van der Herten",
+ "email": "freek@spatie.be",
+ "homepage": "https://freek.dev",
+ "role": "Developer"
+ }
+ ],
+ "description": "Convert an array to xml",
+ "homepage": "https://github.com/spatie/array-to-xml",
+ "keywords": [
+ "array",
+ "convert",
+ "xml"
+ ],
+ "support": {
+ "source": "https://github.com/spatie/array-to-xml/tree/3.4.4"
+ },
+ "funding": [
+ {
+ "url": "https://spatie.be/open-source/support-us",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/spatie",
+ "type": "github"
+ }
+ ],
+ "time": "2025-12-15T09:00:41+00:00"
+ },
+ {
+ "name": "squizlabs/php_codesniffer",
+ "version": "3.13.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
+ "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4",
+ "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4",
+ "shasum": ""
+ },
+ "require": {
+ "ext-simplexml": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4"
+ },
+ "bin": [
+ "bin/phpcbf",
+ "bin/phpcs"
+ ],
+ "type": "library",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Greg Sherwood",
+ "role": "Former lead"
+ },
+ {
+ "name": "Juliette Reinders Folmer",
+ "role": "Current lead"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors"
+ }
+ ],
+ "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
+ "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
+ "keywords": [
+ "phpcs",
+ "standards",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues",
+ "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy",
+ "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
+ "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/PHPCSStandards",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/jrfnl",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/php_codesniffer",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/phpcsstandards",
+ "type": "thanks_dev"
+ }
+ ],
+ "time": "2025-11-04T16:30:35+00:00"
+ },
+ {
+ "name": "symfony/config",
+ "version": "v6.4.34",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/config.git",
+ "reference": "ce9cb0c0d281aaf188b802d4968e42bfb60701e9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/config/zipball/ce9cb0c0d281aaf188b802d4968e42bfb60701e9",
+ "reference": "ce9cb0c0d281aaf188b802d4968e42bfb60701e9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/filesystem": "^5.4|^6.0|^7.0",
+ "symfony/polyfill-ctype": "~1.8"
+ },
+ "conflict": {
+ "symfony/finder": "<5.4",
+ "symfony/service-contracts": "<2.5"
+ },
+ "require-dev": {
+ "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
+ "symfony/finder": "^5.4|^6.0|^7.0",
+ "symfony/messenger": "^5.4|^6.0|^7.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/yaml": "^5.4|^6.0|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Config\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
}
],
- "description": "Looks up which function or method a line of code belongs to",
- "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
+ "homepage": "https://symfony.com",
"support": {
- "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
- "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0"
+ "source": "https://github.com/symfony/config/tree/v6.4.34"
},
"funding": [
{
- "url": "https://github.com/sebastianbergmann",
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
"type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
}
],
- "time": "2023-02-03T06:59:15+00:00"
+ "time": "2026-02-24T17:34:50+00:00"
},
{
- "name": "sebastian/comparator",
- "version": "5.0.4",
+ "name": "symfony/console",
+ "version": "v6.4.34",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e"
+ "url": "https://github.com/symfony/console.git",
+ "reference": "7b1f1c37eff5910ddda2831345467e593a5120ad"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e",
- "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e",
+ "url": "https://api.github.com/repos/symfony/console/zipball/7b1f1c37eff5910ddda2831345467e593a5120ad",
+ "reference": "7b1f1c37eff5910ddda2831345467e593a5120ad",
"shasum": ""
},
"require": {
- "ext-dom": "*",
- "ext-mbstring": "*",
"php": ">=8.1",
- "sebastian/diff": "^5.0",
- "sebastian/exporter": "^5.0"
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/string": "^5.4|^6.0|^7.0"
+ },
+ "conflict": {
+ "symfony/dependency-injection": "<5.4",
+ "symfony/dotenv": "<5.4",
+ "symfony/event-dispatcher": "<5.4",
+ "symfony/lock": "<5.4",
+ "symfony/process": "<5.4"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0|2.0|3.0"
},
"require-dev": {
- "phpunit/phpunit": "^10.5"
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^5.4|^6.0|^7.0",
+ "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+ "symfony/event-dispatcher": "^5.4|^6.0|^7.0",
+ "symfony/http-foundation": "^6.4|^7.0",
+ "symfony/http-kernel": "^6.4|^7.0",
+ "symfony/lock": "^5.4|^6.0|^7.0",
+ "symfony/messenger": "^5.4|^6.0|^7.0",
+ "symfony/process": "^5.4|^6.0|^7.0",
+ "symfony/stopwatch": "^5.4|^6.0|^7.0",
+ "symfony/var-dumper": "^5.4|^6.0|^7.0"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-main": "5.0-dev"
- }
- },
"autoload": {
- "classmap": [
- "src/"
+ "psr-4": {
+ "Symfony\\Component\\Console\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
- },
- {
- "name": "Jeff Welch",
- "email": "whatthejeff@gmail.com"
- },
- {
- "name": "Volker Dusch",
- "email": "github@wallbash.com"
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
},
{
- "name": "Bernhard Schussek",
- "email": "bschussek@2bepublished.at"
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
}
],
- "description": "Provides the functionality to compare PHP values for equality",
- "homepage": "https://github.com/sebastianbergmann/comparator",
+ "description": "Eases the creation of beautiful and testable command line interfaces",
+ "homepage": "https://symfony.com",
"keywords": [
- "comparator",
- "compare",
- "equality"
+ "cli",
+ "command-line",
+ "console",
+ "terminal"
],
"support": {
- "issues": "https://github.com/sebastianbergmann/comparator/issues",
- "security": "https://github.com/sebastianbergmann/comparator/security/policy",
- "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.4"
+ "source": "https://github.com/symfony/console/tree/v6.4.34"
},
"funding": [
{
- "url": "https://github.com/sebastianbergmann",
- "type": "github"
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
},
{
- "url": "https://liberapay.com/sebastianbergmann",
- "type": "liberapay"
+ "url": "https://github.com/fabpot",
+ "type": "github"
},
{
- "url": "https://thanks.dev/u/gh/sebastianbergmann",
- "type": "thanks_dev"
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
},
{
- "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-09-07T05:25:07+00:00"
+ "time": "2026-02-23T15:42:15+00:00"
},
{
- "name": "sebastian/complexity",
- "version": "3.2.0",
+ "name": "symfony/dependency-injection",
+ "version": "v6.4.34",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/complexity.git",
- "reference": "68ff824baeae169ec9f2137158ee529584553799"
+ "url": "https://github.com/symfony/dependency-injection.git",
+ "reference": "91e49958b8a6092e48e4711894a1aeb1b151c62a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799",
- "reference": "68ff824baeae169ec9f2137158ee529584553799",
+ "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/91e49958b8a6092e48e4711894a1aeb1b151c62a",
+ "reference": "91e49958b8a6092e48e4711894a1aeb1b151c62a",
"shasum": ""
},
"require": {
- "nikic/php-parser": "^4.18 || ^5.0",
- "php": ">=8.1"
+ "php": ">=8.1",
+ "psr/container": "^1.1|^2.0",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/service-contracts": "^2.5|^3.0",
+ "symfony/var-exporter": "^6.4.20|^7.2.5"
+ },
+ "conflict": {
+ "ext-psr": "<1.1|>=2",
+ "symfony/config": "<6.1",
+ "symfony/finder": "<5.4",
+ "symfony/proxy-manager-bridge": "<6.3",
+ "symfony/yaml": "<5.4"
+ },
+ "provide": {
+ "psr/container-implementation": "1.1|2.0",
+ "symfony/service-implementation": "1.1|2.0|3.0"
},
"require-dev": {
- "phpunit/phpunit": "^10.0"
+ "symfony/config": "^6.1|^7.0",
+ "symfony/expression-language": "^5.4|^6.0|^7.0",
+ "symfony/yaml": "^5.4|^6.0|^7.0"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-main": "3.2-dev"
- }
- },
"autoload": {
- "classmap": [
- "src/"
+ "psr-4": {
+ "Symfony\\Component\\DependencyInjection\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de",
- "role": "lead"
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
}
],
- "description": "Library for calculating the complexity of PHP code units",
- "homepage": "https://github.com/sebastianbergmann/complexity",
+ "description": "Allows you to standardize and centralize the way objects are constructed in your application",
+ "homepage": "https://symfony.com",
"support": {
- "issues": "https://github.com/sebastianbergmann/complexity/issues",
- "security": "https://github.com/sebastianbergmann/complexity/security/policy",
- "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0"
+ "source": "https://github.com/symfony/dependency-injection/tree/v6.4.34"
},
"funding": [
{
- "url": "https://github.com/sebastianbergmann",
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
"type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
}
],
- "time": "2023-12-21T08:37:17+00:00"
+ "time": "2026-02-24T15:33:38+00:00"
},
{
- "name": "sebastian/diff",
- "version": "5.1.1",
+ "name": "symfony/filesystem",
+ "version": "v6.4.34",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/diff.git",
- "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e"
+ "url": "https://github.com/symfony/filesystem.git",
+ "reference": "01ffe0411b842f93c571e5c391f289c3fdd498c3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e",
- "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/01ffe0411b842f93c571e5c391f289c3fdd498c3",
+ "reference": "01ffe0411b842f93c571e5c391f289c3fdd498c3",
"shasum": ""
},
"require": {
- "php": ">=8.1"
+ "php": ">=8.1",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-mbstring": "~1.8"
},
"require-dev": {
- "phpunit/phpunit": "^10.0",
- "symfony/process": "^6.4"
+ "symfony/process": "^5.4|^6.4|^7.0"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-main": "5.1-dev"
- }
- },
"autoload": {
- "classmap": [
- "src/"
+ "psr-4": {
+ "Symfony\\Component\\Filesystem\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
},
{
- "name": "Kore Nordmann",
- "email": "mail@kore-nordmann.de"
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
}
],
- "description": "Diff implementation",
- "homepage": "https://github.com/sebastianbergmann/diff",
- "keywords": [
- "diff",
- "udiff",
- "unidiff",
- "unified diff"
- ],
+ "description": "Provides basic utilities for the filesystem",
+ "homepage": "https://symfony.com",
"support": {
- "issues": "https://github.com/sebastianbergmann/diff/issues",
- "security": "https://github.com/sebastianbergmann/diff/security/policy",
- "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1"
+ "source": "https://github.com/symfony/filesystem/tree/v6.4.34"
},
"funding": [
{
- "url": "https://github.com/sebastianbergmann",
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
"type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
}
],
- "time": "2024-03-02T07:15:17+00:00"
+ "time": "2026-02-24T17:51:06+00:00"
},
{
- "name": "sebastian/environment",
- "version": "6.1.0",
+ "name": "symfony/finder",
+ "version": "v6.4.34",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/environment.git",
- "reference": "8074dbcd93529b357029f5cc5058fd3e43666984"
+ "url": "https://github.com/symfony/finder.git",
+ "reference": "9590e86be1d1c57bfbb16d0dd040345378c20896"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984",
- "reference": "8074dbcd93529b357029f5cc5058fd3e43666984",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/9590e86be1d1c57bfbb16d0dd040345378c20896",
+ "reference": "9590e86be1d1c57bfbb16d0dd040345378c20896",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"require-dev": {
- "phpunit/phpunit": "^10.0"
- },
- "suggest": {
- "ext-posix": "*"
+ "symfony/filesystem": "^6.0|^7.0"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-main": "6.1-dev"
- }
- },
"autoload": {
- "classmap": [
- "src/"
+ "psr-4": {
+ "Symfony\\Component\\Finder\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
- }
- ],
- "description": "Provides functionality to handle HHVM/PHP environments",
- "homepage": "https://github.com/sebastianbergmann/environment",
- "keywords": [
- "Xdebug",
- "environment",
- "hhvm"
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
],
+ "description": "Finds files and directories via an intuitive fluent interface",
+ "homepage": "https://symfony.com",
"support": {
- "issues": "https://github.com/sebastianbergmann/environment/issues",
- "security": "https://github.com/sebastianbergmann/environment/security/policy",
- "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0"
+ "source": "https://github.com/symfony/finder/tree/v6.4.34"
},
"funding": [
{
- "url": "https://github.com/sebastianbergmann",
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
"type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
}
],
- "time": "2024-03-23T08:47:14+00:00"
+ "time": "2026-01-28T15:16:37+00:00"
},
{
- "name": "sebastian/exporter",
- "version": "5.1.4",
+ "name": "symfony/polyfill-intl-grapheme",
+ "version": "v1.33.0",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/exporter.git",
- "reference": "0735b90f4da94969541dac1da743446e276defa6"
+ "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
+ "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6",
- "reference": "0735b90f4da94969541dac1da743446e276defa6",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70",
+ "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70",
"shasum": ""
},
"require": {
- "ext-mbstring": "*",
- "php": ">=8.1",
- "sebastian/recursion-context": "^5.0"
+ "php": ">=7.2"
},
- "require-dev": {
- "phpunit/phpunit": "^10.5"
+ "suggest": {
+ "ext-intl": "For best performance"
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-main": "5.1-dev"
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
}
},
"autoload": {
- "classmap": [
- "src/"
- ]
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Grapheme\\": ""
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
- },
- {
- "name": "Jeff Welch",
- "email": "whatthejeff@gmail.com"
- },
- {
- "name": "Volker Dusch",
- "email": "github@wallbash.com"
- },
- {
- "name": "Adam Harvey",
- "email": "aharvey@php.net"
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
},
{
- "name": "Bernhard Schussek",
- "email": "bschussek@gmail.com"
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
}
],
- "description": "Provides the functionality to export PHP variables for visualization",
- "homepage": "https://www.github.com/sebastianbergmann/exporter",
+ "description": "Symfony polyfill for intl's grapheme_* functions",
+ "homepage": "https://symfony.com",
"keywords": [
- "export",
- "exporter"
+ "compatibility",
+ "grapheme",
+ "intl",
+ "polyfill",
+ "portable",
+ "shim"
],
"support": {
- "issues": "https://github.com/sebastianbergmann/exporter/issues",
- "security": "https://github.com/sebastianbergmann/exporter/security/policy",
- "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4"
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0"
},
"funding": [
{
- "url": "https://github.com/sebastianbergmann",
- "type": "github"
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
},
{
- "url": "https://liberapay.com/sebastianbergmann",
- "type": "liberapay"
+ "url": "https://github.com/fabpot",
+ "type": "github"
},
{
- "url": "https://thanks.dev/u/gh/sebastianbergmann",
- "type": "thanks_dev"
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
},
{
- "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-09-24T06:09:11+00:00"
+ "time": "2025-06-27T09:58:17+00:00"
},
{
- "name": "sebastian/global-state",
- "version": "6.0.2",
+ "name": "symfony/polyfill-php81",
+ "version": "v1.33.0",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/global-state.git",
- "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9"
+ "url": "https://github.com/symfony/polyfill-php81.git",
+ "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9",
- "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9",
+ "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
+ "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"shasum": ""
},
"require": {
- "php": ">=8.1",
- "sebastian/object-reflector": "^3.0",
- "sebastian/recursion-context": "^5.0"
- },
- "require-dev": {
- "ext-dom": "*",
- "phpunit/phpunit": "^10.0"
+ "php": ">=7.2"
},
"type": "library",
"extra": {
- "branch-alias": {
- "dev-main": "6.0-dev"
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
}
},
"autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php81\\": ""
+ },
"classmap": [
- "src/"
+ "Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
}
],
- "description": "Snapshotting of global state",
- "homepage": "https://www.github.com/sebastianbergmann/global-state",
+ "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
"keywords": [
- "global state"
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
],
"support": {
- "issues": "https://github.com/sebastianbergmann/global-state/issues",
- "security": "https://github.com/sebastianbergmann/global-state/security/policy",
- "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2"
+ "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0"
},
"funding": [
{
- "url": "https://github.com/sebastianbergmann",
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
"type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
}
],
- "time": "2024-03-02T07:19:19+00:00"
+ "time": "2024-09-09T11:45:10+00:00"
},
{
- "name": "sebastian/lines-of-code",
- "version": "2.0.2",
+ "name": "symfony/process",
+ "version": "v6.4.33",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/lines-of-code.git",
- "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0"
+ "url": "https://github.com/symfony/process.git",
+ "reference": "c46e854e79b52d07666e43924a20cb6dc546644e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0",
- "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0",
+ "url": "https://api.github.com/repos/symfony/process/zipball/c46e854e79b52d07666e43924a20cb6dc546644e",
+ "reference": "c46e854e79b52d07666e43924a20cb6dc546644e",
"shasum": ""
},
"require": {
- "nikic/php-parser": "^4.18 || ^5.0",
"php": ">=8.1"
},
- "require-dev": {
- "phpunit/phpunit": "^10.0"
- },
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-main": "2.0-dev"
- }
- },
"autoload": {
- "classmap": [
- "src/"
+ "psr-4": {
+ "Symfony\\Component\\Process\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de",
- "role": "lead"
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
}
],
- "description": "Library for counting the lines of code in PHP source code",
- "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+ "description": "Executes commands in sub-processes",
+ "homepage": "https://symfony.com",
"support": {
- "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
- "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy",
- "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2"
+ "source": "https://github.com/symfony/process/tree/v6.4.33"
},
"funding": [
{
- "url": "https://github.com/sebastianbergmann",
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
"type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
}
],
- "time": "2023-12-21T08:38:20+00:00"
+ "time": "2026-01-23T16:02:12+00:00"
},
{
- "name": "sebastian/object-enumerator",
- "version": "5.0.0",
+ "name": "symfony/string",
+ "version": "v6.4.34",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/object-enumerator.git",
- "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906"
+ "url": "https://github.com/symfony/string.git",
+ "reference": "2adaf4106f2ef4c67271971bde6d3fe0a6936432"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906",
- "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906",
+ "url": "https://api.github.com/repos/symfony/string/zipball/2adaf4106f2ef4c67271971bde6d3fe0a6936432",
+ "reference": "2adaf4106f2ef4c67271971bde6d3fe0a6936432",
"shasum": ""
},
"require": {
"php": ">=8.1",
- "sebastian/object-reflector": "^3.0",
- "sebastian/recursion-context": "^5.0"
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-intl-grapheme": "~1.0",
+ "symfony/polyfill-intl-normalizer": "~1.0",
+ "symfony/polyfill-mbstring": "~1.0"
+ },
+ "conflict": {
+ "symfony/translation-contracts": "<2.5"
},
"require-dev": {
- "phpunit/phpunit": "^10.0"
+ "symfony/http-client": "^5.4|^6.0|^7.0",
+ "symfony/intl": "^6.2|^7.0",
+ "symfony/translation-contracts": "^2.5|^3.0",
+ "symfony/var-exporter": "^5.4|^6.0|^7.0"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-main": "5.0-dev"
- }
- },
"autoload": {
- "classmap": [
- "src/"
+ "files": [
+ "Resources/functions.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\String\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
}
],
- "description": "Traverses array structures and object graphs to enumerate all referenced objects",
- "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "grapheme",
+ "i18n",
+ "string",
+ "unicode",
+ "utf-8",
+ "utf8"
+ ],
"support": {
- "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
- "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0"
+ "source": "https://github.com/symfony/string/tree/v6.4.34"
},
"funding": [
{
- "url": "https://github.com/sebastianbergmann",
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
"type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
}
],
- "time": "2023-02-03T07:08:32+00:00"
+ "time": "2026-02-08T20:44:54+00:00"
},
{
- "name": "sebastian/object-reflector",
- "version": "3.0.0",
+ "name": "symfony/var-exporter",
+ "version": "v6.4.26",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/object-reflector.git",
- "reference": "24ed13d98130f0e7122df55d06c5c4942a577957"
+ "url": "https://github.com/symfony/var-exporter.git",
+ "reference": "466fcac5fa2e871f83d31173f80e9c2684743bfc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957",
- "reference": "24ed13d98130f0e7122df55d06c5c4942a577957",
+ "url": "https://api.github.com/repos/symfony/var-exporter/zipball/466fcac5fa2e871f83d31173f80e9c2684743bfc",
+ "reference": "466fcac5fa2e871f83d31173f80e9c2684743bfc",
"shasum": ""
},
"require": {
- "php": ">=8.1"
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3"
},
"require-dev": {
- "phpunit/phpunit": "^10.0"
+ "symfony/property-access": "^6.4|^7.0",
+ "symfony/serializer": "^6.4|^7.0",
+ "symfony/var-dumper": "^5.4|^6.0|^7.0"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-main": "3.0-dev"
- }
- },
"autoload": {
- "classmap": [
- "src/"
+ "psr-4": {
+ "Symfony\\Component\\VarExporter\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
}
],
- "description": "Allows reflection of object attributes, including inherited and non-public ones",
- "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+ "description": "Allows exporting any serializable PHP data structure to plain PHP code",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "clone",
+ "construct",
+ "export",
+ "hydrate",
+ "instantiate",
+ "lazy-loading",
+ "proxy",
+ "serialize"
+ ],
"support": {
- "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
- "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0"
+ "source": "https://github.com/symfony/var-exporter/tree/v6.4.26"
},
"funding": [
{
- "url": "https://github.com/sebastianbergmann",
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
"type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
}
],
- "time": "2023-02-03T07:06:18+00:00"
+ "time": "2025-09-11T09:57:09+00:00"
},
{
- "name": "sebastian/recursion-context",
- "version": "5.0.1",
+ "name": "symfony/yaml",
+ "version": "v6.4.34",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/recursion-context.git",
- "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a"
+ "url": "https://github.com/symfony/yaml.git",
+ "reference": "7bca30dabed7900a08c5ad4f1d6483f881a64d0f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a",
- "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/7bca30dabed7900a08c5ad4f1d6483f881a64d0f",
+ "reference": "7bca30dabed7900a08c5ad4f1d6483f881a64d0f",
"shasum": ""
},
"require": {
- "php": ">=8.1"
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-ctype": "^1.8"
+ },
+ "conflict": {
+ "symfony/console": "<5.4"
},
"require-dev": {
- "phpunit/phpunit": "^10.5"
+ "symfony/console": "^5.4|^6.0|^7.0"
},
+ "bin": [
+ "Resources/bin/yaml-lint"
+ ],
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-main": "5.0-dev"
- }
- },
"autoload": {
- "classmap": [
- "src/"
+ "psr-4": {
+ "Symfony\\Component\\Yaml\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
- },
- {
- "name": "Jeff Welch",
- "email": "whatthejeff@gmail.com"
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
},
{
- "name": "Adam Harvey",
- "email": "aharvey@php.net"
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
}
],
- "description": "Provides functionality to recursively process PHP variables",
- "homepage": "https://github.com/sebastianbergmann/recursion-context",
+ "description": "Loads and dumps YAML files",
+ "homepage": "https://symfony.com",
"support": {
- "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
- "security": "https://github.com/sebastianbergmann/recursion-context/security/policy",
- "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1"
+ "source": "https://github.com/symfony/yaml/tree/v6.4.34"
},
"funding": [
{
- "url": "https://github.com/sebastianbergmann",
- "type": "github"
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
},
{
- "url": "https://liberapay.com/sebastianbergmann",
- "type": "liberapay"
+ "url": "https://github.com/fabpot",
+ "type": "github"
},
{
- "url": "https://thanks.dev/u/gh/sebastianbergmann",
- "type": "thanks_dev"
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
},
{
- "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context",
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2025-08-10T07:50:56+00:00"
+ "time": "2026-02-06T18:32:11+00:00"
},
{
- "name": "sebastian/type",
- "version": "4.0.0",
+ "name": "theseer/tokenizer",
+ "version": "1.3.1",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/type.git",
- "reference": "462699a16464c3944eefc02ebdd77882bd3925bf"
+ "url": "https://github.com/theseer/tokenizer.git",
+ "reference": "b7489ce515e168639d17feec34b8847c326b0b3c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf",
- "reference": "462699a16464c3944eefc02ebdd77882bd3925bf",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c",
+ "reference": "b7489ce515e168639d17feec34b8847c326b0b3c",
"shasum": ""
},
"require": {
- "php": ">=8.1"
- },
- "require-dev": {
- "phpunit/phpunit": "^10.0"
+ "ext-dom": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": "^7.2 || ^8.0"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-main": "4.0-dev"
- }
- },
"autoload": {
"classmap": [
"src/"
@@ -4839,127 +8486,191 @@
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de",
- "role": "lead"
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
}
],
- "description": "Collection of value objects that represent the types of the PHP type system",
- "homepage": "https://github.com/sebastianbergmann/type",
+ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"support": {
- "issues": "https://github.com/sebastianbergmann/type/issues",
- "source": "https://github.com/sebastianbergmann/type/tree/4.0.0"
+ "issues": "https://github.com/theseer/tokenizer/issues",
+ "source": "https://github.com/theseer/tokenizer/tree/1.3.1"
},
"funding": [
{
- "url": "https://github.com/sebastianbergmann",
+ "url": "https://github.com/theseer",
"type": "github"
}
],
- "time": "2023-02-03T07:10:45+00:00"
+ "time": "2025-11-17T20:03:58+00:00"
},
{
- "name": "sebastian/version",
- "version": "4.0.1",
+ "name": "vimeo/psalm",
+ "version": "5.26.1",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/version.git",
- "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17"
+ "url": "https://github.com/vimeo/psalm.git",
+ "reference": "d747f6500b38ac4f7dfc5edbcae6e4b637d7add0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17",
- "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17",
+ "url": "https://api.github.com/repos/vimeo/psalm/zipball/d747f6500b38ac4f7dfc5edbcae6e4b637d7add0",
+ "reference": "d747f6500b38ac4f7dfc5edbcae6e4b637d7add0",
"shasum": ""
},
"require": {
- "php": ">=8.1"
+ "amphp/amp": "^2.4.2",
+ "amphp/byte-stream": "^1.5",
+ "composer-runtime-api": "^2",
+ "composer/semver": "^1.4 || ^2.0 || ^3.0",
+ "composer/xdebug-handler": "^2.0 || ^3.0",
+ "dnoegel/php-xdg-base-dir": "^0.1.1",
+ "ext-ctype": "*",
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-simplexml": "*",
+ "ext-tokenizer": "*",
+ "felixfbecker/advanced-json-rpc": "^3.1",
+ "felixfbecker/language-server-protocol": "^1.5.2",
+ "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0",
+ "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0",
+ "nikic/php-parser": "^4.17",
+ "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0",
+ "sebastian/diff": "^4.0 || ^5.0 || ^6.0",
+ "spatie/array-to-xml": "^2.17.0 || ^3.0",
+ "symfony/console": "^4.1.6 || ^5.0 || ^6.0 || ^7.0",
+ "symfony/filesystem": "^5.4 || ^6.0 || ^7.0"
},
- "type": "library",
+ "conflict": {
+ "nikic/php-parser": "4.17.0"
+ },
+ "provide": {
+ "psalm/psalm": "self.version"
+ },
+ "require-dev": {
+ "amphp/phpunit-util": "^2.0",
+ "bamarni/composer-bin-plugin": "^1.4",
+ "brianium/paratest": "^6.9",
+ "ext-curl": "*",
+ "mockery/mockery": "^1.5",
+ "nunomaduro/mock-final-classes": "^1.1",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/phpdoc-parser": "^1.6",
+ "phpunit/phpunit": "^9.6",
+ "psalm/plugin-mockery": "^1.1",
+ "psalm/plugin-phpunit": "^0.18",
+ "slevomat/coding-standard": "^8.4",
+ "squizlabs/php_codesniffer": "^3.6",
+ "symfony/process": "^4.4 || ^5.0 || ^6.0 || ^7.0"
+ },
+ "suggest": {
+ "ext-curl": "In order to send data to shepherd",
+ "ext-igbinary": "^2.0.5 is required, used to serialize caching data"
+ },
+ "bin": [
+ "psalm",
+ "psalm-language-server",
+ "psalm-plugin",
+ "psalm-refactor",
+ "psalter"
+ ],
+ "type": "project",
"extra": {
"branch-alias": {
- "dev-main": "4.0-dev"
+ "dev-1.x": "1.x-dev",
+ "dev-2.x": "2.x-dev",
+ "dev-3.x": "3.x-dev",
+ "dev-4.x": "4.x-dev",
+ "dev-master": "5.x-dev"
}
},
"autoload": {
- "classmap": [
- "src/"
- ]
+ "psr-4": {
+ "Psalm\\": "src/Psalm/"
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de",
- "role": "lead"
+ "name": "Matthew Brown"
}
],
- "description": "Library that helps with managing the version number of Git-hosted PHP projects",
- "homepage": "https://github.com/sebastianbergmann/version",
+ "description": "A static analysis tool for finding errors in PHP applications",
+ "keywords": [
+ "code",
+ "inspection",
+ "php",
+ "static analysis"
+ ],
"support": {
- "issues": "https://github.com/sebastianbergmann/version/issues",
- "source": "https://github.com/sebastianbergmann/version/tree/4.0.1"
+ "docs": "https://psalm.dev/docs",
+ "issues": "https://github.com/vimeo/psalm/issues",
+ "source": "https://github.com/vimeo/psalm"
},
- "funding": [
- {
- "url": "https://github.com/sebastianbergmann",
- "type": "github"
- }
- ],
- "time": "2023-02-07T11:34:05+00:00"
+ "time": "2024-09-08T18:53:08+00:00"
},
{
- "name": "theseer/tokenizer",
- "version": "1.2.3",
+ "name": "webmozart/assert",
+ "version": "1.12.1",
"source": {
"type": "git",
- "url": "https://github.com/theseer/tokenizer.git",
- "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
+ "url": "https://github.com/webmozarts/assert.git",
+ "reference": "9be6926d8b485f55b9229203f962b51ed377ba68"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
- "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+ "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68",
+ "reference": "9be6926d8b485f55b9229203f962b51ed377ba68",
"shasum": ""
},
"require": {
- "ext-dom": "*",
- "ext-tokenizer": "*",
- "ext-xmlwriter": "*",
+ "ext-ctype": "*",
+ "ext-date": "*",
+ "ext-filter": "*",
"php": "^7.2 || ^8.0"
},
+ "suggest": {
+ "ext-intl": "",
+ "ext-simplexml": "",
+ "ext-spl": ""
+ },
"type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.10-dev"
+ }
+ },
"autoload": {
- "classmap": [
- "src/"
- ]
+ "psr-4": {
+ "Webmozart\\Assert\\": "src/"
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "BSD-3-Clause"
+ "MIT"
],
"authors": [
{
- "name": "Arne Blankerts",
- "email": "arne@blankerts.de",
- "role": "Developer"
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
}
],
- "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "description": "Assertions to validate method input/output with nice error messages.",
+ "keywords": [
+ "assert",
+ "check",
+ "validate"
+ ],
"support": {
- "issues": "https://github.com/theseer/tokenizer/issues",
- "source": "https://github.com/theseer/tokenizer/tree/1.2.3"
+ "issues": "https://github.com/webmozarts/assert/issues",
+ "source": "https://github.com/webmozarts/assert/tree/1.12.1"
},
- "funding": [
- {
- "url": "https://github.com/theseer",
- "type": "github"
- }
- ],
- "time": "2024-03-03T12:36:25+00:00"
+ "time": "2025-10-29T15:56:20+00:00"
}
],
"aliases": [],
diff --git a/docs/FEATURES.md b/docs/FEATURES.md
new file mode 100644
index 00000000..58583582
--- /dev/null
+++ b/docs/FEATURES.md
@@ -0,0 +1,33 @@
+---
+sidebar_position: 1
+---
+
+# Features
+
+Software Catalogus is a structured software portfolio management app for Nextcloud. It enables organizations to register, track, and publish their software landscape in an open, standardized way.
+
+## Core Features
+
+### Software Registration
+Register applications with full metadata: name, description, organization, repository links, and licence. Maintain a single source of truth for your entire software portfolio.
+
+### Module Tracking
+Break down applications into functional modules. Track each module's purpose, dependencies, and integration points with other systems.
+
+### Connection Mapping
+Map connections (koppelingen) between applications and modules. Visualize your software landscape and understand system dependencies at a glance.
+
+### Federated Synchronization
+Synchronize catalogue data across organizations. Import and merge listings from external sources to build a collaborative, federated software registry.
+
+### Automatic User Provisioning
+Automatically create Nextcloud user accounts for registered organizations and their members. Keeps access control in sync with your catalogue data.
+
+### Open Data Publishing
+Publish your software catalogue as open data. Exposes standardized API endpoints for public consumption and GEMMA compliance.
+
+### GEMMA Compliance
+Built around the GEMMA (Gemeentelijk Model Architectuur) reference architecture for Dutch municipalities. Supports standardized categorization and classification.
+
+### OpenRegister Integration
+All objects are stored as flexible OpenRegister objects, enabling full audit trails, versioning, and cross-app data sharing.
diff --git a/docusaurus/docusaurus.config.js b/docusaurus/docusaurus.config.js
new file mode 100644
index 00000000..449452f7
--- /dev/null
+++ b/docusaurus/docusaurus.config.js
@@ -0,0 +1,102 @@
+// @ts-check
+
+/** @type {import('@docusaurus/types').Config} */
+const config = {
+ title: 'Software Catalogus',
+ tagline: 'Manage your software portfolio with applications, modules, and connections',
+ url: 'https://softwarecatalog.app',
+ baseUrl: '/',
+
+ organizationName: 'ConductionNL',
+ projectName: 'softwarecatalog',
+ trailingSlash: false,
+
+ onBrokenLinks: 'warn',
+ onBrokenMarkdownLinks: 'warn',
+
+ i18n: {
+ defaultLocale: 'en',
+ locales: ['en'],
+ },
+
+ presets: [
+ [
+ 'classic',
+ /** @type {import('@docusaurus/preset-classic').Options} */
+ ({
+ docs: {
+ path: '../docs',
+ sidebarPath: require.resolve('./sidebars.js'),
+ editUrl:
+ 'https://github.com/ConductionNL/softwarecatalog/tree/main/docusaurus/',
+ },
+ blog: false,
+ theme: {
+ customCss: require.resolve('./src/css/custom.css'),
+ },
+ }),
+ ],
+ ],
+
+ themeConfig:
+ /** @type {import('@docusaurus/preset-classic').ThemeConfig} */
+ ({
+ navbar: {
+ title: 'Software Catalogus',
+ logo: {
+ alt: 'Software Catalogus Logo',
+ src: 'img/logo.svg',
+ },
+ items: [
+ {
+ type: 'docSidebar',
+ sidebarId: 'tutorialSidebar',
+ position: 'left',
+ label: 'Documentation',
+ },
+ {
+ href: 'https://github.com/ConductionNL/softwarecatalog',
+ label: 'GitHub',
+ position: 'right',
+ },
+ ],
+ },
+ footer: {
+ style: 'dark',
+ links: [
+ {
+ title: 'Docs',
+ items: [
+ {
+ label: 'Documentation',
+ to: '/docs/FEATURES',
+ },
+ ],
+ },
+ {
+ title: 'Community',
+ items: [
+ {
+ label: 'GitHub',
+ href: 'https://github.com/ConductionNL/softwarecatalog',
+ },
+ ],
+ },
+ ],
+ copyright: `Copyright © ${new Date().getFullYear()} for Open Webconcept by Conduction B.V. `,
+ },
+ prism: {
+ theme: require('prism-react-renderer/themes/github'),
+ darkTheme: require('prism-react-renderer/themes/dracula'),
+ },
+ mermaid: {
+ theme: { light: 'default', dark: 'dark' },
+ },
+ }),
+ markdown: {
+ mermaid: true,
+ },
+ themes: ['@docusaurus/theme-mermaid'],
+};
+
+module.exports = config;
diff --git a/docusaurus/package.json b/docusaurus/package.json
new file mode 100644
index 00000000..78efa2a0
--- /dev/null
+++ b/docusaurus/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "softwarecatalog-docs",
+ "version": "0.0.0",
+ "private": true,
+ "scripts": {
+ "docusaurus": "docusaurus",
+ "start": "docusaurus start",
+ "build": "docusaurus build",
+ "swizzle": "docusaurus swizzle",
+ "deploy": "docusaurus deploy",
+ "clear": "docusaurus clear",
+ "serve": "docusaurus serve",
+ "write-translations": "docusaurus write-translations",
+ "write-heading-ids": "docusaurus write-heading-ids",
+ "ci": "npm ci --legacy-peer-deps && npm run build"
+ },
+ "dependencies": {
+ "@docusaurus/core": "^3.7.0",
+ "@docusaurus/preset-classic": "^3.7.0",
+ "@docusaurus/theme-mermaid": "^3.7.0",
+ "@mdx-js/react": "^3.1.0",
+ "clsx": "^1.2.1",
+ "prism-react-renderer": "^1.3.5",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@docusaurus/module-type-aliases": "^3.7.0"
+ },
+ "browserslist": {
+ "production": [
+ ">0.5%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "engines": {
+ "node": ">=18.0"
+ }
+}
diff --git a/docusaurus/sidebars.js b/docusaurus/sidebars.js
new file mode 100644
index 00000000..74c2e6ce
--- /dev/null
+++ b/docusaurus/sidebars.js
@@ -0,0 +1,6 @@
+/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
+const sidebars = {
+ tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
+};
+
+module.exports = sidebars;
diff --git a/docusaurus/src/components/HomepageFeatures/index.js b/docusaurus/src/components/HomepageFeatures/index.js
new file mode 100644
index 00000000..1c659190
--- /dev/null
+++ b/docusaurus/src/components/HomepageFeatures/index.js
@@ -0,0 +1,55 @@
+import React from 'react';
+import clsx from 'clsx';
+import styles from './styles.module.css';
+
+const FeatureList = [
+ {
+ title: 'Software Registration',
+ description: (
+ <>
+ Register applications and modules with full metadata. Maintain a single source of truth for your entire software portfolio across your organization.
+ >
+ ),
+ },
+ {
+ title: 'Connection Mapping',
+ description: (
+ <>
+ Map connections between applications and modules. Visualize your software landscape and understand system dependencies at a glance.
+ >
+ ),
+ },
+ {
+ title: 'Open Data & GEMMA',
+ description: (
+ <>
+ Publish your catalogue as open data with GEMMA-compliant classification. Federated synchronization enables cross-organization data sharing.
+ >
+ ),
+ },
+];
+
+function Feature({title, description}) {
+ return (
+
+
+
{title}
+
{description}
+
+
+ );
+}
+
+export default function HomepageFeatures() {
+ return (
+
+
+
+ {FeatureList.map((props, idx) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/docusaurus/src/components/HomepageFeatures/styles.module.css b/docusaurus/src/components/HomepageFeatures/styles.module.css
new file mode 100644
index 00000000..2132b3d1
--- /dev/null
+++ b/docusaurus/src/components/HomepageFeatures/styles.module.css
@@ -0,0 +1,6 @@
+.features {
+ display: flex;
+ align-items: center;
+ padding: 2rem 0;
+ width: 100%;
+}
diff --git a/docusaurus/src/css/custom.css b/docusaurus/src/css/custom.css
new file mode 100644
index 00000000..44fc527e
--- /dev/null
+++ b/docusaurus/src/css/custom.css
@@ -0,0 +1,121 @@
+/**
+ * Any CSS included here will be global. The classic template
+ * bundles Infima by default. Infima is a CSS framework designed to
+ * work well for content-first websites.
+ */
+
+/* Import Poppins font from Google Fonts */
+@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
+
+/* You can override the default Infima variables here. */
+:root {
+ /* Primary color: Open Webconcept green */
+ --ifm-color-primary: #2fb298;
+ --ifm-color-primary-dark: #28a088;
+ --ifm-color-primary-darker: #248e79;
+ --ifm-color-primary-darkest: #1d7563;
+ --ifm-color-primary-light: #34c4a7;
+ --ifm-color-primary-lighter: #3dd1b3;
+ --ifm-color-primary-lightest: #5ad9c1;
+
+ /* Typography settings */
+ --ifm-font-family-base: 'Poppins', system-ui, -apple-system, sans-serif;
+ --ifm-heading-font-family: 'Poppins', system-ui, -apple-system, sans-serif;
+ --ifm-font-weight-semibold: 600;
+ --ifm-heading-font-weight: 600;
+ --ifm-h1-font-size: 2.5rem;
+ --ifm-h2-font-size: 2rem;
+ --ifm-h3-font-size: 1.5rem;
+ --ifm-h4-font-size: 1.25rem;
+
+ /* Code settings */
+ --ifm-code-font-size: 95%;
+ --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
+}
+
+/* Dark mode color palette */
+[data-theme='dark'] {
+ /* Primary colors */
+ --ifm-color-primary: #34c4a7;
+ --ifm-color-primary-dark: #2fb298;
+ --ifm-color-primary-darker: #2ba68d;
+ --ifm-color-primary-darkest: #248e79;
+ --ifm-color-primary-light: #47cbb1;
+ --ifm-color-primary-lighter: #54cfb7;
+ --ifm-color-primary-lightest: #71d8c4;
+ --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
+
+ /* Background colors */
+ --ifm-background-color: #1e1e1e;
+ --ifm-background-surface-color: #242526;
+
+ /* Text colors */
+ --ifm-font-color-base: #e5e5e5;
+ --ifm-heading-color: #ffffff;
+ --ifm-color-content: #e5e5e5;
+ --ifm-color-content-secondary: #b0b0b0;
+
+ /* Navbar */
+ --ifm-navbar-background-color: #242526;
+ --ifm-navbar-link-color: #e5e5e5;
+ --ifm-navbar-link-hover-color: #34c4a7;
+
+ /* Sidebar */
+ --ifm-sidebar-background-color: #1e1e1e;
+ --ifm-menu-color: #e5e5e5;
+ --ifm-menu-color-active: #34c4a7;
+
+ /* Code blocks */
+ --ifm-code-background: rgba(0, 0, 0, 0.3);
+ --ifm-code-color: #e5e5e5;
+
+ /* Tables */
+ --ifm-table-border-color: #3a3a3a;
+ --ifm-table-stripe-background: rgba(255, 255, 255, 0.05);
+
+ /* Cards */
+ --ifm-card-background-color: #242526;
+ --ifm-card-border-color: #3a3a3a;
+
+ /* Performance optimizations */
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+/* Typography adjustments */
+.markdown {
+ font-weight: 400;
+ line-height: 1.8;
+}
+
+.markdown h1, .markdown h2, .markdown h3, .markdown h4 {
+ margin-top: 2rem;
+ margin-bottom: 1rem;
+ font-weight: 600;
+}
+
+/* Navbar adjustments */
+.navbar {
+ font-weight: 500;
+}
+
+/* Sidebar adjustments */
+.menu {
+ font-weight: 400;
+}
+
+/* Smooth transitions for theme switching */
+html {
+ transition: background-color 0.2s ease, color 0.2s ease;
+}
+
+/* Reduce motion for accessibility */
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+}
diff --git a/docusaurus/src/pages/index.js b/docusaurus/src/pages/index.js
new file mode 100644
index 00000000..069eff88
--- /dev/null
+++ b/docusaurus/src/pages/index.js
@@ -0,0 +1,41 @@
+import React from 'react';
+import clsx from 'clsx';
+import Link from '@docusaurus/Link';
+import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
+import Layout from '@theme/Layout';
+import HomepageFeatures from '@site/src/components/HomepageFeatures';
+
+import styles from './index.module.css';
+
+function HomepageHeader() {
+ const {siteConfig} = useDocusaurusContext();
+ return (
+
+
+
{siteConfig.title}
+
{siteConfig.tagline}
+
+
+ Documentation
+
+
+
+
+ );
+}
+
+export default function Home() {
+ const {siteConfig} = useDocusaurusContext();
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/docusaurus/src/pages/index.module.css b/docusaurus/src/pages/index.module.css
new file mode 100644
index 00000000..f331949d
--- /dev/null
+++ b/docusaurus/src/pages/index.module.css
@@ -0,0 +1,18 @@
+.heroBanner {
+ padding: 4rem 0;
+ text-align: center;
+ position: relative;
+ overflow: hidden;
+}
+
+@media screen and (max-width: 996px) {
+ .heroBanner {
+ padding: 2rem;
+ }
+}
+
+.buttons {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
diff --git a/docusaurus/static/CNAME b/docusaurus/static/CNAME
new file mode 100644
index 00000000..ca42b216
--- /dev/null
+++ b/docusaurus/static/CNAME
@@ -0,0 +1 @@
+softwarecatalog.app
\ No newline at end of file
diff --git a/docusaurus/static/img/logo.svg b/docusaurus/static/img/logo.svg
new file mode 100644
index 00000000..331eecee
--- /dev/null
+++ b/docusaurus/static/img/logo.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/img/app-store.svg b/img/app-store.svg
new file mode 100644
index 00000000..331eecee
--- /dev/null
+++ b/img/app-store.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/img/screenshot-applications.png b/img/screenshot-applications.png
new file mode 100644
index 00000000..c7494998
Binary files /dev/null and b/img/screenshot-applications.png differ
diff --git a/img/screenshot-connections.png b/img/screenshot-connections.png
new file mode 100644
index 00000000..f8744fe4
Binary files /dev/null and b/img/screenshot-connections.png differ
diff --git a/img/screenshot-dashboard.png b/img/screenshot-dashboard.png
new file mode 100644
index 00000000..c31828a1
Binary files /dev/null and b/img/screenshot-dashboard.png differ
diff --git a/issues.md b/issues.md
new file mode 100644
index 00000000..a39a06d2
--- /dev/null
+++ b/issues.md
@@ -0,0 +1,3617 @@
+# Softwarecatalogus - Issues In Review
+
+**Date:** 2026-03-05
+**Source:** [VNG-Realisatie Project Board #17, View 22](https://github.com/orgs/VNG-Realisatie/projects/17/views/22)
+
+> Zie ook: [aanvullende-informatie.md](aanvullende-informatie.md) voor het overzicht van alle IGS-gelabelde issues (nieuw, overleg, review).
+
+## Summary
+
+- **Total open issues on GitHub:** 207
+- **IGS issues (detailed with acceptance criteria):** 136 (was 130, +6 new issues)
+- **Other issues (summary table):** 79
+- **Issues closed since last update:** 58 (see closed list below)
+- **Reopened issues:** 3 (#6, #15, #23 — removed from closed list)
+- **Issues with screenshot-based acceptance criteria:** See "Image Comparison" note below
+
+### Image Comparison for Screenshot-Based Issues
+
+Many issues (especially wizard text changes #359, #360, #274, #376, #386, #387, #390) reference PowerPoint screenshots as the source of truth for expected text. Testing agents should:
+1. **Fetch the reference screenshot** from the GitHub issue image URL using `WebFetch`
+2. **Navigate to the relevant wizard step** in the browser
+3. **Take a screenshot** of the current UI using `browser_take_screenshot`
+4. **Compare visually** — Claude is multimodal and can read text from both images to compare labels, titles, tooltips, field names, etc.
+5. Mark individual text elements as matching or not matching
+
+The authoritative PowerPoint source document is attached to issue #329.
+
+
+### Test Type Legend
+
+Each acceptance criterion is tagged with a test type:
+- **[API]** — Fully testable via Postman/Newman API calls
+- **[UI]** — Requires browser/visual testing (not coverable by API tests)
+- **[HYBRID]** — API portion tested in Postman; UI portion requires browser testing
+
+### Recently Closed Issues (since 2026-02-28)
+
+The following 56 issues have been **closed** on GitHub since the last update. They should be marked as resolved in test results:
+
+#3, #4, #66, #160, #172, #186, #189, #190, #191, #248, #254, #263, #264, #265, #273, #274, #277, #278, #280, #283, #284, #285, #287, #288, #289, #290, #291, #292, #295, #297, #298, #299, #301, #303, #304, #305, #306, #330, #334, #337, #344, #345, #346, #347, #351, #364, #365, #366, #370, #378, #379, #389, #392, #399, #402, #407, #225, #315
+
+Previously closed (2026-02-22): #185, #266, #267, #286, #294, #300, #302, #307, #308, #350, #353, #355, #356, #358, #359, #360, #361, #362, #363, #368, #369, #372, #374, #380, #382, #383, #384, #385, #386, #387, #390, #395, #396, #397, #400, #403, #406, #408, #409
+
+**Reopened:** #6, #15, #23 (previously listed as closed, now open again on GitHub)
+
+### New Issues Added (2026-03-04)
+
+15 new issues synced: #187, #443, #444, #445, #446, #447, #448, #449, #450, #451, #452, #453, #454, #455, #456
+
+---
+
+## IGS Issues (In Review) — 136 issues
+
+### #6: Als aanbod-beheerder wil ik kunnen registreren welke standaarden door mijn pakket worden ondersteund en eventueel testrapporten beschikbaar stellen
+
+**Labels:** Aanbod, PvE eis, Bevinding
+**Test Step:** Step 16
+
+**Summary:** As a supply manager, I want to register which standards my package supports and optionally make test reports available, so that municipalities can see compliance information when selecting software.
+
+**Acceptance Criteria:**
+- [x] [API] On the application edit/wizard page, a standards section is available where the supplier can add supported standards
+- [ ] [UI] The supplier can select standards from a dropdown that shows GEMMA and other recognized standards
+- [x] [API] After selecting a standard, the compliance status can be set (e.g., "Ondersteund", "Niet ondersteund")
+- [ ] [UI] An optional URL field for test reports/compliance proof is available per standard
+- [x] [API] The compliance link opens correctly to the external URL (not treated as relative path — see #382)
+- [x] [API] The registered standards are visible on the application's public detail page
+- [x] [API] Standards persist correctly after saving
+- [ ] [HYBRID] When reference components are selected or changed in the wizard, the standards dropdown dynamically refreshes to show only standards linked to the selected reference components
+- [x] [API] Standards are presented at the "standaardversie" (standard version) level, not just at the "standaard" level
+- [x] [API] The total standards list equals the union of all standards linked to all selected reference components
+
+**Key Context from Comments:** Related to #407 (standard links containing duplicate "id-" prefix), #382 (compliance links not working), and #378 (standards resetting after edit). The standards management has had multiple bugs affecting display and saving.
+
+---
+
+### #15: Als aanbod- en gebruik-beheerder wil ik data vanuit de softwarecatalogus kunnen exporteren
+
+**Labels:** Aanbod, Gebruik, PvE eis, Bevinding, Wijziging
+**Test Step:** Step 13
+
+**Summary:** As a supply and usage manager, I want to export data from the Softwarecatalogus so I can use it in other systems or for reporting.
+
+**Acceptance Criteria:**
+- [ ] [UI] On the management overview pages (Applicaties, Diensten, Koppelingen), an export button is available
+- [x] [API] The exported data contains ONLY the applications/products belonging to the user's own organization (not all products in the catalogue)
+- [x] [API] Exported columns include both human-readable names AND UUIDs (extra column prefixed with "_" for ID columns alongside name columns)
+- [ ] [HYBRID] The CSV format correctly separates into columns when using "Text to Columns" in Excel (no columns being overwritten)
+- [x] [API] The export works correctly for both aanbod-beheerder (supply manager) and gebruik-beheerder (usage manager) roles
+- [x] [API] The export reflects RBAC permissions (users only see what they are authorized to see)
+
+**Key Context from Comments:** A previous bug caused all products across the entire catalogue to be exported instead of just the user's own organization. The latest update adds an extra column prefixed with "_" for readable names alongside UUIDs. Related to #355 for UUID readability.
+
+---
+
+### #23: Als aanbod- en gebruik-beheerder van de huidige Softwarecatalogus wil ik mijn reeds geregistreerde gegevens weer zien in de nieuwe Softwarecatalogus
+
+**Labels:** Organisatie en configuratie, Aanbod, Gebruik, PvE eis, Bevinding, Datamigratie
+**Test Step:** Step 19
+
+**Summary:** As a supply and usage manager of the current (old) Softwarecatalogus, I want to see my previously registered data in the new Softwarecatalogus after data migration.
+
+**Acceptance Criteria:**
+- [x] [API] Organizations (gemeenten, samenwerkingen, leveranciers) from the old Softwarecatalogus are present in the new system
+- [x] [API] Applications/modules from the old catalogue are visible and searchable
+- [x] [API] Module versions have been imported without duplicates (unique IDs only)
+- [x] [API] Relationships between objects (e.g., samenwerkingen linked to their gemeenten) are correctly maintained
+- [x] [API] A supplier logging in can see their own previously registered applications and products
+- [x] [API] A municipality logging in can see their own previously registered usage data
+- [x] [API] Koppelingen are imported correctly with readable names where both linked applications exist (UUIDs in names are expected when a referenced application no longer exists — see #312)
+- [x] [API] References to GEMMA reference components and standards point to valid entries
+- [x] [API] Organization names display correctly (not as UUIDs)
+
+**Key Context from Comments:** Data migration CSVs at https://github.com/VNG-Realisatie/Softwarecatalogus-datamigratie/tree/main/data. Multiple corrections were made to the dataset. Related: #312, #315. The import tool is complete; final acceptance depends on verifying the complete imported dataset.
+
+---
+
+### #57: Pakketten opvoeren voor samenwerkingsverband
+
+**Labels:** Gebruik, PvE eis
+**Test Step:** Step 20
+
+**Summary:** As a gebruik-beheerder of a samenwerkingsverband (collaboration), the user should be able to register software packages on behalf of member municipalities. The dashboard crashed with `TypeError: Cannot read properties of undefined (reading 'includes')` when accessing `user.userGroups` without optional chaining.
+
+**Acceptance Criteria:**
+- [x] [HYBRID] Samenwerking user can log in and see the dashboard without crash
+- [x] [UI] Dashboard shows organization name ("Test Samenwerking")
+- [x] [HYBRID] No `TypeError: Cannot read properties of undefined` in console
+- [x] [UI] Welcome section renders correctly for gebruik-beheerder role
+- [ ] [UI] Wizards are available for samenwerking organizations (requires org type configuration)
+- [ ] [UI] Samenwerking user can register packages on behalf of member municipalities (feature not yet implemented)
+
+**Fix (2026-02-26):**
+- Added optional chaining (`?.`) to all `user.userGroups` and `user.isAuthenticated` accesses across 6 files
+- Root cause: 8 locations accessed `user.userGroups` without null-checking the `user` object, causing crash during store hydration or org switching
+- Files fixed: `ac-dashboard.js`, `ac-navigation.js`, `ac-header.js`, `con-dynamic-sidenav.js`, `field-authorization.js`, `ac-beheer.js`
+- Dashboard crash is fixed; remaining items (wizards for samenwerking, member management) are feature gaps, not bugs
+
+---
+
+### #65: Als aanbod- en gebruik-beheerder van een organisatie wil ik mijn collega's toegang kunnen geven tot de softwarecatalogus
+
+**Labels:** Aanbod, Gebruik, PvE eis, Bevinding
+**Test Step:** Step 5
+
+**Summary:** As an organization manager, I want to give my colleagues access to the Softwarecatalogus independently, including managing users/contact persons, assigning roles, and inviting new users.
+
+**Acceptance Criteria:**
+- [ ] [UI] On the /beheer/contactpersonen page, an organization admin can add a new user with: Voornaam, Tussenvoegsel, Achternaam, E-mailadres, Telefoonnummer, Functie
+- [ ] [HYBRID] After creating a new user, they appear in the contact persons overview without page refresh
+- [ ] [UI] The "Organisatie" field is pre-filled (not manually entered)
+- [x] [API] Username is automatically set to the email address
+- [x] [API] Roles appropriate to the organization type are available for selection
+- [x] [API] "Is aanspreekpunt" toggle works and persists after saving
+- [x] [API] Editing a user and saving correctly updates the data
+- [x] [API] Invite functionality sends an invitation granting access
+- [ ] [UI] Deleting a contact person linked to an application shows a warning
+- [x] [API] Only contact persons from the user's own organization are shown
+- [x] [API] Roles available for selection are filtered by organization type (leverancier = only aanbod-beheerder; gemeente = multiple roles)
+- [ ] [UI] A notification field (multi-select) allows configuring which system notifications the user receives
+- [x] [API] A user can be linked to multiple organizations by being added as contact person in each
+- [x] [API] "Uitnodigen" functionality sends an actual invitation email granting the user Softwarecatalogus access
+- [ ] [API] Editing a contact person's email address does not produce a 400 error — the email field accepts valid email formats and saves successfully
+- [ ] [API] If the username equals the email address, changing the email also updates the username (or provides a clear error explaining why it cannot be changed)
+
+**Key Context from Comments:** Known overlap with #365 (user creation not working). Multiple bugs reported: users not appearing after creation, editing not saving, duplicate entries, Conduction accounts appearing. The notification field should be multi-select. VNG reports 400-error when editing email address (2026-03-02).
+
+---
+
+### #73: Als aanbod-beheerder wil ik meerdere contactpersonen kunnen registreren en deze aan specifieke pakketten kunnen koppelen
+
+**Labels:** question, Aanbod, PvE eis, Bevinding
+**Test Step:** Step 5
+
+**Summary:** As a supply manager, I want to register multiple contact persons and link them to specific software packages.
+
+**Acceptance Criteria:**
+- [x] [API] A supplier can add multiple contact persons for their organization
+- [x] [API] Contact persons can be linked to specific applications from a dropdown showing only the organization's own applications
+- [ ] [UI] Contact person form includes: Voornaam, Tussenvoegsel, Achternaam, E-mailadres, Telefoonnummer, Functie
+- [ ] [UI] After saving, a success notification is shown
+- [x] [API] Contact persons are not duplicated when clicking save multiple times
+- [x] [API] "Is aanspreekpunt" toggle persists after save
+- [x] [API] Contact persons display with readable names (not UUIDs) in the application overview
+- [ ] [UI] Deleting a linked contact person shows a warning
+- [ ] [UI] Publishing works in a single action (no double confirmation)
+- [x] [API] List only shows persons from the current user's organization
+- [x] [API] Creating a second contact person for the same organization succeeds without errors
+- [ ] [UI] Pagination on the contact persons overview works correctly
+
+**Key Context from Comments:** Multiple bugs fixed: duplicate saving, form not reopening after save, null for empty first names. Latest comment references #365 as blocking duplicate.
+
+---
+
+### #85: (VNGR) Als ontwikkelaar wil ik via een veilige, publieke API toegang hebben tot aanbodinformatie uit de Softwarecatalogus ID-104
+
+**Labels:** Aanbod, PvE eis, Bevinding
+**Test Step:** Step 12
+
+**Summary:** As a developer, I want secure public API access to supply information (organizations, software packages, standards) for integration.
+
+**Acceptance Criteria:**
+- [x] [API] The public API for the Softwarecatalogus register is accessible and returns data
+- [x] [API] Auto-generated OAS documentation is accessible per register at `/index.php/apps/openregister/api/registers/{id}/oas` (e.g., register 2 for Publications: `/index.php/apps/openregister/api/registers/2/oas`, register 3 for Voorzieningen: `/index.php/apps/openregister/api/registers/3/oas`). **Note:** Registers with an `organisation` field (registers 3 and 4) currently return 500 unless the requesting user is in that org — this is an OpenRegister bug where the OAS endpoint incorrectly filters by organisation.
+- [x] [API] The API returns data about aanbiedende organisaties (offering organizations)
+- [x] [API] The API returns data about aangeboden softwarepakketten (offered software packages)
+- [x] [API] The API returns data about ondersteunde standaarden (supported standards)
+- [x] [API] The API supports standard query parameters for filtering and pagination
+- [x] [API] The OAS documentation link is accessible from the register action menu in the backend
+
+**Key Context from Comments:** The GEMMA data is served by a separate register (#148). This issue covers the Softwarecatalogus (voorzieningen) data specifically. OAS documentation is generated per-register, NOT per-app — use `/api/registers/{id}/oas` endpoints.
+
+---
+
+### #105: Als gebruik-beheerder willen we dat aanbieders onze applicatielandschappen en koppelingen niet zien
+
+**Labels:** Gebruik, PvE eis
+**Test Step:** Step 12
+
+**Summary:** As a gebruik-beheerder, we want suppliers (aanbod-beheerder) to not see our application landscapes and connections. The RBAC model scopes data visibility per organization — the page itself may be accessible, but aanbod-beheerder should only see their own organization's data.
+
+**RBAC Reference:** See `softwarecatalog/lib/Settings/softwarecatalogus_register.json`:
+- `module` (applicatie) schema → `authorization.read`: `{ "group": "aanbod-beheerder", "match": { "_organisation": "$organisation" } }` — own org only
+- `koppeling` schema → `authorization.read`: `{ "group": "aanbod-beheerder", "match": { "_organisation": "$organisation" } }` — own org only
+
+**Acceptance Criteria:**
+- [x] [API] As aanbod-beheerder, the `/beheer/applicatielandschappen` page shows ONLY applications belonging to my own organization
+- [x] [API] As aanbod-beheerder, no other organization's applications are visible in the applicatielandschappen overview
+- [x] [API] As aanbod-beheerder, koppelingen are scoped to my own organization's connections only
+- [x] [API] The RBAC `_organisation` matching correctly filters data per the register.json authorization rules
+- [x] [API] API requests from aanbod-beheerder return only own-org module/koppeling objects
+
+**Key Context from Comments:** The page itself being accessible to aanbod-beheerder is acceptable — the requirement is about **data scoping**, not page visibility. The RBAC model uses conditional matching (`{ "match": { "_organisation": "$organisation" } }`) to ensure aanbod-beheerder only sees their own org's data.
+
+---
+
+### #141: Als functioneel beheerder wil ik, naar aanleiding van gemeentelijke herindeling of een leveranciersovername, organisaties en al hun relaties (aanbod en/of gebruik) kunnen samenvoegen met een bestaande of nieuwe organisatie
+
+**Labels:** Aanbod, PvE eis, nonblock, Bevinding
+**Test Step:** Step 21
+
+**Summary:** As a functional administrator, I want to merge organizations and their relationships following municipal redistricting or vendor acquisitions.
+
+**Acceptance Criteria:**
+- [ ] [UI] In the backend tables view, an object (organization) can be selected for merging
+- [ ] [UI] A merge modal appears with three columns: Object A, Object B, and Result
+- [ ] [UI] In the Result column, the user can choose which object's values to keep for each field
+- [x] [API] After saving, the target object is updated with the chosen values
+- [x] [API] All relationships (aanbod, gebruik, koppelingen) from Object A are transferred to Object B
+- [x] [API] Object A is deleted after merge completion
+- [x] [API] No timeout errors during merge (previously "timeout of 30000ms exceeded")
+- [ ] [UI] Merge result displays readable object card titles (not UUIDs)
+- [x] [API] Merge correctly handles the "group" field
+- [x] [API] Merge functionality works correctly with imported/migrated data (not just manually created data)
+- [ ] [UI] Documentation/handleiding for performing a merge is available
+- [x] [API] The "group" field is correctly set based on the target organization's actual group name (not a generated name like "groningen_1")
+- [ ] [API] After merging two organisations, the count of koppelingen on the merged target equals the sum of koppelingen from both source organisations (no koppelingen lost during merge)
+- [ ] [API] After merging, all koppeling names on the target organisation resolve to human-readable names (not UUIDs or partial names)
+
+**Key Context from Comments:** A test merging "Groningen" into "Almere" succeeded but the "group" field was incorrectly set. Timeouts were experienced. Documentation/instructions for merging not yet available. VNG reports 2 koppelingen missing after Fortuna→Centric merge and incomplete koppeling names (2026-03-02).
+
+---
+
+### #144: Als gebruiker van de Softwarecatalogus wil ik een overzicht met zoek- en filteropties van alle organisaties die pakketten of diensten aanbieden
+
+**Labels:** Aanbod, PvE eis, Bevinding, Restpunt, Zoeken
+**Test Step:** Step 14
+
+**Summary:** As a user, I want an overview with search and filter options for all organizations that offer packages or services.
+
+**Acceptance Criteria:**
+- [x] [API] The search page (/zoeken) shows results for organizations, applications, and services
+- [x] [API] Filter facets allow filtering by organization type (gemeente, samenwerking, leverancier)
+- [ ] [UI] A "clear all filters" button resets all applied filters
+- [x] [API] Search results for applications show the supplier name (clickable)
+- [x] [API] Search results show a short description (samenvatting)
+- [ ] [UI] Search result cards display appropriate icons for applicatie, dienst, and aanbieders
+- [ ] [HYBRID] Entering a search term on the homepage and clicking search navigates to search results with the term preserved
+- [x] [API] Organization names display as readable names (not UUIDs)
+- [x] [API] Filter counts match the actual number of results
+- [x] [API] Search result cards for applications show the top 2 most frequently registered reference components by gemeenten
+- [x] [API] For logged-in gebruik-beheerder/raadpleger, search result cards show the number of gemeenten that have registered usage of the application
+- [ ] [HYBRID] Supplier name on search result cards is clickable and navigates to the supplier's detail page
+- [ ] [API] The total count of organisations with type "Leverancier" matches the expected count from the data migration source (currently ~341 on softwarecatalogus.nl)
+
+**Key Context from Comments:** Extensive testing feedback. Previously auto-triggering while typing, search terms disappearing, org names showing as UUIDs. Related: #346, #315, #74.
+
+**Testing Note (RBAC-aware result counts):** Result counts differ based on authentication status — this is by design. RBAC is group-based (Nextcloud groups). Key behaviors:
+- **Unauthenticated (public):** Only sees Applicaties where `geregistreerdDoor = Leverancier`, only sees Organisaties of type Leverancier/Gemeente/Samenwerking. Cannot see Contactpersonen, Gebruik, or Koppelingen at all. (~1,853 results)
+- **gebruik-beheerder (e.g., Maria):** Unrestricted read on Applicatie, Organisatie, Gebruik, Koppeling — sees ALL of these including municipality-registered applications. This is expected behavior, not a data leak.
+- **aanbod-beheerder (e.g., Jan):** Sees own-org Applicaties + all public ones, own-org Gebruik/Koppelingen.
+- **admin:** Bypasses ALL RBAC, sees everything (~12,645 results).
+- Testers should compare against their role's expected visibility, not against public/unauthenticated counts.
+
+---
+
+### #148: (VNGR) De GEMMA-architectuur is opvraagbaar met een API
+
+**Labels:** Referentiearchitectuur
+**Test Step:** Step 12
+
+**Summary:** The GEMMA architecture (ArchiMate model data) should be queryable via a public API, including elements, relations, views, and property definitions.
+
+**Acceptance Criteria:**
+- [x] [API] The ArchiMate API auto-generated documentation (OAS) is accessible at `/index.php/apps/openregister/api/registers/4/oas` (register 4 = GEMMA/AMEFF). **Note:** Currently returns 500 because register 4 has `organisation` set — the OAS endpoint incorrectly filters by org. This is an OpenRegister bug, not a Softwarecatalogus config issue.
+- [x] [API] The /elements endpoint returns ArchiMate elements with correct counts matching the GEMMA model
+- [x] [API] Elements include the ArchiMate-type field
+- [x] [API] Empty properties are omitted from element responses
+- [x] [API] The /relations endpoint returns relations correctly (not "bad gateway")
+- [x] [API] Relations include the ArchiMate-type field
+- [x] [API] The /views endpoint returns view definitions with correct count
+- [x] [API] The API supports a model-id query parameter for querying specific models
+- [x] [API] The /models endpoint returns a list of available models
+- [ ] [UI] ID fields (Archi id, Object ID, Open Register id) are documented
+- [ ] [UI] The GEMMA model can be downloaded via the "Gemma downloaden" button on the Mijn omgeving page
+- [ ] [HYBRID] The downloaded XML file can be successfully imported into Archi without errors
+- [ ] [UI] The imported model in Archi matches the original GEMMA model (only label placement differences expected)
+
+**Key Context from Comments:** Multiple endpoints were previously broken. The API needs to support multiple ArchiMate models. ID fields are confusing — Archi IDs, Object IDs, and Open Register IDs.
+
+---
+
+### #155: (VNGR) Definities worden weergegeven via een interactieve optie binnen de softwarecatalogus
+
+**Labels:** Organisatie en configuratie, Cms
+**Test Step:** Step 21
+
+**Summary:** Definitions/glossary terms should be displayed interactively, with hover tooltips and a searchable glossary panel.
+
+**Acceptance Criteria:**
+- [x] [API] The glossary endpoint at /apps/opencatalogi/api/glossary returns glossary terms
+- [x] [API] Terms from the current Softwarecatalogus lexicon are present
+- [ ] [UI] Pages containing glossary terms show them as interactive (clickable or hover-able)
+- [ ] [UI] Hovering/clicking a term shows its definition in a tooltip or panel
+- [ ] [UI] A glossary search panel allows searching across all defined terms
+- [x] [API] Definitions include links to external sources where appropriate
+- [x] [API] **Admin: Add term with empty external link** — Creating a glossary term without an external link succeeds (no validation error)
+- [ ] [UI] **Admin: Add term with keywords** — Keywords field shows a taggable text input, not collaborative tag UUIDs
+- [ ] [UI] **Admin: Edit existing term** — Opening an existing term shows keywords as readable text tags, not UUIDs
+- [ ] [HYBRID] Glossary term detection works on all relevant page types (CMS pages, organization pages, application pages, dienst pages, koppeling pages)
+- [x] [API] Keywords are not case-sensitive for term detection (e.g., "API" matches "api")
+- [ ] [UI] The "description" field content is shown when a term is expanded/clicked (not just the "summary")
+
+**Key Context from Comments:** The glossary was built for Dimpact with two parts: terms detected on current page, and search-in-terms. The lexicon terms were manually copied. The endpoint has been added. Fixed in opencatalogi@74e46927: NcSelectTags replaced with NcSelect for keywords, externalLink validation made optional.
+
+---
+
+### #160: (VNGR) Performance plotten views tbv ID-77
+
+**Labels:** Referentiearchitectuur
+**Test Step:** Step 22
+
+**Summary:** ArchiMate views should load with acceptable performance. Benchmark: "Poster basisbeveiligingsniveau" view (388 nodes) under 11 seconds total.
+
+**Acceptance Criteria:**
+- [ ] [UI] The largest ArchiMate view (388 nodes) loads and becomes interactive within 11 seconds on Chromium (i5/16GB)
+- [ ] [UI] Each loading phase completes in approximately 3 seconds average
+- [ ] [UI] Smaller views load in under 7 seconds
+- [ ] [UI] Views become interactive (tooltips, zoom) after rendering completes
+- [x] [API] Backend API for a single view returns data within ~0.5 seconds
+- [ ] [UI] Large views display a loading indicator
+- [ ] [UI] Acceptable performance on Chrome, Edge, and Firefox without ad-blockers
+- [x] [API] Benchmark view is specifically "Poster basisbeveiligingsniveau van referentiecomponenten" (388 nodes)
+- [ ] [UI] Warning/loading indicator shown for large views that may take a moment to fully load
+
+**Key Context from Comments:** Performance varies by browser (Firefox ~2x slower) and ad-blockers (uBlock can double render time). Benchmark: "Zeist" with 261 packages in 11 seconds. Hardware: i5, 16GB DDR4/DDR5, 512GB SSD.
+
+---
+
+### #169: Rest issues van Organisatie en Configuratie
+
+**Labels:** Organisatie en configuratie, Restpunt
+**Test Step:** Step 21
+
+**Summary:** Remaining issues from Organization and Configuration: registration form alignment, account sync, organization activation, "My Account" improvements.
+
+**Acceptance Criteria:**
+- [ ] [UI] Registration form fields align with "Mijn Account" form, including "tussenvoegsel" field
+- [ ] [UI] "Mijn Account" page shows the user's organization name (clickable link to /my-organisation)
+- [ ] [UI] "Mijn Account" does NOT show "Weergavenaam" or "E-mail geverifieerd", but DOES show "Functie"
+- [ ] [UI] KVK number from registration is displayed in "Organisatie bewerken"
+- [x] [API] After activating organization, status changes to "Actief"
+- [x] [API] After activating organization, user account is also activated
+- [ ] [UI] Consistent capitalization for form field labels
+- [x] [API] Nextcloud account data synchronized with linked contact person
+- [x] [API] No repeated "Nextcloud autorisatie - De tijd is verstreken" errors on first login
+- [x] [API] After clicking "Activeren" on an organization, the status column immediately changes from "Concept" to "Actief" (without needing to reload)
+- [ ] [UI] The registration form asks for "tussenvoegsel" (prefix), consistent with the "Mijn Account" form fields
+
+**Key Context from Comments:** Nextcloud account must sync with linked contact person. "Weergavenaam" should be removed, replaced with "Functie". Organization link to /my-organisation.
+
+---
+
+### #185: Detailpagina's
+
+**Labels:** Aanbod, Bevinding, Restpunt
+**Test Step:** Step 7
+
+**Summary:** Various UI improvements needed on detail pages: showing page type, supplier names, reorganizing tabs, fixing navigation consistency.
+
+**Acceptance Criteria:**
+- [ ] [UI] Detail page clearly shows what type of page is being viewed (Organisatie, Product, Applicatie, etc.)
+- [ ] [UI] Supplier/leverancier name is displayed on application detail pages
+- [ ] [UI] "Standaarden" shown in its own tab
+- [ ] [UI] "Geschikt voor" shown in its own tab
+- [ ] [UI] Tab previously labeled "Producten" is renamed to "Onderdeel van product(en)"
+- [ ] [UI] Both "beschrijving kort" and "beschrijving lang" are displayed
+- [ ] [UI] URL and breadcrumb navigation are consistent regardless of navigation path
+- [ ] [UI] Left menu "Applicaties" is highlighted when viewing an application detail page
+
+**Key Context from Comments:** Discrepancy found: search navigation produces /beheer/module/{id} while Applicaties navigation produces /beheer/applicaties/{id}. Different layout between search and management side is by design (screen size).
+
+---
+
+### #186: Koppelingen
+
+**Labels:** Aanbod, Bevinding, Restpunt, Koppeling
+**Test Step:** Step 11
+
+**Summary:** Multiple bugs in the "Koppelingen" (connections) feature: displaying titles, linked external services, handling non-existent applications, detail pages.
+
+**Acceptance Criteria:**
+- [x] [API] Koppelingen display in a table format with readable titles (not blank or UUID-only)
+- [x] [API] Koppelingen linked to "buitengemeentelijke voorzieningen" correctly display the referenced external service
+- [x] [API] Koppelingen do not reference non-existent applications (graceful handling)
+- [ ] [UI] Detail page shows all relevant fields: name, type, transport protocol, linked applications, external service
+- [x] [API] Koppeling detail page at /publicatie/{uuid} renders correctly
+
+**Key Context from Comments:** All items in the body checklist are marked resolved ([x]) but the issue remains open, suggesting verification is needed.
+
+**Testing Note (data quality):** UUID-only titles, "null" references, and arrow-only names in older koppelingen are caused by **bad client data** (koppelingen referencing applications that were deleted or never existed). This is not a code bug. Testers should focus on newly created koppelingen to verify the display logic is correct, and ignore legacy koppelingen with broken references.
+
+---
+
+### #225: Testresultaten 29-10-2025
+
+**Status: CLOSED (2026-03-04)**
+
+**Labels:** Aanbod, Bevinding, Restpunt
+**Test Step:** General
+
+**Summary:** Collection of test findings from October 29, 2025: RBAC/permission issues, search visibility, organization discoverability, confusing cross-organization actions.
+
+**Acceptance Criteria:**
+- [x] [API] A newly registered and activated organization is findable via the search engine
+- [ ] [UI] The blue "+" button for adding products is NOT shown on other organizations' public pages (or clearly adds to own organization)
+- [ ] [UI] A logged-in aanbod-beheerder can see their own products under "Producten" and "Applicaties"
+- [ ] [HYBRID] A logged-in aanbod-beheerder can edit their own applications from the search page
+- [ ] [HYBRID] Organization's published status is accurately reflected in backend and frontend
+- [ ] [UI] URL fields for standards do not require "https://" prefix
+- [ ] [UI] On another organization's public page, the blue "+" button either does not appear OR is clearly labeled to indicate it adds to the current user's own organization
+
+**Key Context from Comments:** Many issues expected to be resolved by RBAC replacing the "published" status approach. The organization "Baron" created during testing was not findable via search.
+
+---
+
+### #248: Titels van de tabs in orde maken
+
+**Labels:** Aanbod, Afschalen Producten
+**Test Step:** Step 7
+
+**Summary:** Not all tabs on detail pages have proper text titles. Some tabs only show an icon without text.
+
+**Acceptance Criteria:**
+- [ ] [UI] ALL tabs on application/product detail pages have a visible text label (not just an icon)
+- [ ] [UI] The "Overige" tab displays both its icon AND the text label "Overige"
+- [ ] [UI] Tab labels are consistent across all detail page types
+- [ ] [UI] Tab labels are accessible for screen readers
+
+**Key Context from Comments:** Retesting on both test and accept environments was marked OK by WilcoLouwerse. Issue remains open despite retests being OK.
+
+---
+
+### #263: Niet ingelogd: onder een applicatie staat in het tabje gebruik de gemeenten
+
+**Labels:** Organisatie en configuratie, Afschalen Producten
+**Test Step:** Step 7
+
+**Summary:** When not logged in, the "Gebruik" tab shows which municipalities use the application. This should not be visible to unauthenticated users.
+
+**Acceptance Criteria:**
+- [ ] [UI] When NOT logged in, the "Gebruik" tab is NOT visible or does not show municipality usage data
+- [ ] [UI] When logged in as an authorized user, the "Gebruik" tab IS visible with correct data
+- [ ] [UI] Verify on both test and accept environments
+
+**Key Context from Comments:** Retesting on test environment confirmed "the usage tab is not visible when not logged in" (expected behavior). Issue remains open pending final sign-off.
+
+---
+
+### #266: Na inloggen: Mijn account & persoonlijke gegevens leeg?
+
+**Labels:** Organisatie en configuratie, Afschalen Producten, Bevinding
+**Test Step:** Step 4
+
+**Summary:** After logging in, "Mijn account" page and personal details are empty, possibly caused by incorrect data synchronization between contact person and Nextcloud account.
+
+**Acceptance Criteria:**
+- [ ] [UI] After logging in, "Mijn account" displays personal information (name, email, function, organization)
+- [x] [API] "Persoonlijke gegevens" section is populated from the linked contact person object
+- [x] [API] When a contact person is converted to a Nextcloud account, data is correctly transferred
+- [x] [API] The "me" endpoint returns correct user data including organization
+- [ ] [UI] No delay beyond a few seconds between login and data appearing
+- [x] [API] The `/api/me` endpoint returns correct personal data (name, email, function) and organization for the logged-in user
+
+**Key Context from Comments:** Root cause: converting contact person to Nextcloud account doesn't correctly transfer data. Reproduced on both test and accept environments.
+
+---
+
+### #267: Naam is softwarecatalogus i.p.v. Softwarecatalogus
+
+**Labels:** Organisatie en configuratie, Afschalen Producten, Tekstuele wijzigingen
+**Test Step:** Step 21
+
+**Summary:** The application name displays as "softwarecatalogus" or "Development Catalogus" instead of "Softwarecatalogus".
+
+**Acceptance Criteria:**
+- [ ] [UI] Browser tab, header, and homepage read "Softwarecatalogus"
+- [ ] [UI] The name is consistent across all pages (header, footer, login, registration)
+- [ ] [UI] Verified on both test and accept environments
+
+**Key Context from Comments:** As of February 4, 2026, tester confirmed name is still incorrect. This is a configuration/text change.
+
+---
+
+### #274: Wizard dienst: tekst dient nog aangepast te worden naar nieuwe benamingen
+
+**Labels:** Aanbod, Afschalen Producten, Tekstuele wijzigingen
+**Test Step:** Step 9
+
+**Summary:** The service wizard still uses old terminology that needs updating to the new naming conventions.
+
+**Acceptance Criteria:**
+- [ ] [UI] Service wizard uses updated terminology consistent with the rest of the application (no "product" references where "applicatie" should be used)
+- [ ] [UI] All form labels, button texts, help texts, and confirmation messages use new naming
+- [ ] [UI] Wizard title and step descriptions match current agreed terminology
+- [ ] [UI] No references to deprecated terms remain in the dienst wizard flow
+- [ ] [UI] Wizard text matches the diensten wizard texts from #187 (section 9: "Dienst registreren")
+- [ ] [UI] **Image comparison**: Fetch the reference screenshot from the issue (`https://github.com/user-attachments/assets/77eb9be8-27d0-4c19-bac0-cf82eb12b343`) showing the old terminology, then verify in the live UI whether the terms have been updated
+
+**Key Context from Comments:** Moved from "Afschalen Producten" to "Gebruik" scope. Texts need review after the product-to-application rename. Related to #187 (Tekstvoorstellen) for the exact replacement text.
+
+---
+
+### #278: Filterteksten aanpassen
+
+**Labels:** Aanbod, Afschalen Producten, Tekstuele wijzigingen, Zoeken
+**Test Step:** Step 14
+
+**Summary:** Filter texts on the search page need adjustment. Suspected caching issue affecting display.
+
+**Acceptance Criteria:**
+- [ ] [UI] Filter labels on /zoeken display correct, updated text
+- [x] [API] Updated texts appear without stale cached content
+- [ ] [UI] Filter texts are consistent with terminology used in wizards and management pages
+- [ ] [UI] Filter currently labeled "Schema" or "Objecttype" is renamed to "Type" (or agreed alternative)
+- [ ] [UI] Documentation is available explaining how VNG can manage filter texts
+
+**Key Context from Comments:** A caching issue was noted. Documentation on how VNG can manage filter texts still needs to be written.
+
+---
+
+### #280: Zoeken: sorteren gaat niet goed.
+
+**Labels:** Aanbod, Afschalen Producten, Zoeken
+**Test Step:** Step 14
+
+**Summary:** Sorting on search results page does not work correctly. Also, the "Type" filter is missing.
+
+**Acceptance Criteria:**
+- [x] [UI] Clicking a sort option (e.g., "Naam A-Z") correctly reorders results
+- [x] [API] Sorting applies across ALL pages (full-dataset sorting, not just current page)
+- [x] [HYBRID] Changing sort order after a text search correctly re-sorts results
+- [x] [UI] A "Type" filter is available in the search filters
+- [x] [HYBRID] Sort order is maintained when navigating between pages
+
+**Key Context from Comments:** As of Feb 3, sorting issues still not resolved and "Type" filter still missing. Expectation is full-dataset sorting.
+
+**Resolution (2026-03-01):** Opgelost. Alle 5 sorteeropties (Naam A-Z/Z-A, Datum nieuw/oud, Relevantie) werken correct server-side over 12.666 items. "Type" facet beschikbaar als filter. Zie [reactie 280](reacties/280.md).
+
+---
+
+### #286: Aanmelden organisatie: 500-error bij wachtwoord wijzigen
+
+**Labels:** Organisatie en configuratie, Afschalen Producten
+**Test Step:** Step 4
+
+**Summary:** 500 server error when changing password during organization registration.
+
+**Acceptance Criteria:**
+- [x] [API] Changing the account password during registration/first login completes without errors
+- [x] [API] After changing password, user can log in with new password
+- [ ] [UI] Password change form provides appropriate validation feedback
+- [x] [API] Server responds with success status code (2xx)
+
+**Key Context from Comments:** Confirmed OK on both test and accept environments by WilcoLouwerse. Should be re-verified.
+
+---
+
+### #294: Applicatie publiceren: uitlijning rechthoek om op te voeren.
+
+**Labels:** Aanbod, Afschalen Producten, Bevinding, Wizard
+**Test Step:** Step 7
+
+**Summary:** Alignment of input rectangle/box is incorrect in the application publishing wizard when a reference component is selected.
+
+**Acceptance Criteria:**
+- [ ] [UI] When NO reference component is selected, fields are properly aligned
+- [ ] [UI] When a reference component IS selected, fields remain properly aligned (no overlap)
+- [ ] [UI] Layout does not break when toggling between having/not having a reference component
+- [ ] [UI] Alignment is consistent across different screen sizes
+
+**Key Context from Comments:** Re-opened after being previously marked OK. Alignment breaks specifically when a reference component IS selected (confirmed Feb 4).
+
+---
+
+### #300: Beheer: overzicht applicaties teveel applicaties
+
+**Labels:** Aanbod, Afschalen Producten, Bevinding, Beheer
+**Test Step:** Step 7
+
+**Summary:** Management overview shows too many applications — count doesn't match organization's actual applications.
+
+**Acceptance Criteria:**
+- [x] [API] On /beheer/applicaties, number of applications matches the logged-in organization's actual count
+- [x] [API] Count is correct for newly created suppliers
+- [x] [API] Count is correct for imported/migrated suppliers (e.g., Centric)
+- [x] [API] Applications from other organizations are NOT shown
+- [x] [API] Spot-check 5 imported suppliers against import CSV data
+
+**Key Context from Comments:** Count correct for "new" suppliers, not yet verified for imported ones. Need to pick 5 from import files including Centric.
+
+---
+
+### #302: Beheer: applicatie bewerken (ophalen van gegevens is traag)
+
+**Labels:** Aanbod, Afschalen Producten, Bevinding, Beheer
+**Test Step:** Step 7
+
+**Summary:** Loading application data for editing in the management section is slow.
+
+**Acceptance Criteria:**
+- [ ] [UI] Clicking "edit" on an application loads the form within 3 seconds
+- [ ] [UI] Loading indicator is shown while data is fetching
+- [x] [API] All fields are correctly populated when loading completes
+- [ ] [UI] Performance is acceptable on the production/performance environment
+
+**Key Context from Comments:** Need to verify on the "b omgeving performance" environment.
+
+---
+
+### #306: Dienst: Overzicht controleren verbeteren
+
+**Labels:** help wanted, Aanbod, Afschalen Producten, Bevinding, Wizard, Wijziging
+**Test Step:** Step 9
+
+**Summary:** Service overview needs improvements: duplicate "Type"/"Diensttype" labels, redundant "Relaties" section, hidden properties still showing.
+
+**Acceptance Criteria:**
+- [ ] [UI] No duplicate between "Type" and "Diensttype" — only "Diensttype" shown
+- [ ] [UI] "Relaties" section showing the provider is hidden (already in provider context)
+- [x] [API] Properties configured as "not displayed" are NOT shown
+- [ ] [UI] "dienstType" field properly descaled
+- [ ] [UI] The "Relaties" section on the dienst overview page is hidden (provider context is already clear)
+
+**Key Context from Comments:** Confirmed Feb 20: "de dubbelingen zijn verwijderd, dienstType is afgeschaald."
+
+---
+
+### #307: Diensten overzicht: meer dienst bij organisatie dan er horen
+
+**Labels:** Aanbod, Afschalen Producten, Bevinding, Beheer, Wijziging
+**Test Step:** Step 9
+
+**Summary:** Services overview shows more services than belong to the organization. "Koppelingen" column shouldn't be selectable.
+
+**Acceptance Criteria:**
+- [x] [API] Only services belonging to the current organization are displayed
+- [x] [API] Number of services matches expected count
+- [ ] [UI] "Koppelingen" column is NOT available in the column picker
+- [x] [API] Data model configuration excludes "koppelingen" from services table
+
+**Key Context from Comments:** As of Feb 4, "koppelingen" column can still be selected. Should be hidden since connections can't be filled via service wizard.
+
+---
+
+### #308: Diensten overzicht: default kolommen + kolom verwijderen
+
+**Labels:** Aanbod, Afschalen Producten, Bevinding, Beheer, Wijziging
+**Test Step:** Step 9
+
+**Summary:** Services overview needs correct default columns, "koppelingen" column should be removed.
+
+**Acceptance Criteria:**
+- [ ] [UI] Default columns are relevant and useful (without "koppelingen")
+- [ ] [UI] "Koppelingen" column is NOT selectable
+- [ ] [UI] Default columns shown are: name, type, status (or similar relevant set)
+- [ ] [HYBRID] Column configuration persists after page reload
+
+**Key Context from Comments:** "Koppelingen" should be hidden since it can't be filled. Property definitions need adjusting.
+
+---
+
+### #312: Koppeling heeft verplicht een naam
+
+**Labels:** Aanbod, Afschalen Producten, Restpunt, Datamigratie
+**Test Step:** Step 11
+
+**Summary:** Connections must have a mandatory name. Some appear as UUIDs or "Geen titel". Name should auto-generate from connected application names.
+
+**Acceptance Criteria:**
+- [ ] [UI] Name field is required when creating a new connection
+- [ ] [UI] Name is pre-filled with "[Application A] [arrow] [Application B]" format
+- [x] [API] Imported connections without a name receive the auto-generated default
+- [x] [API] Imported connections with existing names retain them
+- [x] [API] Newly created connections show readable names (not UUIDs) in search results
+- [ ] [UI] Koppeling card highlights "Application A" explicitly in the card display
+- [ ] [UI] The full connection description (A -> B) is shown in the description field of the card
+
+**Note:** UUIDs in koppeling names are expected when one side references an application that no longer exists or was deleted. The name resolution uses application UUIDs as fallback when the referenced object cannot be found. This is not a bug — only koppelingen where both applications exist should show fully resolved names.
+
+**Key Context from Comments:** Two test cases: creating connections and importing connections. Both must be verified.
+
+**VNG Manual Test (2026-02-25) — FAIL:** "In de wizard krijgt een koppeling nu geen default naam." VNG proposes: koppeling should always get a default name automatically and the name field should not be user-editable. Current UI looks "dubbelop" (redundant).
+
+**Additional Acceptance Criteria (from VNG feedback):**
+- [ ] [UI] In the wizard, a koppeling automatically receives a default name (format: "[App A] → [App B]")
+- [ ] [UI] The name field is NOT editable by the user — always auto-generated
+- [ ] [HYBRID] The auto-generated name updates when Application A or B selection changes
+
+**Additional Acceptance Criteria (from 2026-03-04 feedback):**
+- [ ] [UI] Koppeling search result cards show the application names in the title (not blank or UUID)
+- [ ] [UI] Koppeling card description shows the connected application names clearly
+
+---
+
+### #314: Wizard Koppeling publiceren vind zelf aangemaakte applicaties niet
+
+**Labels:** Aanbod, Bevinding, Koppeling
+**Test Step:** Step 11
+
+**Summary:** In the connection wizard, a supplier cannot find their own applications in the list. Shows applications from other suppliers instead.
+
+**Acceptance Criteria:**
+- [x] [API] In the connection wizard (/forms/koppeling), suppliers can find and select their own applications
+- [x] [API] Searching by name returns the correct result
+- [x] [API] Suppliers can only create connections where Application A is their own
+- [x] [API] Municipalities can create connections for applications in their landscape
+- [x] [API] Application list does not show unrelated organizations' applications
+- [x] [API] Suppliers can ONLY set their own application as "Application A" in the connection wizard
+
+**Key Context from Comments:** Supplier flow works on B environment. Municipality/collaboration flow (users creating connections for their landscape) does not yet work.
+
+---
+
+### #315: Hoge prioriteit: Zoekpagina toont deel van gemeentelijk applicatielandschap. Dit is géén publieke informatie
+
+**Status: CLOSED (2026-03-04)**
+
+**Labels:** Aanbod, Afschalen Producten, Datamigratie, Zoeken
+**Test Step:** Step 14
+
+**Summary:** HIGH PRIORITY: Search page incorrectly shows municipalities as "suppliers" and displays private municipal application landscapes.
+
+**Acceptance Criteria:**
+- [x] [API] "Leverancier" filter on /zoeken contains ONLY actual suppliers, NOT municipalities
+- [x] [API] Search result cards show the actual supplier as "aangeboden door", NOT a municipality
+- [ ] [UI] Filtering by municipality name is not possible
+- [x] [API] Application detail page shows the correct supplier
+- [x] [API] Municipal application landscape data is not publicly visible to unauthenticated users
+- [x] [API] Supplier on search card matches supplier on detail page
+- [x] [API] RBAC-based filtering replaces the old "published" status approach for controlling visibility
+- [x] [API] Import data no longer contains `@self.published` column (using RBAC instead)
+
+**Key Context from Comments:** Root cause: municipalities were set as suppliers in import data. Fix involves RBAC and removing `@self.published` from import data. Test environment OK but "depubliceren" doesn't work.
+
+---
+
+### #332: Voorpagina inrichten
+
+**Labels:** help wanted, Organisatie en configuratie, Cms
+**Test Step:** Step 21
+
+**Summary:** Home page needs configurable content: logo, menu bar, search window with banner, quote section, three content blocks, text+image section, footer.
+
+**Acceptance Criteria:**
+- [ ] [UI] Home page displays a logo linking to home
+- [ ] [UI] Top-right shows dashboard and logout links (when logged in)
+- [ ] [UI] Menu bar contains "Home" + configurable additional items
+- [ ] [UI] When logged in, user's name and organization appear in menu bar
+- [x] [API] Search window performs a search respecting user's permissions
+- [ ] [UI] Banner behind search is configurable by functional admin
+- [ ] [UI] Quote section (bold text with subtitle) is present and editable
+- [ ] [UI] 3 content blocks with icon, title, text, and link, all equal dimensions
+- [ ] [UI] Text section with title, text, link, and image is configurable
+- [ ] [UI] Footer is configurable
+- [ ] [UI] Functional administrators can edit all configurable sections
+- [ ] [UI] VNG functional administrators can independently edit all home page content without developer intervention
+- [ ] [UI] CMS documentation/instructions available at the Softwarecatalogus docs site
+
+**Key Context from Comments:** Delivery should allow VNG to customize content. Duplicates with #397.
+
+---
+
+### #340: Bevindingen op tussenoplevering Zoeken
+
+**Labels:** Zoeken, Wijziging
+**Test Step:** Step 14
+
+**Summary:** Collection of findings from search interim delivery: performance, sorting, missing filters, text changes.
+
+**Acceptance Criteria:**
+- [ ] [UI] Search filters load within 3 seconds (not 7-10 seconds)
+- [ ] [UI] Text search results appear within 2 seconds, filters update within 3 seconds
+- [x] [API] Default sorting is "Naam - A naar Z" (client confirmed this is correct in meeting)
+- [ ] [HYBRID] Sorting after text search actually reorders results
+- [ ] [UI] A date is visible on cards, using "Eerste registratie" (@self.created) date
+- [ ] [UI] "Meest relevant" has a tooltip or explanation
+- [ ] [UI] A "Type" filter is present (replacing removed "Schema" filter)
+- [ ] [UI] Active filter indicator remains visible when text search is performed
+- [ ] [UI] "Soort dienst" renamed to "Diensttype"
+
+**Key Context from Comments:** Most fixes live on performance.accept.opencatalogi.nl/zoeken except "Soort dienst" rename. Date on cards uses @self.created.
+
+---
+
+### #343: Zoeken: Filter 'Type koppeling' toevoegen.
+
+**Labels:** Aanbod, Gebruik, Zoeken, Koppeling, Wijziging
+**Test Step:** Step 14
+
+**Summary:** "Type koppeling" filter needed on search page with "extern" and "intern" values.
+
+**Acceptance Criteria:**
+- [ ] [UI] On /zoeken, a "Type koppeling" filter is available for connections
+- [ ] [UI] Filter has exactly two options: "extern" and "intern"
+- [x] [API] "extern" filters to external connections only
+- [x] [API] "intern" filters to internal connections only
+- [x] [API] Filter reflects the `koppelingType` attribute
+- [x] [API] Filter visible only to logged-in users (RBAC on connections)
+
+**Key Context from Comments:** "koppelingType" is derived: external = connection with external app. RBAC: must be logged in to see connections. Gebruik-beheerder sees all; aanbod-beheerder only their own.
+
+---
+
+### #344: Zoeken: Geen resultaten bij het selecteren van het Gravenbeheercomponent. Niet ingelogd.
+
+**Labels:** Aanbod, Wijziging
+**Test Step:** Step 14
+
+**Summary:** Filtering by "Gravenbeheercomponent" reference component returns zero results despite applications existing.
+
+**Acceptance Criteria:**
+- [x] [API] Filtering by "Gravenbeheercomponent" returns matching applications
+- [x] [API] The Type/schema filter is active and working
+- [x] [API] This works for unauthenticated users
+- [x] [API] Other reference component filters also return correct results
+
+**Key Context from Comments:** Root cause: schema filter was disabled. Re-enabled on B environment.
+
+---
+
+### #345: Zoeken: toegevoegde dienst verschijnt niet in filters
+
+**Labels:** Aanbod, Zoeken
+**Test Step:** Step 14
+
+**Summary:** Newly added service doesn't appear in search filters. "Diensttype" filter values not populated. Unknown "eigen-organisatie" value appears.
+
+**Acceptance Criteria:**
+- [x] [API] After adding a new service, it appears in search results
+- [x] [API] "Diensttype" filter is populated with correct service type values
+- [ ] [UI] "Type=Dienst" is available as a filter option
+- [ ] [UI] No test configuration values like "eigen-organisatie" appear in production
+- [x] [API] Filtering by "Dienst" shows only services
+
+**Key Context from Comments:** "eigen-organisatie" was a test config value accidentally deployed. Service discoverability fixed on B environment.
+
+---
+
+### #346: Zoeken: paginering werkt niet
+
+**Labels:** Aanbod, Zoeken
+**Test Step:** Step 14
+
+**Summary:** Pagination doesn't work — same results shown on every page when filtering (e.g., WCAG standard, 78 results).
+
+**Acceptance Criteria:**
+- [x] [API] Navigating to page 2 shows DIFFERENT results than page 1
+- [x] [API] Pages 1, 2, 3, 4 each show unique, non-overlapping results
+- [x] [API] Pagination works with filters applied
+- [x] [API] Pagination works with different sort orders
+- [x] [API] Total result count matches sum across all pages
+- [ ] [UI] Page indicator reflects current page number
+
+**Key Context from Comments:** Genuine bug introduced during performance refactor. Fixed on B environment. Needs verification on accept.
+
+---
+
+### #347: Zoeken: Dienstkaartje toont array
+
+**Labels:** Aanbod, Wijziging
+**Test Step:** Step 14
+
+**Summary:** Service cards show raw array (e.g., `["type1", "type2"]`) instead of readable text. "Concept" status is unclear.
+
+**Acceptance Criteria:**
+- [ ] [UI] Service types displayed as readable comma-separated list (NOT raw JSON array)
+- [x] [API] Service type values are human-readable labels
+- [ ] [UI] "Concept" status either has a tooltip or is replaced with a clearer term
+- [ ] [UI] Service card layout is consistent with application cards
+
+**Key Context from Comments:** Graphical bug fixed on B environment. "Concept" status comes from data model.
+
+---
+
+### #348: Het aantal standaarden komen niet overeen bij Centric Begraven tussen de huidige softwarecatalogus en de nieuwe
+
+**Labels:** Aanbod
+**Test Step:** Step 7
+
+**Summary:** Standards count for "Centric Begraven" differs between old and new softwarecatalogus.
+
+**Acceptance Criteria:**
+- [x] [API] "Centric Begraven" shows same number of standards as old softwarecatalogus
+- [x] [API] Standards list is complete and matches imported data
+- [x] [API] Compare 3-5 other applications to check for systemic issues
+- [x] [API] Standards visible to unauthenticated users
+- [ ] [HYBRID] Display bug (identified as root cause) is fixed
+
+**Key Context from Comments:** Turned out to be a display bug, not data migration issue. Sample checks on other apps looked correct.
+
+---
+
+### #349: Zoeken: UUID's onder standaarden filter.
+
+**Labels:** Referentiearchitectuur, Aanbod
+**Test Step:** Step 14
+
+**Summary:** Standards filter displays raw UUIDs instead of human-readable names. Some apps reference non-existent standard version UUID.
+
+**Acceptance Criteria:**
+- [ ] [UI] Standards filter shows human-readable names (no UUIDs or "id-" prefixed entries)
+- [ ] [UI] Sorting of filter list is alphabetical
+- [ ] [UI] Apps referencing non-existent UUID handle it gracefully
+- [ ] [UI] Reference components filter also contains no UUID entries
+- [ ] [API] The facet/filter response for standaardversies returns human-readable standard version names, not raw UUIDs
+
+**Key Context from Comments:** No longer occurring on B environment for reference components. 4 apps (Geoboxx, Gouw6 BAG, GT-BAG, UDS BAG) still reference non-existent standard version UUID from older GEMMA version. VNG reports UUIDs still visible under standaardversies (2026-03-02).
+
+---
+
+### #350: De link achter de gebruikersnaam laten verwijzen naar Mij account
+
+**Labels:** Aanbod, Wijziging
+**Test Step:** Step 4
+
+**Summary:** Username link in navigation should point to "My Account" instead of dashboard.
+
+**Acceptance Criteria:**
+- [ ] [UI] Clicking username in top navigation navigates to "Mijn account" page
+- [ ] [UI] Separate dashboard link still navigates to dashboard
+- [ ] [UI] Username is displayed correctly
+
+**Key Context from Comments:** Agreed as a good change but planned for after "afschalen producten" is complete.
+
+**Testing Note:** This issue was not replicatable in manual testing. The "Beheer" link URL inconsistency reported by the test agent may be caused by MCP browser session state or navigation timing. Leave as-is until confirmed by a human tester.
+
+---
+
+### #351: Het laden van de tabbladen gaat ongelijk
+
+**Labels:** Aanbod
+**Test Step:** Step 7
+
+**Summary:** Application detail page tabs load at noticeably different speeds, causing a jarring experience.
+
+**Acceptance Criteria:**
+- [ ] [UI] All tabs load within 1 second of page load (no staggered loading)
+- [ ] [UI] If tabs can't load simultaneously, loading indicators are shown
+- [ ] [UI] Performance meets acceptable thresholds on test environment
+
+**Key Context from Comments:** Attributed to configuration error on accept environment. On B environment, tabs load within half a second.
+
+---
+
+### #352: Mijn account - Contactpersoon bij applicatie publiceren is niet veranderd ondanks aanpassing zojuist.
+
+**Labels:** Aanbod
+**Test Step:** Step 5
+
+**Summary:** After editing account info (name, prefix), changes are not reflected in the contact person shown during application publishing. Nextcloud account updates but contact person object doesn't.
+
+**Acceptance Criteria:**
+- [x] [HYBRID] Updated first name, last name, or prefix on "Mijn Account" is immediately reflected in contact person on application forms
+- [x] [UI] Contact person name in listings matches updated account info
+- [x] [HYBRID] No cache clearing needed for updated name to appear
+- [x] [UI] Prefix (tussenvoegsel) displayed correctly between first and last name
+
+**Key Context from Comments:** Root cause: updating Nextcloud user doesn't update contact person object. Fix deployed to B environment. Root cause for #353, #356, #364, #367.
+
+**Resolution (2026-03-01):** Opgelost. Contactpersoon-lookup gescoped op organisatie (`_multitenancy: true`) zodat het juiste record wordt gevonden en bijgewerkt. Zie [reactie 352](reacties/352.md).
+
+---
+
+### #353: Mijn account – Je "functie" wordt niet aangepast na bewerken en opslaan. Cache legen werkt ook niet
+
+**Labels:** Aanbod
+**Test Step:** Step 6
+
+**Summary:** After saving "functie" (job title) on My Account, the change is not reflected anywhere. Cache clearing doesn't help.
+
+**Acceptance Criteria:**
+- [x] [API] Editing "functie" on "Mijn Account" and saving immediately shows the update
+- [ ] [UI] Updated function reflected everywhere the contact person's function appears
+- [ ] [HYBRID] No cache clearing needed
+
+**Key Context from Comments:** Same root cause as #352 — contact person object not updated when Nextcloud account is modified.
+
+---
+
+### #354: Diensten - incomplete lijst applicaties
+
+**Labels:** Aanbod, Wijziging
+**Test Step:** Step 9
+
+**Summary:** Application dropdown in service wizard shows incomplete/random subset instead of full list. Should use search-based selection like reference components.
+
+**Acceptance Criteria:**
+- [x] [API] Application selection allows searching through ALL available applications
+- [ ] [UI] Uses a searchable dropdown (like reference components selector)
+- [x] [API] All applications in the system are findable and selectable
+- [x] [API] Selected application correctly saved after form submission
+- [x] [API] Dropdown haalt minimaal 40 items op per zoekopdracht (verhoogd van 20 naar 40 in alle wizard-dropdowns)
+
+**Key Context from Comments:** Dropdown works and supports searching, but UX can be improved. Planned for after "afschalen producten."
+
+**VNG Manual Test (2026-02-25) — FAIL:** "Het zoeken met de dropdown naar applicaties gaat zowel in de Diensten als bij het zoeken naar applicatieB van koppelingen onvoorspelbaar en onduidelijk." Searching for "chap" returns nothing; searching for "chap1" shows chap1; then searching "chap" again shows chap1 but not chap2prem. Conclusion: you can only find what you're looking for if you know the exact name.
+
+**Additional Acceptance Criteria (from VNG feedback):**
+- [x] [API] Searching for a partial name (e.g., "chap") returns ALL applications containing that string (including "chap1", "chap2prem")
+- [x] [API] Search is consistent: repeating the same query returns the same results
+- [ ] [HYBRID] Search works identically in Diensten wizard and Koppeling wizard (applicatieB field)
+- [x] [API] Partial/substring matches are supported (not just prefix matches)
+
+**Mitigatie (2026-03-01):** `_limit` verhoogd van 20 naar 40 in 12 plekken over 7 bestanden. Dit verbetert de dropdown-bruikbaarheid maar lost het onderliggende zoekprobleem niet volledig op. **Grafisch effect:** dropdowns tonen nu meer items, wat langer laden en grotere lijsten oplevert. Zie [reactie 354](reacties/354.md).
+
+---
+
+### #355: Diensten: Export geeft allerlei UUID's
+
+**Labels:** Aanbod, Wijziging
+**Test Step:** Step 13
+
+**Summary:** Services export contains raw UUIDs instead of human-readable text.
+
+**Acceptance Criteria:**
+- [x] [API] CSV export shows human-readable names for all reference fields
+- [x] [API] Export combines readable text with UUIDs for re-import compatibility
+- [x] [API] Application names, contact persons, service types exported with display names
+- [x] [API] Export can be re-imported without data loss
+
+**Key Context from Comments:** Confirmed Feb 19: "Export has been adjusted as requested." Duplicates #15.
+
+---
+
+### #356: Diensten: geen tussenvoegsel bij namen
+
+**Labels:** Aanbod
+**Test Step:** Step 9
+
+**Summary:** Contact person column in services overview doesn't show prefix/tussenvoegsel.
+
+**Acceptance Criteria:**
+- [ ] [UI] "Contactpersoon" column shows full name including prefix (e.g., "Peter de Steam")
+- [ ] [UI] Prefix displayed consistently across all views
+
+**Key Context from Comments:** Same root cause as #352.
+
+---
+
+### #357: Diensten: Diensttype en Type wordt door elkaar gebruikt
+
+**Labels:** Aanbod
+**Test Step:** Step 9
+
+**Summary:** "Diensttype" and "Type" used inconsistently. "eigen-organisatie" should be hidden.
+
+**Acceptance Criteria:**
+- [ ] [UI] "Diensttype" used consistently (not "Type")
+- [ ] [UI] "Type" column renamed to "Diensttype" or removed if technical
+- [ ] [UI] "eigen-organisatie" not shown to end users
+- [ ] [UI] Facets/filters use correct data model terminology
+- [ ] [UI] "Soort dienst" does NOT appear as a separate term or column — it is a duplication of "Diensttype" and should be removed or consolidated
+
+**Key Context from Comments:** Configuration error. Confirmed Feb 20: all "...Type" entries removed, faceting refactored.
+
+---
+
+### #358: Diensten: De status "Concept" wordt nog op verschillende plekken getoond
+
+**Labels:** Aanbod, Wijziging
+**Test Step:** Step 9
+
+**Summary:** "Concept" status appears in multiple places but was not in the requirements. Should be removed.
+
+**Acceptance Criteria:**
+- [ ] [UI] "Concept" not in services wizard
+- [x] [API] "Concept" not in search results
+- [ ] [UI] "Concept" not in services overview table
+- [ ] [UI] "Concept" not available as filter option
+- [ ] [UI] "Status" field for services doesn't contain "Concept"
+
+**Key Context from Comments:** Not in PvE. Removed from backend data model. Frontend also needs removal.
+
+---
+
+### #359: Diensten wizard: Uw dienst publiceren - tekst aanpassen
+
+**Labels:** Aanbod, Tekstuele wijzigingen
+**Test Step:** Step 9
+
+**Summary:** Information tooltip text in the "Uw dienst publiceren" wizard (step: searching for applications) doesn't match the approved PowerPoint text.
+
+**Acceptance Criteria:**
+- [ ] [UI] Tooltip text (behind "i" icon) when searching for Applicaties matches the PowerPoint reference
+- [ ] [UI] **Image comparison**: Fetch the reference screenshot from the issue (WilcoLouwerse comment image: `https://github.com/user-attachments/assets/822dce30-9bd2-4104-9509-c547a69d7d4c`) showing the expected text, then compare with the live UI
+- [ ] [UI] Text is consistent with the diensten wizard texts defined in #316 through #328
+- [x] [API] Text preserved across environment resets (stored in configuration, not overwritten on reset)
+
+**Key Context from Comments:** Texts were updated but not stored in config, so they were overwritten on environment reset. Issues #316-#328 are the source of truth for the "toevoegen" wizard. This issue is about the "publiceren" wizard text.
+
+---
+
+### #360: Diensten wizard – Uw dienst publiceren: Meerdere i komen niet overeen met ppt
+
+**Labels:** Aanbod, Tekstuele wijzigingen
+**Test Step:** Step 9
+
+**Summary:** ALL information tooltips ("i" icons) in step 2 of the diensten "publiceren" wizard (entering service data) don't match the approved PowerPoint.
+
+**Acceptance Criteria:**
+- [ ] [UI] ALL tooltips ("i" icons) in step 2 of the diensten wizard match the PowerPoint reference
+- [ ] [UI] **Image comparison**: Fetch the reference screenshot from WilcoLouwerse's comment (`https://github.com/user-attachments/assets/359e9ec2-9026-4ac3-b937-593a393899c3`) showing the expected text, then compare each tooltip with the live UI
+- [ ] [UI] Each field label's tooltip text matches the PowerPoint specification
+- [ ] [UI] No tooltip is missing (all fields that should have an "i" icon do have one)
+- [x] [API] Changes stored in configuration for persistence
+
+**Key Context from Comments:** Related to #359. Being tracked together with similar textual issues.
+
+---
+
+### #361: Diensten wizard – Uw dienst publiceren: inconsistentie in labels
+
+**Labels:** Aanbod, Tekstuele wijzigingen
+**Test Step:** Step 9
+
+**Summary:** Labels on review/confirmation form don't match input field labels.
+
+**Acceptance Criteria:**
+- [ ] [UI] Review form labels exactly match input field labels
+- [ ] [UI] All labels follow naming conventions from approved PowerPoint
+- [ ] [UI] No discrepancies between data entry and summary/review steps
+
+**Key Context from Comments:** Not a regression — texts present since wizard was first accepted but not yet aligned with latest PowerPoint.
+
+---
+
+### #362: Diensten wizard – Uw dienst publiceren: onlogische tekst bovenaan de aanmeld-stap
+
+**Labels:** Aanbod
+**Test Step:** Step 9
+
+**Summary:** On the "Service successfully registered" page, header still shows "Uw diensten publiceren" which is confusing.
+
+**Acceptance Criteria:**
+- [ ] [UI] Confirmation page does NOT show "Uw diensten publiceren" as header
+- [ ] [UI] Only success message and relevant follow-up actions shown
+- [ ] [UI] Page title is contextually appropriate for completed registration
+
+**Key Context from Comments:** Agreed as good change, planned for after "afschalen producten." Assigned to SudoThijn.
+
+---
+
+### #363: Diensten wizard – Uw dienst publiceren: catalogus i.p.v. softwarecatalogus
+
+**Labels:** Aanbod, Tekstuele wijzigingen, Wijziging
+**Test Step:** Step 9
+
+**Summary:** Success message uses "catalogus" instead of "softwarecatalogus".
+
+**Acceptance Criteria:**
+- [ ] [UI] Success message uses "softwarecatalogus" not "catalogus"
+- [x] [API] Codebase-wide: no standalone "catalogus" where "softwarecatalogus" should be used
+- [x] [API] No instances of "software catalogus" (with space) — always "softwarecatalogus"
+
+**Key Context from Comments:** Requested code search for all incorrect instances. Assigned to SudoThijn.
+
+---
+
+### #364: Contactpersonen: e-mailadres is leeg
+
+**Labels:** Aanbod
+**Test Step:** Step 5
+
+**Summary:** After creating a new contact person, the email address field appears empty despite being entered.
+
+**Acceptance Criteria:**
+- [ ] [UI] After creating a contact person with an email, the email is immediately visible
+- [ ] [UI] Email displayed in contact persons overview
+- [x] [API] Email persists after page refresh
+
+**Key Context from Comments:** Same root cause as #352 — contact person not updated from Nextcloud account.
+
+---
+
+### #365: Contactpersonen: error bij het opslaan van een contactpersoon
+
+**Labels:** Aanbod
+**Test Step:** Step 5
+
+**Summary:** Editing and saving an existing contact person produces a 400 error, specifically when a role is selected.
+
+**Acceptance Criteria:**
+- [x] [API] Editing a contact person and saving does not produce a 400 error
+- [x] [API] Saving with a role selected works without errors
+- [x] [API] Changes are persisted and visible after saving
+- [x] [API] Works regardless of whether a role is selected
+
+**Key Context from Comments:** Bug fixed on B environment. Only happens when a role is selected before saving. Duplicates #73 and #65.
+
+---
+
+### #366: Contactpersonen: veld Rollen niet consistent
+
+**Labels:** Aanbod, Wijziging
+**Test Step:** Step 5
+
+**Summary:** For suppliers, the "Roles" field has little value since they're always "Aanbod beheerders". Frontend/backend inconsistency.
+
+**Acceptance Criteria:**
+- [ ] [UI] For suppliers, "Rollen" field is hidden (always "Aanbod beheerders")
+- [x] [API] Backend role matches frontend display for all contact persons
+- [ ] [UI] Role column shows correct role matching backend data
+- [ ] [UI] For municipalities, "Rollen" field remains visible (multiple roles applicable)
+
+**Key Context from Comments:** Change request related to RBAC and security. Implementation details need refinement.
+
+**VNG Manual Test (2026-02-25) — FAIL:** "Bij een leverancier is het veld rollen niet meer zichtbaar. Bij een gemeente zie je wel een veld rollen en daar gaat het mis." Specific failures for gemeente role management:
+1. New user does not receive a role
+2. Editing a user allows selecting a role, but it is NOT saved and disappears
+3. The unknown role "organisatie-beheerder" appears as an option
+
+**Additional Acceptance Criteria (from VNG feedback):**
+- [ ] [UI] For leverancier: "Rollen" field is correctly hidden (verified working)
+- [x] [API] For gemeente: newly created users receive a default role
+- [x] [API] For gemeente: editing a user's role persists correctly after save
+- [ ] [UI] For gemeente: only valid roles appear in the dropdown (no "organisatie-beheerder" unless intended)
+- [x] [API] Role assignment is saved to the backend and visible on page reload
+
+---
+
+### #367: Contactpersonen: Tussenvoegsel wordt niet getoond
+
+**Labels:** Organisatie en configuratie, Aanbod
+**Test Step:** Step 5
+
+**Summary:** Prefix/tussenvoegsel not displayed in contact persons overview.
+
+**Acceptance Criteria:**
+- [x] [UI] Names include prefix (e.g., "Jan de Vries" not "Jan Vries")
+- [x] [UI] Prefix shown in all views where contact person name appears
+- [x] [API] After editing to add prefix, it's immediately visible
+
+**Key Context from Comments:** Same root cause as #352.
+
+**VNG Manual Test (2026-02-25) — CLARIFICATION:** VNG confirms tussenvoegsel IS shown in a separate column, but the "Naam" column shows voornaam + achternaam WITHOUT tussenvoegsel. Request: change "Naam" column to show ONLY voornaam (not voornaam + achternaam minus tussenvoegsel).
+
+**Additional Acceptance Criteria (from VNG feedback):**
+- [ ] [UI] ~~"Naam" column in contactpersonen overview shows ONLY voornaam~~ → **Niet gekozen** (zie afwijking hieronder)
+- [x] [UI] "Tussenvoegsel" column shows tussenvoegsel separately
+- [x] [UI] "Achternaam" column shows achternaam separately
+- [x] [UI] **GEKOZEN:** "Naam" column shows full name including tussenvoegsel (voornaam + tussenvoegsel + achternaam)
+
+**Afwijking van klantwens:** VNG vroeg om de Naam-kolom alleen de voornaam te tonen. We wijken hiervan af: de Naam-kolom toont de **volledige naam** (voornaam + tussenvoegsel + achternaam) via het objectNameField template `{{ voornaam }} {{ tussenvoegsel }} {{ achternaam }}`. Reden: een kolom "Naam" die alleen de voornaam toont is verwarrend en afwijkend van gangbare UX-patronen. Tussenvoegsel en achternaam zijn daarnaast beschikbaar als aparte kolommen voor wie dat wenst.
+
+**Resolution (2026-03-01):** Opgelost. Tussenvoegsel correct opgenomen in de Naam-kolom. Contactpersoon-sync gescoped op organisatie (#352). Zie [reactie 367](reacties/367.md).
+
+---
+
+### #368: Applicatie publiceren: Zonder een richting aan te geven is de koppeling op te voeren
+
+**Labels:** Aanbod, Wijziging
+**Test Step:** Step 11
+
+**Summary:** Connection wizard allows saving without specifying a direction. "Richting" was being used as default value.
+
+**Acceptance Criteria:**
+- [x] [API] "Richting" field does NOT have "Richting" as a submittable default
+- [ ] [UI] User must select one of: "<-> Bi-directioneel", "A -> B", "B -> A"
+- [x] [API] Saving without selecting a direction shows a validation error
+- [ ] [UI] Error clearly indicates direction is required
+- [x] [API] Existing connections without valid direction handled gracefully
+
+**Key Context from Comments:** Validation rule was missing since first version. Fix deployed to B environment.
+
+---
+
+### #369: Applicatie publiceren: de aangemaakte koppeling is niet zichtbaar
+
+**Labels:** Aanbod, Koppeling
+**Test Step:** Step 11
+
+**Summary:** Connection created through the "Publish Application" wizard is not visible in connections overview. RBAC bug.
+
+**Acceptance Criteria:**
+- [x] [API] Connection created via wizard appears in /beheer/koppelingen
+- [x] [API] Connection displays correct application name, type, and fields
+- [ ] [HYBRID] Visible immediately after wizard completion without page refresh
+- [ ] [UI] Overview properly loads and shows all connections
+
+**Key Context from Comments:** Confirmed as RBAC bug, fixed on B environment.
+
+---
+
+### #370: Applicatie: teveel kolommen worden getoond
+
+**Labels:** Organisatie en configuratie, Aanbod, Wijziging
+**Test Step:** Step 7
+
+**Summary:** Applications table shows too many columns including unclear fields (Type, Applicatietype, Omvat, etc.).
+
+**Acceptance Criteria:**
+- [ ] [UI] Column selector does NOT offer: Type, Applicatietype, Omvat, Onderdeel van, Beoordelingen, Kwetsbaarheden, Geregistreerd door
+- [ ] [UI] Only columns with wizard-managed data are available
+- [ ] [UI] Same rule applies to Services and Connections tables
+- [ ] [UI] Default columns display meaningful data
+- [x] [API] Koppeling names in the table resolve to readable names immediately (not delayed UUID-to-name resolution)
+- [ ] [UI] Compliance column shows clear, understandable content (not just application name repeated or a dash)
+- [ ] [UI] SaaS application version is immediately visible after creation (not delayed)
+
+**Key Context from Comments:** General rule: tables should only show columns with wizard-managed data. Applies to Applications, Services, and Connections.
+
+---
+
+### #371: Applicatie: UUID onder compliance
+
+**Labels:** Aanbod, Wijziging
+**Test Step:** Step 7
+
+**Summary:** Applications table shows raw UUID in Compliance column. Should be replaced with "standaardVersies".
+
+**Acceptance Criteria:**
+- [x] [UI] No UUID values in any column on /beheer/applicaties
+- [x] [UI] ~~Compliance column replaced with "Standaardversies" showing readable names~~ → **Vervallen: Compliancy kolom is volledig verwijderd conform #430** (VNG besluit: per-applicatie compliance is te complex voor een tabelkolom)
+- [x] [UI] ~~Multiple standards displayed in readable format~~ → Compliancy-informatie is beschikbaar op de applicatie-detailpagina onder standaarden tab
+- [x] [UI] Column heading accurately reflects content
+- [x] [UI] Compliance column does NOT show the application name repeated → **Kolom verwijderd**
+- [x] [UI] ~~For imported suppliers, Compliance column shows actual compliance data~~ → **Vervallen: kolom verwijderd conform #430**
+
+**Key Context from Comments:** Oorspronkelijk voorstel: UUID vervangen door standaardVersies. Uiteindelijke oplossing conform #430: Compliancy kolom volledig verwijderd uit de beheertabel. Per-applicatie compliance is alleen zinvol op de detailpagina.
+
+**Resolution (2026-03-01):** Opgelost. Compliancy kolom verwijderd conform #430 (VNG besluit). Zie [reactie 371](reacties/371.md).
+
+---
+
+### #372: Applicaties: Kolom Contactpersoon toont geen tussenvoegsel
+
+**Labels:** Aanbod
+**Test Step:** Step 7
+
+**Summary:** Contact Person column doesn't display tussenvoegsel (e.g., "van der").
+
+**Acceptance Criteria:**
+- [ ] [UI] Contact Person column shows full name including prefix
+- [ ] [UI] E.g., "Maria van der Berg" not "Maria Berg"
+- [ ] [UI] Tussenvoegsel displayed in all locations where contact person appears
+
+**Key Context from Comments:** Duplicate of #352. Same underlying bug.
+
+---
+
+### #373: Applicatie: Gekoppelde diensten worden niet getoond
+
+**Labels:** Aanbod
+**Test Step:** Step 7
+
+**Summary:** Application overview doesn't show associated services. "Diensten" display option was missing.
+
+**Acceptance Criteria:**
+- [x] [UI] "Diensten" column is available and can be enabled on /beheer/applicaties
+- [x] [UI] When application has linked services, they are displayed
+- [x] [UI] Multiple services shown or count/link displayed
+- [ ] [HYBRID] Clicking navigates to filtered services view *(nog niet geïmplementeerd)*
+- [x] [API] Bidirectional: service shows application AND application shows services
+- [ ] [UI] When >10 diensten are linked to one applicatie, a count/link to filtered diensten view is shown *(nog niet geïmplementeerd)*
+
+**Key Context from Comments:** Display option was missing, added to B environment. Duplicate of #377.
+
+**Resolution (2026-03-01):** Grotendeels opgelost. Drie samenhangende fixes: (1) `diensten` toegevoegd aan extend array, (2) custom renderer voor diensten-kolom, (3) extend-parameter doorgifte naar API gefixed. Resterende punten: klikbare navigatie naar gefilterd dienstenoverzicht en count/link bij >10 diensten. Zie [reactie 373](reacties/373.md).
+
+---
+
+### #374: Applicaties: Standaarden, Standaarden GEMMA en Standaardversies?
+
+**Labels:** Aanbod
+**Test Step:** Step 7
+
+**Summary:** Three confusing standards columns: "Standaarden", "Standaarden GEMMA", "Standaardversies". Should only show "Standaardversies".
+
+**Acceptance Criteria:**
+- [ ] [UI] Only "Standaardversies" column shown by default
+- [ ] [UI] Separate "Standaarden" and "Standaarden GEMMA" columns removed or hidden
+- [ ] [UI] If a standards column exists, labeled "Standaarden" (not "Standaarden GEMMA")
+- [ ] [UI] No duplication of standards information
+- [x] [API] Standaardversies shows readable names (not UUIDs)
+- [ ] [UI] "Standaarden GEMMA" column is renamed to "Standaarden" (the "GEMMA" suffix is removed)
+
+**Key Context from Comments:** Only show "Standaardversies" — separate standards columns are redundant.
+
+---
+
+### #375: Applicaties: versie voor SaaS applicaties?
+
+**Labels:** Aanbod
+**Test Step:** Step 7
+
+**Summary:** SaaS applications show no version under "Applicatieversies". Should automatically receive a default version.
+
+**Acceptance Criteria:**
+- [x] [API] SaaS application created via wizard automatically receives a default version
+- [x] [UI] Default version visible in Applicatieversies tab
+- [ ] [UI] For "SaaS en On Premise" apps, both version types can be managed *(te verifiëren)*
+- [x] [API] Removing a version type properly handles cleanup *(te verifiëren)*
+- [x] [UI] Versions visible in overview table when column enabled
+
+**Key Context from Comments:** Wizard should always create a version. Test with: SaaS, SaaS and On Premise, removing On Premise, removing SaaS.
+
+**Additional Acceptance Criteria (from 2026-03-04 feedback):**
+- [ ] [UI] When changing hosting from On-premise to SaaS, the old On-premise versions are removed or archived
+- [ ] [UI] Switching hosting type does not leave orphaned versions from the previous hosting type
+
+**Resolution (2026-03-01):** Opgelost. `shouldShowVersiesStep()` retourneert nu altijd `true` — Versies-stap is zichtbaar voor alle hosting-typen. Wizard maakt default versie "1.0.0" aan met status "in gebruik". Zie [reactie 375](reacties/375.md).
+
+---
+
+### #376: Applicaties: labels wizard en tabel zijn anders
+
+**Labels:** Aanbod, Tekstuele wijzigingen
+**Test Step:** Step 7
+
+**Summary:** Field labels in management table differ from wizard labels. Should be consistent.
+
+**Acceptance Criteria:**
+- [ ] [UI] Column headers on /beheer/applicaties match wizard field labels exactly
+- [ ] [UI] No spelling differences between table and wizard
+- [ ] [UI] No extra/unknown labels in table
+- [ ] [UI] Same consistency for Services and Connections tables
+- [ ] [UI] Labels match approved PowerPoint from 17-12-2025 (slide 42)
+- [ ] [UI] **Image comparison**: Fetch the reference screenshot from the issue (`https://github.com/user-attachments/assets/f02880fc-4295-4cf2-85b8-0809e75808a2`) and compare table column headers with wizard field labels in the live UI
+- [ ] [UI] Column header uses "Applicatieversies" (one word, no space) not "Applicatie Versies" (two words)
+
+**Key Context from Comments:** Labels only updated in wizards, not in OpenRegister schema. This is a schema/OpenRegister labels change, not just UI. Related to #359. See also slide 42 of the PowerPoint.
+
+---
+
+### #377: Applicaties: tabel toont diensten niet
+
+**Labels:** Aanbod
+**Test Step:** Step 7
+
+**Summary:** "Diensten" column empty for applications with linked services (e.g., PinoApp doesn't show "PinoApp beheer"), but reverse works.
+
+**Acceptance Criteria:**
+- [x] [UI] When application has linked services, Diensten column shows them
+- [x] [API] Applicatie met gekoppelde dienst toont de dienstnaam in de Diensten kolom (lokaal: koppel een dienst aan een testapplicatie en verifieer dat de naam verschijnt)
+- [x] [API] Bidirectional relationship is consistent
+- [x] [UI] Diensten column can be enabled via column selector
+
+**Key Context from Comments:** Display for services in this table was missing, added to B environment. Existed since "Aanbod" phase.
+
+**Resolution (2026-03-01):** Opgelost via dezelfde fix als #373. Zie [reactie 377](reacties/377.md).
+
+---
+
+### #378: Applicatie: Standaarden na wijzigen veranderd
+
+**Labels:** wontfix, Aanbod
+**Test Step:** Step 7
+
+**Summary:** After editing standards via Actions > "Bewerk standaarden", all standards reset to "Ondersteund" regardless of individual values.
+
+**Acceptance Criteria:**
+- [ ] [UI] Compliance statuses display correctly before editing
+- [x] [API] After saving without changes, values remain unchanged (no reset to "Ondersteund")
+- [ ] [UI] Each standard retains its individual status with correct color coding
+- [ ] [UI] Alternatively, if all editing goes through wizards (#384), "Bewerk standaarden" is removed from Actions menu
+- [x] [API] Uploaded compliance evidence documents retain their original filename (not renamed to "Bewijs_.")
+
+**Key Context from Comments:** Labeled "wontfix" — will become moot once all editing routes through wizards (#384). Bug persists until then.
+
+---
+
+### #379: Applicatie: verschillende manier van tonen compliancy
+
+**Labels:** Aanbod
+**Test Step:** Step 7
+
+**Summary:** Standards compliance displayed inconsistently in three different views. Management page is correct; search tab is incomplete; wizard review only shows compliant ones.
+
+**Acceptance Criteria:**
+- [x] [UI] Management detail page shows all standards with correct status and colors
+- [x] [API] Search/public view shows SAME information as management page
+- [x] [UI] Wizard review page shows ALL standards (supported AND not supported)
+- [x] [UI] All three views use the same table format
+- [x] [UI] Non-supported standards visible on all views
+- [x] [UI] Standards list sort order is consistent across all views (control page, detail page, wizard review page)
+
+**Key Context from Comments:** Screenshots confirm both control and detail pages now use same table. Related to #348. Compliancy kolom verwijderd uit overzichtstabel conform #430; detail view is nu de uniforme weergave.
+
+**VNG Manual Test (2026-02-25) — CLARIFICATION:** Links to #284. Only standard versions with status "in gebruik" or "in ontwikkeling" should be shown in the compliance table. Standard versions with status "einde ondersteuning" or "teruggetrokken" should be shown as "added standards" (toegevoegde standaarden), separate from the compliance overview.
+
+**Additional Acceptance Criteria (from VNG feedback):**
+- [x] [API] Compliance table only shows standard versions with status "in gebruik" or "in ontwikkeling"
+- [x] [API] Standard versions with status "einde ondersteuning" or "teruggetrokken" are displayed separately as "toegevoegde standaarden"
+- [ ] [UI] The count/total in the compliance overview only counts active standards (in gebruik + in ontwikkeling)
+- [ ] [HYBRID] Filtering by status is consistent across management page, detail page, and wizard review
+
+**Resolution (2026-03-01):** Opgelost. Compliancy-inconsistentie opgelost door de tabelkolom te verwijderen (#430) — de applicatie-detailpagina is nu de uniforme weergave. VNG-feedback over standaardversie-status filtering is nog niet geïmplementeerd. Zie [reactie 379](reacties/379.md).
+
+---
+
+### #380: Applicatie: compliance aantallen komen niet overeen
+
+**Labels:** Aanbod
+**Test Step:** Step 7
+
+**Summary:** Wizard shows all reference components imposing standards, management page shows only one. Inconsistent counts.
+
+**Acceptance Criteria:**
+- [ ] [UI] Management page shows ALL standard versions imposed by reference components
+- [x] [API] Count matches wizard standards step count
+- [ ] [UI] Multiple reference components each imposing standards all appear in both views
+- [x] [API] Count/total consistent across wizard and management page
+
+**Key Context from Comments:** Same root cause as #379 — inconsistency in compliance display.
+
+---
+
+### #381: Applicaties: non-compliant vervangen door niet ondersteund
+
+**Labels:** Aanbod, Wijziging
+**Test Step:** Step 7
+
+**Summary:** "non-compliant" should be changed to "niet ondersteund" with red color indicator everywhere.
+
+**Acceptance Criteria:**
+- [x] [API] "non-compliant" does NOT appear anywhere in the UI
+- [ ] [UI] All instances show "niet ondersteund"
+- [ ] [UI] "niet ondersteund" displayed with red color indicator
+- [ ] [UI] Applies to: tables, detail pages, wizard steps, review pages
+- [ ] [UI] Consistent Dutch terminology (no English mixing)
+- [x] [API] "Compliant" does NOT appear anywhere — replaced with "Ondersteund" for consistency (all Dutch terminology)
+- [ ] [UI] "niet ondersteund" is ALWAYS displayed in red (not grey in some views and red in others)
+- [ ] [UI] "Ondersteund" is used consistently for supported standards (not "Compliant" in some views)
+
+**Key Context from Comments:** Good change, planned for after "producten afschalen."
+
+---
+
+### #382: Applicatie: compliancy link werkt niet
+
+**Labels:** Aanbod
+**Test Step:** Step 7
+
+**Summary:** Compliance links treated as relative paths (e.g., "pino.nl/compliancy" becomes localhost/pino.nl/compliancy).
+
+**Acceptance Criteria:**
+- [ ] [UI] Compliance links open correct external URL in new tab
+- [x] [API] URLs without protocol prefix handled properly (prepend "https://" or validate format)
+- [x] [API] Link NOT treated as relative path
+- [x] [API] Links with "http://" or "https://" work correctly
+
+**Key Context from Comments:** Router issue — URLs without protocol treated as relative. Fix in progress.
+
+---
+
+### #383: Applicatie: selectie vakken werken niet
+
+**Labels:** Aanbod
+**Test Step:** Step 7
+
+**Summary:** Selection checkboxes on application management page don't work.
+
+**Acceptance Criteria:**
+- [ ] [UI] Clicking a row checkbox selects that row (visual indication)
+- [ ] [UI] Multiple rows can be selected
+- [ ] [UI] "Select all" checkbox works
+- [ ] [UI] Selected rows enable bulk actions (if applicable)
+- [ ] [UI] Selection state is visually clear
+- [x] [API] After selecting specific rows and clicking export, ONLY the selected rows are exported (not all rows)
+
+**Key Context from Comments:** May be a regression from performance changes. Will be addressed after "producten afschalen."
+
+---
+
+### #384: Applicaties: eenduidige manier van bewerken
+
+**Labels:** Aanbod, Wijziging
+**Test Step:** Step 7
+
+**Summary:** Multiple editing methods with different interfaces. All editing should go through wizards. Non-wizard editing options should be disabled except "Verwijderen".
+
+**Acceptance Criteria:**
+- [ ] [HYBRID] "Bewerken" from table opens the application publish wizard
+- [ ] [UI] "Bewerken" from detail view opens the wizard
+- [ ] [UI] Actions menu only has "Bewerken" (via wizard) and "Verwijderen"
+- [ ] [UI] Other action options (Bewerk standaarden, etc.) removed
+- [ ] [UI] Wizard includes "Lange omschrijving" (long description) field
+- [ ] [UI] Same pattern for Services and Connections
+- [x] [API] Editing via wizard pre-fills all existing data
+- [ ] [UI] Field is labeled "Uitgebreide omschrijving" (matching PowerPoint) not "Lange omschrijving"
+
+**Key Context from Comments:** "Lange omschrijving" is a prerequisite. Not in design PowerPoints but necessary for wizard-only editing.
+
+---
+
+### #385: Applicatie: Geen huidige versie in gebruik
+
+**Labels:** Aanbod, Wijziging
+**Test Step:** Step 7
+
+**Summary:** Detail page shows "Geen huidige versie in gebruik" in sidebar even when versions exist. Should remove "Huidige versie" from sidebar since Versions tab already shows this.
+
+**Acceptance Criteria:**
+- [ ] [UI] Gray sidebar does NOT show "Huidige versie" section
+- [ ] [UI] "Geen huidige versie in gebruik" text not in sidebar
+- [ ] [UI] Version info shown exclusively under "Versies" tab
+- [x] [API] Versies tab displays correct version count and statuses
+- [ ] [UI] Sidebar still shows other metadata
+
+**Key Context from Comments:** Agreed: remove "Huidige versie" from gray block.
+
+---
+
+### #386: Applicaties – Uw applicatie publiceren: andere labels
+
+**Labels:** Aanbod
+**Test Step:** Step 7
+
+**Summary:** Labels in the "Publish application" wizard don't match management/control form labels.
+
+**Acceptance Criteria:**
+- [ ] [UI] All wizard labels match management table and control form labels
+- [ ] [UI] Labels consistent with approved PowerPoint specification
+- [ ] [UI] No spelling differences between wizard steps and review/control form
+- [ ] [UI] Labels in proper Dutch with agreed terminology
+- [ ] [UI] **Image comparison**: Fetch the reference screenshot from the issue (`https://github.com/user-attachments/assets/e4f8f5bf-e1d5-44df-b7e5-dc52a1ce8676`) and compare each label with the live UI wizard
+
+**Key Context from Comments:** Not regressions — labels have been this way since acceptance. Some labels were previously intentionally lengthened (diverged from schema) but should now be aligned back to the PowerPoint.
+
+---
+
+### #387: Applicaties – Uw applicatie publiceren: i niet aanwezig
+
+**Labels:** Aanbod, Tekstuele wijzigingen
+**Test Step:** Step 7
+
+**Summary:** Information (i) tooltip icons missing for all fields in the version step of the application publish wizard.
+
+**Acceptance Criteria:**
+- [ ] [UI] Every field label on the version step has an (i) icon/tooltip (they are currently MISSING entirely)
+- [ ] [UI] Clicking/hovering shows relevant help text matching the PowerPoint
+- [ ] [UI] (i) icons present for ALL version-related labels, not just some
+- [ ] [UI] Help text is meaningful and matches the PowerPoint specification
+- [ ] [UI] Styling consistent with other wizard steps
+- [ ] [UI] **Image comparison**: Fetch the reference screenshot from the issue (`https://github.com/user-attachments/assets/002c16a6-4ddb-4072-9a54-2aaff8bcd4f5`) showing the MISSING tooltips, then verify in the live UI whether they have been added
+
+**Key Context from Comments:** Adjusted on B environment. The "i" tooltips need to be ADDED (they were completely absent, not just wrong text). The tooltip text should come from the PowerPoint.
+
+---
+
+### #390: Applicaties – Uw applicatie publiceren: labels komen niet overeen
+
+**Labels:** Aanbod, Tekstuele wijzigingen
+**Test Step:** Step 7
+
+**Summary:** Labels in publish wizard don't match control/review form labels.
+
+**Acceptance Criteria:**
+- [ ] [UI] Control/review page labels exactly match wizard input step labels
+- [ ] [UI] No discrepancies between any wizard step and its summary
+- [ ] [UI] Same terminology and spelling throughout
+- [ ] [UI] Labels follow approved PowerPoint specification
+- [ ] [UI] **Image comparison**: Fetch the reference screenshot from the issue (`https://github.com/user-attachments/assets/63e3fc9a-b51b-4a6c-8002-9c17773f62e9`) showing the inconsistent labels, then verify in the live UI whether review form labels now match wizard input labels
+
+**Key Context from Comments:** Fixed on B environment but still needs testing. Part of broader textual changes effort (#359).
+
+---
+
+### #391: Testen met een gebruiker van een bestaande organisatie
+
+**Labels:** Aanbod
+**Test Step:** Step 3
+
+**Summary:** Testing with imported users from imported organizations was disabled. Need to verify activation and login works.
+
+**Acceptance Criteria:**
+- [x] [API] An imported organization exists with at least one contact person
+- [x] [API] Contact person can be activated as a user (after fixing invalid email)
+- [x] [API] Activated imported user can log in successfully
+- [x] [API] Imported user can view and manage their organization's data
+- [x] [API] Same capabilities as a user from a newly registered organization
+- [x] [API] Imported users with invalid email addresses show a clear error or warning when activation is attempted without first correcting the email
+- [x] [API] After correcting the email address of an imported contact person, activation proceeds successfully
+
+**Key Context from Comments:** Duplicate of #392. Imported users received invalid emails — must be corrected before activation.
+
+**VNG Manual Test (2026-02-25) — PARTIAL:**
+- ❌ Editing imported users with invalid email addresses fails with 400 error via frontend. The invalid characters in the email prevent updating the user record.
+- ✅ Creating a NEW user via the backend works, and that new user can log in under an imported organization.
+
+**Additional Acceptance Criteria (from VNG feedback):**
+- [ ] [UI] Imported users with invalid email addresses CAN be edited (email field allows correction)
+- [ ] [UI] After correcting invalid email, saving does not produce 400 error
+- [x] [API] Backend validates email format but allows transition from invalid → valid
+- [x] [API] Creating new users for imported organizations works via both frontend and backend
+
+---
+
+### #392: Back-end: geimporteerde gebruiker geeft error bij omzetten naar user
+
+**Labels:** Aanbod
+**Test Step:** Step 3
+
+**Summary:** Creating a contact person for an imported organization does NOT convert them to a user (produces error), unlike new organizations where this is automatic.
+
+**Acceptance Criteria:**
+- [x] [API] Creating a contact person for an imported organization does NOT produce an error
+- [x] [API] Contact person is converted to a user automatically, like for new organizations
+- [x] [API] Converted user can log in with correct permissions
+- [x] [API] Behavior consistent between imported and newly created organizations
+- [x] [API] No backend errors in logs during conversion
+- [x] [API] After environment re-deployment, previously imported contact persons can be activated without blockade
+- [x] [API] Data import is completed before testing user conversion
+
+**Key Context from Comments:** Duplicate of #391. Data import must be completed before testing.
+
+---
+
+### #393: Backend: fouten in voorzieningenregister
+
+**Labels:** Aanbod
+**Test Step:** Step 19
+
+**Summary:** Errors in the voorzieningenregister (provisions register). Excel export broken. Scope narrowed to: schema retrieval, API docs, and exports must work.
+
+**Acceptance Criteria:**
+- [x] [API] Backend API returns valid schema data (GET schema endpoint returns 200)
+- [x] [API] API documentation endpoint is accessible and complete
+- [x] [API] Excel export works without errors and produces valid .xlsx file
+- [ ] [HYBRID] Exported Excel contains expected columns and rows
+- [x] [API] No 500 errors when accessing voorzieningenregister endpoints
+- [x] [API] Excel export works per register/schema combination (not the entire catalog in one export)
+- [x] [API] Export file correctly represents the data for the selected register/schema combination only
+
+**Key Context from Comments:** Excel export is a regression from "products scaling down." Data model partially cleaned. Scope: schema, API docs, exports.
+
+---
+
+### #394: Contactpersonen van gemeenten publiekelijke zichtbaar
+
+**Labels:** Aanbod, Wijziging
+**Test Step:** Step 12
+
+**Summary:** Contact persons of **gemeenten** (municipalities) are publicly visible but should NOT be. Note: contact persons of **leveranciers** (vendors) ARE expected to be publicly visible — only gemeente/samenwerking contact persons should be hidden.
+
+**RBAC Reference:** See `softwarecatalog/lib/Settings/softwarecatalogus_register.json` → `contactpersoon` schema → `authorization` block. The `contactpersoon` schema does NOT have `public` read access. Leverancier contact persons are exposed via **publications** (which extend contactpersonen), not via direct public access to the contactpersoon schema.
+
+**Acceptance Criteria:**
+- [x] [API] Contact persons of **leveranciers** ARE visible on public pages (this is expected/correct behavior)
+- [x] [API] Contact persons of **gemeenten** are NOT visible to unauthenticated users on frontend
+- [x] [API] Contact persons of **samenwerkingen** are NOT visible to unauthenticated users
+- [x] [API] Public API (`_extend=contactpersonen`) correctly distinguishes: leverancier contacts visible, gemeente/samenwerking contacts hidden
+- [x] [API] No personal contact information (name, email, phone) of gemeente users on public pages
+- [x] [API] API endpoint enforces RBAC: authenticated gebruik-beheerder can see all contactpersonen, aanbod-beheerder sees only own org
+
+**Key Context from Comments:** Regression from "published scaling" change. The RBAC model in `softwarecatalogus_register.json` shows contactpersoon read access is NOT public — leverancier contacts are only expected to be visible via publication extensions. The bug is that gemeente contact persons are exposed, not that leverancier ones are.
+
+**Resolution (2026-03-01):** Opgelost. RBAC blokkeert correct ongeauthenticeerde toegang tot contactpersonen — API retourneert 401 voor anonieme verzoeken. Zie [reactie 394](reacties/394.md).
+
+---
+
+### #395: Menu linkerkant verdwijnt
+
+**Labels:** Aanbod
+**Test Step:** Step 4
+
+**Summary:** Left navigation menu disappears after pressing F5/Ctrl+R to refresh on the Applicaties overview.
+
+**Acceptance Criteria:**
+- [ ] [UI] Navigate to "Applicaties" overview while logged in
+- [ ] [UI] Press F5 or Ctrl+R to refresh
+- [ ] [UI] Left navigation menu remains visible after refresh
+- [ ] [UI] Menu present when directly navigating to URL (not just SPA navigation)
+- [ ] [UI] Menu persists across refreshes on other pages (Diensten, Koppelingen, etc.)
+
+**Key Context from Comments:** Marked as fixed on B environment. Needs verification on accept.
+
+---
+
+### #396: Verouderde NextCloud versie
+
+**Labels:** Aanbod
+**Test Step:** Infra
+
+**Summary:** Backend running unsupported Nextcloud version. Agreement to upgrade to Nextcloud 32.
+
+**Acceptance Criteria:**
+- [x] [API] Nextcloud backend running version 32.x
+- [ ] [UI] No "unsupported version" warnings in admin panel
+- [x] [API] All softwarecatalogus functionality works on NC 32
+- [x] [API] Verify via admin interface or status.php endpoint
+- [ ] [UI] NOTE: Infrastructure issue — verify via admin panel
+- [x] [API] Nextcloud admin log does not contain critical errors related to softwarecatalogus apps
+
+**Key Context from Comments:** Both environments set to NC 32. NC 33 has breaking changes. VNG policy: run n-1, test quarterly.
+
+---
+
+### #397: Pagina aanmaken via CMS
+
+**Labels:** Aanbod, Wijziging
+**Test Step:** Step 21
+
+**Summary:** CMS page editing broken. Cannot edit "algemene voorwaarden" text despite it previously working.
+
+**Acceptance Criteria:**
+- [ ] [UI] Admin can navigate to CMS page management
+- [ ] [UI] Admin can create a new CMS page with custom content
+- [ ] [UI] Admin can edit existing CMS pages
+- [ ] [HYBRID] After editing and saving, updated text is visible on public page
+- [ ] [UI] CMS editor properly renders content in edit view
+- [ ] [UI] CMS editing documentation/manual is accessible via the handleidingen page
+
+**Key Context from Comments:** Regression from performance changes. Duplicates #332. CMS is a non-functional requirement. Page texts still need to be provided by VNG (#182).
+
+---
+
+### #399: Versies: een versie van een applicatie van een andere leverancier levert een foutmelding
+
+**Labels:** Aanbod
+**Test Step:** Step 7
+
+**Summary:** Viewing a version of another supplier's application produces "Kon publicatie niet laden" error. Should be viewable in read-only.
+
+**Acceptance Criteria:**
+- [x] [API] Log in as supplier A, navigate to supplier B's application
+- [ ] [UI] Click on a version — detail page loads without error
+- [ ] [UI] Version details shown in read-only mode (no edit button)
+- [ ] [UI] Own application's versions have edit functionality available
+- [x] [API] Published endpoint returns correct data across suppliers
+- [ ] [UI] Non-owner suppliers can navigate to the version detail page via the application's version list
+- [ ] [UI] If the published endpoint returns a 404 for a version, the frontend shows a proper error page
+
+**Key Context from Comments:** RBAC fix adjusts so versions viewable by other suppliers. Published endpoint updated.
+
+---
+
+### #400: Koppeling - Opslaan van een koppeling geeft een foutmelding
+
+**Labels:** Aanbod
+**Test Step:** Step 11
+
+**Summary:** Saving a connection produces an error message.
+
+**Acceptance Criteria:**
+- [ ] [UI] Create a new koppeling via the wizard and fill in required fields
+- [x] [API] Click save — saves successfully without error
+- [x] [API] Saved koppeling appears in overview
+- [x] [API] Editing and re-saving also works without errors
+- [x] [API] Data persisted correctly
+
+**Key Context from Comments:** None beyond original report.
+
+---
+
+### #401: Koppeling - geïmporteerde koppelingen kaartjes zijn leeg
+
+**Labels:** Aanbod, Wijziging
+**Test Step:** Step 11
+
+**Summary:** Imported connection cards appear empty. PIMS@all connection directions incorrect. Requesting details produces error.
+
+**Acceptance Criteria:**
+- [ ] [UI] Imported koppeling cards display name and short description (not empty)
+- [ ] [UI] Direction indicator on PIMS@all matches correct values
+- [x] [API] Clicking a card opens detail view without error
+- [ ] [UI] Cards from other suppliers also render with metadata
+- [ ] [UI] Test on renewed environment (not old URL)
+- [ ] [UI] Connections missing both moduleB and buitengemeentelijke voorziening show a clear indication of incomplete data
+- [ ] [UI] Invalid standard version references in connections do not cause UUIDs to display in the card view
+
+**Key Context from Comments:** Import data lacked @name column. Fix: default name during import. Re-importing should fix data. Issue #312 handles koppeling naming, this issue focuses on imported data quality. Data analysis revealed: non-existent standaardversie IDs in the import CSV (possibly Drupal UUIDs instead of production UUIDs), missing moduleB references, and koppelingen pointing to non-existent applications.
+
+---
+
+### #402: Verschil tussen Edge en Chrome bij laden applicaties
+
+**Labels:** Aanbod
+**Test Step:** General
+
+**Summary:** Same user sees different content in Edge vs Chrome. Side-by-side comparison shows different results.
+
+**Acceptance Criteria:**
+- [x] [API] Same user account in Edge and Chrome shows identical application list
+- [x] [API] Data, ordering, and item count matches between browsers
+- [x] [API] No caching artifacts cause differences (test after clearing cache)
+- [ ] [UI] NOTE: Team could NOT reproduce with 3 testers — verify if issue persists
+
+**Key Context from Comments:** Not reproducible by development team. May be browser caching behavior.
+
+---
+
+### #403: Tekst verwijderen aanpassen
+
+**Labels:** Aanbod, Wijziging
+**Test Step:** Step 21
+
+**Summary:** Deletion confirmation text needs updating. Different text per object type, and should indicate if municipalities are using the item.
+
+**Acceptance Criteria:**
+- [ ] [UI] Deleting application NOT in use: "De applicatie \"\" wordt niet gebruikt door gemeenten of samenwerkingen en kan veilig worden verwijderd."
+- [ ] [UI] Deleting service NOT in use: "De dienst \"\" wordt niet gebruikt..."
+- [ ] [UI] Deleting connection NOT in use: "De koppeling \"\" wordt niet gebruikt..."
+- [ ] [UI] Deleting item IN USE: "De {type} \"\" wordt gebruikt door onderstaande gemeenten en/of samenwerkingen en kan niet worden verwijderd." with list
+- [ ] [UI] Object name dynamically inserted
+- [ ] [UI] Object type dynamically inserted
+- [ ] [UI] When deleting an application that has diensten linked by OTHER leveranciers, the system shows a specific warning
+- [x] [API] A defined flow exists for the scenario where a leverancier attempts to delete an object used by another leverancier's diensten
+
+**Key Context from Comments:** Exact text templates provided by markbacker. Two variants: not in use (can delete) and in use (cannot delete, shows users).
+
+---
+
+### #404: Regelmatig witte schermen
+
+**Labels:** Organisatie en configuratie
+**Test Step:** General
+
+**Summary:** White screens (blank pages) occur regularly in Edge browser. Factory reset fixes it, cache clearing doesn't always.
+
+**Acceptance Criteria:**
+- [ ] [UI] Navigate through all major pages in Edge — no white screens
+- [ ] [UI] Refreshing pages (F5) doesn't produce white screens
+- [ ] [UI] After clearing cache in Edge, pages load correctly
+- [ ] [UI] JavaScript console shows no critical errors causing blank rendering
+- [ ] [UI] NOTE: Team hasn't seen this for a week — may be resolved
+
+**Key Context from Comments:** Unable to reproduce for a week on test environment. May have been resolved as side effect of other fixes.
+
+---
+
+### #406: SiteImprove verwijderen
+
+**Labels:** Organisatie en configuratie
+**Test Step:** Step 21
+
+**Summary:** Template contains unauthorized SiteImprove analytics script that must be removed. Only Piwik should be present.
+
+**Acceptance Criteria:**
+- [x] [API] HTML source does NOT contain `siteimproveanalytics.com` script tag
+- [x] [API] No references to "siteimprove" in page source
+- [x] [API] Only Piwik analytics script present
+- [ ] [UI] Verify by viewing page source on public pages
+- [x] [API] Only ONE configurable position for tracking scripts
+- [x] [API] Verify removal by checking page source on production/accept environment — confirm no siteimprove script loads at runtime
+
+**Key Context from Comments:** Must ensure only one configurable position for tracking/measurement scripts.
+
+---
+
+### #407: Toegevoegde standaarden verwijzen naar id-id-....
+
+**Labels:** Aanbod
+**Test Step:** Step 16
+
+**Summary:** Standard links generate broken URLs with "id-" duplicated (e.g., `id-id-beba0771-...`), leading to 404 on GEMMA Online.
+
+**Acceptance Criteria:**
+- [ ] [UI] Click a linked standard on application detail page
+- [x] [API] URL follows format: `https://www.gemmaonline.nl/wiki/GEMMA/id-{uuid}` (single "id-")
+- [ ] [UI] Link does NOT contain "id-id-" (double prefix)
+- [ ] [UI] Link opens correct GEMMA Online page
+- [x] [API] Works for all standards across all applications
+
+**Key Context from Comments:** Root cause: IDs already contain "id-" prefix but old code adds another. Fix: remove extra prefix.
+
+---
+
+### #408: Tabblad beschrijving bij Dienst
+
+**Labels:** Aanbod
+**Test Step:** Step 9
+
+**Summary:** After creating a service, an unexpected "Beschrijving" tab appears containing a number instead of text.
+
+**Acceptance Criteria:**
+- [x] [UI] Create a new dienst and navigate to detail page
+- [x] [UI] No unexpected "Beschrijving" tab appears
+- [x] [UI] Only expected/designed tabs are visible (Applicaties, Organisaties, etc. — no Beschrijving tab)
+- [x] [UI] Empty "uitgebreide omschrijving" doesn't cause phantom tab
+- [x] [UI] No internal numeric values (field length, index, property count) are displayed as tab content or tab labels
+- [x] [UI] beschrijvingKort is displayed inline on the detail page
+- [x] [UI] beschrijvingLang (if present) is displayed inline beneath beschrijvingKort, not in a tab
+
+**Key Context from Comments:** Extended description field was empty, not filled with "13". Rendering bug displaying field length or numeric property instead of text.
+
+**Fix (2026-02-26):**
+- Removed `createBeschrijvingTab` custom tab from dienst RelatedTabs — description is now inline, not in a tab
+- Fixed `beschrijving-tab.helper.js` to not fall back to `@self.description` (which contained numeric metadata like "11", "9")
+- `beschrijvingLang` is rendered inline beneath `beschrijvingKort` using MDEditor.Markdown
+- Files: `ac-publication-dienst.js`, `beschrijving-tab.helper.js`
+
+---
+
+### #409: Footer anders: inlog of uitgelogd
+
+**Labels:** Organisatie en configuratie
+**Test Step:** Step 21
+
+**Summary:** Footer differs between logged-in and logged-out states. "Privacyverklaring" and "Algemene voorwaarden" links point to different pages.
+
+**Acceptance Criteria:**
+- [x] [API] Footer links are identical in logged-in and logged-out states
+- [x] [API] "Privacyverklaring" link points to same URL in both states
+- [x] [API] "Algemene voorwaarden" link points to same URL in both states
+- [ ] [UI] Footer styling consistent between states
+- [ ] [UI] NOTE: Team could not replicate — verify on latest environment
+- [x] [API] A single, definitive set of footer links is defined and applied to both logged-in and logged-out states
+
+**Key Context from Comments:** Couldn't replicate. May have been from two footer menu variants during testing.
+
+---
+
+### #410: Dashboard schrijfwijze softwarecatalogus
+
+**Labels:** Organisatie en configuratie, Tekstuele wijzigingen
+**Test Step:** Step 21
+
+**Summary:** "softwarecatalogus" written inconsistently on dashboard. Should always be lowercase. New welcome text provided for suppliers.
+
+**Acceptance Criteria:**
+- [ ] [UI] All instances use lowercase "softwarecatalogus" (not "Softwarecatalogus", "Software Catalogus", etc.)
+- [ ] [UI] Supplier welcome text heading: "Welkom in uw softwarecatalogus"
+- [ ] [UI] Body includes four bullet points about what suppliers can register
+- [ ] [UI] Instruction text about publishing new items and finding existing items via left menu present
+- [ ] [UI] Closing paragraph about municipalities using the information present
+- [ ] [UI] Spelling consistent across entire dashboard
+- [ ] [UI] Welcome text uses "GEMeentelijke Model Architectuur (GEMMA)" with exact capitalization as provided
+
+**Key Context from Comments:** Exact supplier text provided by Makkmetp (Feb 18). Also resolves #255.
+
+---
+
+### #451: Koppeling: UUID's zichtbaar bij standaardversies
+
+**Labels:** Aanbod, IGS nieuw
+**Test Step:** Step 11
+
+**Summary:** When creating a koppeling via the wizard, UUIDs are displayed for standard versions ("standaardversies") instead of readable names, both during creation and when viewing the koppeling details.
+
+**Acceptance Criteria:**
+- [ ] [UI] When creating a koppeling via the wizard, standard versions show readable names (not UUIDs)
+- [ ] [UI] The koppeling detail page shows standard version names instead of UUIDs in the "Standaardversies" section
+- [ ] [API] The koppeling API response resolves standaardversie references to their display names
+- [ ] [UI] Newly created koppelingen (not imported) always display resolved standard version names
+
+**Key Context from Comments:** Imported koppelingen may contain invalid standaardversie IDs (see #401). This issue specifically concerns newly created koppelingen which should have valid references. Related to #401 (imported data quality).
+
+---
+
+### #452: Applicaties overzicht: toont niet alle koppelingen
+
+**Labels:** Aanbod, IGS nieuw
+**Test Step:** Step 7
+
+**Summary:** The Applicaties overview table's "Koppelingen" column does not show all koppelingen. An application with 3 koppelingen only shows 2 in the overview. The reporter requests a "+N meer" indicator when there are more than 2 koppelingen.
+
+**Acceptance Criteria:**
+- [ ] [API] The applicatie overview API returns the correct total count of koppelingen per application
+- [ ] [UI] The Koppelingen column in the applicatie overview shows all koppelingen or indicates the total count
+- [ ] [UI] When more than 2 koppelingen exist, a "+N meer" indicator is shown below the visible items
+- [ ] [UI] Clicking the "+N meer" indicator or the application row navigates to the full koppeling list
+
+**Key Context from Comments:** Reporter observed 3 koppelingen created for application "Korf" but only 2 displayed in the overview column. Suggests a consistent pattern with "+N meer" suffix.
+
+---
+
+### #453: Zoeken: filters van slag met filter Type=Koppeling
+
+**Labels:** IGS nieuw
+**Test Step:** Step 14
+
+**Summary:** Search facets break when filtering by Type=Koppeling. Other filters don't adjust to the filtered results (still show counts from all types). Selecting a second filter causes the Type=Koppeling filter to disappear. Text search combined with Type=Koppeling also causes filter inconsistencies.
+
+**Acceptance Criteria:**
+- [ ] [UI] After selecting Type=Koppeling filter, other facets update to reflect only koppeling-related values and counts
+- [ ] [UI] Selecting a second filter (e.g., Licentievorm) does not remove the Type=Koppeling filter
+- [ ] [UI] Combining text search with Type=Koppeling filter shows correct results with properly scoped facets
+- [ ] [API] The search API with `_search` + `type=koppeling` returns facet counts scoped to the filtered result set
+- [ ] [UI] Filter counts (e.g., "Licentievorm=Closed source (N)") reflect the actual number within the current filtered view
+
+**Key Context from Comments:** This is related to the faceting architecture. Non-aggregated facets (like "type") should scope subsequent facets to the selected schema. The bug suggests facets are being aggregated across all schemas instead of being scoped.
+
+---
+
+### #454: Wizard koppelingen: Reeds bestaande koppelingen voor worden niet gevonden
+
+**Labels:** Aanbod, IGS nieuw
+**Test Step:** Step 11
+
+**Summary:** When a supplier creates a koppeling for an application they didn't register themselves, the "Reeds bestaande koppelingen voor..." section in the wizard shows no results. This affects cross-supplier koppeling creation.
+
+**Acceptance Criteria:**
+- [ ] [UI] When opening the koppeling wizard from another supplier's application, existing koppelingen for that application are shown
+- [ ] [API] The koppeling search for existing koppelingen is not scoped by organisation/supplier (koppelingen from all suppliers are visible)
+- [ ] [UI] The "Reeds bestaande koppelingen voor [App]" section is populated when koppelingen exist for the target application
+- [ ] [HYBRID] A newly registered supplier can see koppelingen created by other suppliers when creating a new koppeling
+
+**Key Context from Comments:** Use case: supplier creates koppeling for "Centric Betalen" which was registered by a different supplier. The existing koppelingen for that app should be visible regardless of who created them. May be an RBAC scoping issue.
+
+---
+
+### #455: Tabblad koppelingen en contactpersonen worden publiekelijk niet getoond. RBAC?
+
+**Labels:** Aanbod, IGS nieuw
+**Test Step:** Step 12
+
+**Summary:** When viewing an application publicly (not logged in), the tabs "Koppelingen" and "Contactpersonen" are not shown. These should be visible to the public for supplier applications. When logged in as a different supplier, the Koppelingen tab IS shown.
+
+**Acceptance Criteria:**
+- [ ] [HYBRID] The "Koppelingen" tab is visible on application detail pages when not logged in
+- [ ] [HYBRID] The "Contactpersonen" tab is visible on application detail pages when not logged in
+- [ ] [API] Public (unauthenticated) API requests for application koppelingen return data
+- [ ] [API] Public (unauthenticated) API requests for application contactpersonen return data
+- [ ] [UI] Public view shows koppelingen and contactpersonen data matching what authenticated users see (minus edit controls)
+
+**Key Context from Comments:** This appears to be an RBAC issue where the public API does not return related objects (koppelingen, contactpersonen) for applications, even though these should be public data for supplier applications.
+
+---
+
+### #456: Consistentie in werking van wizards
+
+**Labels:** Aanbod, IGS nieuw
+**Test Step:** Step 7
+
+**Summary:** Wizard completion pages are inconsistent across application, dienst, and koppeling wizards. Issues include: button text ("aanmelden" vs "publiceren"), button styling (white vs blue), intermediate page before restarting wizard, and missing text on koppeling completion page.
+
+**Acceptance Criteria:**
+- [ ] [UI] All wizard completion pages use "Nieuw(e) [object] publiceren" button text (not "aanmelden")
+- [ ] [UI] The "Nieuwe [object] publiceren" button is blue (filled) on all wizard types including Koppeling and Gebruik
+- [ ] [UI] Clicking "Nieuwe applicatie publiceren" starts the wizard directly without an intermediate "Kies het type applicatie" page
+- [ ] [UI] The koppeling wizard completion page includes the text "Organisaties kunnen de koppeling bekijken en beoordelen"
+- [ ] [UI] All wizard completion pages have consistent layout and messaging structure
+
+**Key Context from Comments:** Related to #445 (dienst wizard issues). The reporter found 4 specific inconsistencies between the Applicatie, Dienst, and Koppeling wizard flows.
+
+---
+
+### #187: Tekstvoorstellen
+
+**Labels:** Tekstuele wijzigingen
+**Test Step:** Step 21
+
+**Summary:** Mega-issue containing 10 specific text change proposals across the application, serving as the key reference for many textual issues.
+
+**Acceptance Criteria:**
+- [ ] [UI] Contactpersoon text reads: "De geregistreerde contactpersoon is het eerste aanspreekpunt van de organisatie en beheerder van de gebruikers van de softwarecatalogus namens uw organisatie. Dit kan op een later moment nog gewijzigd worden."
+- [ ] [UI] Aanmelding succesvol page title reads: "Aanmelding succesvol!"
+- [ ] [UI] Aanmelding succesvol page body reads: "Beste van ,\n\nUw aanmelding voor de softwarecatalogus is in goede orde ontvangen. We hebben een bevestigingsmail gestuurd naar . Controleer uw inbox (en eventueel uw spam folder) voor deze bevestiging.\n\nEen beheerder beoordeeld de aanmelding. Zodra de aanmelding is goedgekeurd, ontvangt u een nieuwe e-mail met daarin uw inloggegevens en verdere instructies voor het gebruik van de softwarecatalogus.\n\nHeeft u vragen? Neem dan contact op met via softwarecatalogus@vng.nl"
+- [ ] [UI] Organisatie niet zichtbaar banner reads: "Uw organisatie is nog niet zichtbaar in de softwarecatalogus.\nBezoekers kunnen uw organisatie, producten en diensten nu nog niet vinden in de softwarecatalogus.\nMaak uw organisatie zichtbaar voor gemeenten en andere bezoekers door deze te publiceren.\n\nGa naar \"Mijn organisatie\" en klik rechtsboven op \"... Acties\".\nKies vervolgens \"Publiceren\" om uw organisatie zichtbaar te maken in de softwarecatalogus."
+- [ ] [UI] Dashboard welcome title reads: "Welkom in de Softwarecatalogus"
+- [ ] [UI] Dashboard welcome text reads: "Dit is de centrale plek om producten, applicaties, diensten en koppelingen te beheren. Door applicaties te koppelen aan GEMMA-referentiecomponenten wordt uw applicatielandschap gemapped op de GEMMA-referentiearchitectuur."
+- [ ] [UI] Contactpersoon toevoegen dialog title reads: "Gebruiker toevoegen"
+- [ ] [UI] Contactpersoon toevoegen dialog success text reads: "De gebruiker is succesvol toegevoegd."
+- [ ] [UI] Contactpersoon depubliceren dialog title reads: "Gebruiker uitschakelen"
+- [ ] [UI] Contactpersoon depubliceren dialog text reads: "Weet u zeker dat u deze gebruiker wilt depubliceren?\nTe depubliceren gebruiker:\n"
+- [x] [API] Link https://www.gemmaonline.nl/wiki/Overzicht_alle_referentiecomponenten is placed behind the text "alle referentiecomponenten" as a clickable link (bare URL removed)
+- [ ] [UI] "Contactpersonen" is renamed to "Gebruikers" in the left menu and page title
+- [ ] [UI] Explanatory text with icons for login vs non-login users is present on the Gebruikers page
+- [ ] [UI] Diensten registreren wizard title reads: "Dienst registreren"
+- [ ] [UI] Diensten registreren wizard subtitle reads: "Voer de gegevens van uw dienst in, selecteer de relevante producten en/of applicaties en controleer uw invoer."
+- [ ] [UI] Diensten registreren section header reads: "Registreer uw dienst" with text: "Registreer hier een dienst die uw organisatie aanbiedt -- bijvoorbeeld functioneel beheer, implementatieondersteuning, of licentiereseller.\nU kunt een dienst koppelen aan een product of applicatie van uw eigen organisatie, maar ook aan producten of applicaties van andere leveranciers.\nDoor uw diensten te registreren helpt u gemeenten en andere organisaties om snel te zien welke ondersteuning en expertise beschikbaar is."
+- [ ] [UI] Diensten registreren "Basisinformatie" section header reads: "Informatie over uw dienst" with text: "Vul de naam, website en een beschrijving van uw dienst in. Voeg eventueel een logo toe. Gebruik een herkenbare naam, zoals:\n\"Functioneel beheer voor Zaakgericht Werken\" of \"Reseller van Applicatie X\"."
+- [ ] [UI] Samenvatting placeholder reads: "Beschrijf in een of twee zinnen wat uw dienst inhoudt."
+- [ ] [UI] Search tooltip text reads: "De zoekfunctie doorzoekt de naam en beschrijvingen van items. Dit wordt gedaan op basis van vergelijkbare woorden. Met de filters kunnen de zoekresultaten verder worden verfijnd."
+- [ ] [UI] Application wizard success page title reads: "Uw applicatie is succesvol geregistreerd!"
+- [ ] [UI] Application wizard success page body includes explanation of what happens next (visibility in catalogus, management via dashboard)
+
+**Key Context from Comments:** This is a mega-issue with 10 distinct text changes. Items 1 (Contactpersoon text) and 3 (Organisatie niet zichtbaar banner) are marked as DONE per checkmarks in the issue. Related issues: #255, #268 (dashboard text). The diensten wizard texts should be consistent with #316-#328 wizard text standards.
+
+---
+
+### #316: Dienst toevoegen: Stap 1 Dienst zoeken
+
+**Labels:** Gebruik, Bevinding, Wizard, Dienst toevoegen
+**Test Step:** Step 10
+
+**Summary:** The "Dienst toevoegen" wizard step 1 must display specific text and search functionality for finding services linked to the municipality's own applications.
+
+**Acceptance Criteria:**
+- [ ] [UI] Form header title displays exactly: "Een dienst toevoegen"
+- [ ] [UI] Form header subtitle displays exactly: "Vul dit formulier in om de dienst toe te voegen aan uw applicatielandschap."
+- [ ] [UI] Section header displays exactly: "Toevoegen dienst"
+- [ ] [UI] Section text displays exactly: "Zoek naar diensten die op de applicaties in uw applicatielandschap worden uitgevoerd. Zoek op de naam van de betrokken applicatie.\n\nAlle relevantie diensten die relevant zijn voor uw eigen applicaties worden weergegeven.\nBestaat de dienst nog niet, dan kunt u deze toevoegen.\n\nNa het selecteren van de gewenste dienst kunt u in de volgende stappen aanvullende informatie opvoeren."
+- [ ] [UI] Blue info box title displays exactly: "Zoekpagina"
+- [ ] [UI] Blue info box text displays exactly: "U kunt ook starten vanaf de zoekpagina. Open de detailpagina van de gevonden dienst en kies 'Dienst toevoegen'."
+- [ ] [UI] A button with text "Ik kan de gewenste dienst niet vinden" is present
+- [ ] [UI] This step is skipped when adding from a detail application page
+
+**Key Context from Comments:** This step searches for existing services linked to the municipality's own applications. If not found, user can create a new one via a light version of the "Dienst publiceren" wizard. Source text from PowerPoint in #329.
+
+---
+
+### #317: Dienst toevoegen: Stap 2 Gebruiksinformatie
+
+**Labels:** Gebruik, Wizard, Dienst toevoegen
+**Test Step:** Step 10
+
+**Summary:** The "Dienst toevoegen" wizard step 2 must display specific text and fields for entering usage information about the service.
+
+**Acceptance Criteria:**
+- [ ] [UI] Form header title displays exactly: "Een dienst toevoegen"
+- [ ] [UI] Form header subtitle displays exactly: "Vul dit formulier in om de dienst toe te voegen aan uw applicatielandschap."
+- [ ] [UI] Section header displays exactly: "Toevoegen dienst"
+- [ ] [UI] Section text displays exactly: "U kunt hier de status van de dienst aangeven en een interne notitie en toevoegen voor uw collega's."
+- [ ] [UI] Blue info box title displays exactly: "Interne notitie"
+- [ ] [UI] Blue info box text displays exactly: "De interne notitie is alleen te lezen door gebruikers binnen uw organisatie."
+- [x] [API] A "Status" field is present (of de dienst wordt afgenomen)
+- [ ] [UI] An "Interne notitie" field is present
+
+**Key Context from Comments:** Source text from PowerPoint in #329. Part of the "Dienst toevoegen" wizard flow (issues #316-#318).
+
+---
+
+### #318: Dienst toevoegen: Stap 3 Controleren
+
+**Labels:** Gebruik, Wizard, Dienst toevoegen
+**Test Step:** Step 10
+
+**Summary:** The "Dienst toevoegen" wizard step 3 (review/check) must display specific text allowing the user to verify their input before submitting.
+
+**Acceptance Criteria:**
+- [ ] [UI] Form header title displays exactly: "Een dienst toevoegen"
+- [ ] [UI] Form header subtitle displays exactly: "Vul dit formulier in om de dienst toe te voegen aan uw applicatielandschap."
+- [ ] [UI] Section header displays exactly: "Controleer uw gegevens"
+- [ ] [UI] Section text displays exactly: "Controleer of het overzicht van de dienst volledig en juist is voordat u verder gaat.\n\nU kunt met Vorige terug naar de eerdere stappen.\n\nNa het registreren van de koppeling kunt u via uw \"Dashboard\" de koppeling opzoeken en indien gewenst aanpassen."
+- [ ] [UI] Blue info box title displays exactly: "Interne notitie"
+- [ ] [UI] Blue info box text displays exactly: "De interne notitie is alleen te lezen door gebruikers binnen uw organisatie."
+
+**Key Context from Comments:** Source text from PowerPoint in #329. Final review step of the "Dienst toevoegen" wizard (issues #316-#318).
+
+---
+
+### #319: Koppeling toevoegen: Stap 1 Koppeling zoeken
+
+**Labels:** Gebruik, Wizard, Koppeling toevoegen
+**Test Step:** Step 11
+
+**Summary:** The "Koppeling toevoegen" wizard step 1 must display specific text and search functionality for finding connections supported by the municipality's applications.
+
+**Acceptance Criteria:**
+- [ ] [UI] Form header title displays exactly: "Een koppeling toevoegen"
+- [ ] [UI] Form header subtitle displays exactly: "Vul dit formulier in om de koppeling toe te voegen aan uw applicatielandschap"
+- [ ] [UI] Section header displays exactly: "Een koppeling zoeken"
+- [ ] [UI] Section text displays exactly: "Zoek naar koppelingen die door de applicaties in uw applicatielandschap worden ondersteund. Zoek op de naam van een van de betrokken applicaties.\n\nAlle koppelingen die relevant zijn voor uw eigen applicaties en voor buitengemeentelijke voorzieningen worden weergegeven.\n\nBestaat de koppeling nog niet, dan kunt u deze toevoegen.\n\nSelecteer de gewenste koppeling door deze in de lijst aan te vinken."
+- [ ] [UI] Blue info box title displays exactly: "Zoekpagina"
+- [ ] [UI] Blue info box text displays exactly: "U kunt ook starten vanaf de zoekpagina. Open de detailpagina van de gevonden koppeling en kies 'Koppeling toevoegen'."
+- [ ] [UI] A button with text "Ik kan de gewenste koppeling niet vinden" is present
+- [ ] [UI] This step is skipped when adding from a koppeling page
+- [ ] [UI] This step is pre-filled when a koppeling is found from an application detail page
+- [ ] [UI] Section text uses "buitengemeentelijke voorzieningen" instead of "externe systemen of diensten"
+
+**Key Context from Comments:** Source text from PowerPoint in #329. Part of the "Koppeling toevoegen" wizard flow (issues #319-#322).
+
+---
+
+### #320: Koppeling toevoegen: Stap 2 Gebruiksinformatie
+
+**Labels:** Gebruik, Wizard, Koppeling toevoegen
+**Test Step:** Step 11
+
+**Summary:** The "Koppeling toevoegen" wizard step 2 must display specific text and fields for entering usage information about the connection.
+
+**Acceptance Criteria:**
+- [ ] [UI] Form header title displays exactly: "Een koppeling toevoegen"
+- [ ] [UI] Form header subtitle displays exactly: "Vul dit formulier in om de koppeling toe te voegen aan uw applicatielandschap"
+- [ ] [UI] Section header displays exactly: "Gebruiksinformatie"
+- [ ] [UI] Section text displays exactly: "Selecteer de status. Ook kunt u een interne notitie toevoegen voor uw collega's."
+- [ ] [UI] Blue info box title displays exactly: "Interne notitie"
+- [ ] [UI] Blue info box text displays exactly: "De interne notitie is alleen zichtbaar voor de eigen organisatie. Gebruikers van buiten de organisatie zien deze niet."
+- [x] [API] A "Status" field is present with default value "In gebruik"
+- [x] [API] A "Startdatum status" field is present
+- [ ] [UI] An "Interne notitie" field is present
+- [x] [API] "Startdatum status" field defaults to today's date but allows manual entry of past dates
+
+**Key Context from Comments:** Source text from PowerPoint in #329. Part of the "Koppeling toevoegen" wizard flow (issues #319-#322).
+
+---
+
+### #321: Koppeling toevoegen: Stap 3 Deelnemer
+
+**Labels:** Gebruik, Wizard, Koppeling toevoegen
+**Test Step:** Step 11
+
+**Summary:** The "Koppeling toevoegen" wizard step 3 (participants) is ONLY shown for samenwerkingen (collaborations) and must display specific text for selecting participants.
+
+**Acceptance Criteria:**
+- [ ] [UI] This step is ONLY visible for samenwerkingen (collaborations), not for individual gemeenten
+- [ ] [UI] Form header title displays exactly: "Een koppeling toevoegen"
+- [ ] [UI] Form header subtitle displays exactly: "Vul dit formulier in om de koppeling toe te voegen aan uw applicatielandschap"
+- [ ] [UI] Section header displays exactly: "Deelnemers toevoegen"
+- [ ] [UI] Section text displays exactly: "Selecteer de deelnemers van de samenwerking, die gebruik maken van deze koppeling.\n\nDe koppeling wordt getoond in het applicatielandschap van de geselecteerde deelnemer(s)."
+- [ ] [UI] A "Selecteer alle" button is present
+- [ ] [UI] A "Deselecteer alle" button is present
+
+**Key Context from Comments:** Source text from PowerPoint in #329. This step only applies to samenwerkingen. Part of the "Koppeling toevoegen" wizard flow (issues #319-#322).
+
+---
+
+### #322: Koppeling toevoegen: Stap 4 Controleren
+
+**Labels:** Gebruik, Wizard, Koppeling toevoegen
+**Test Step:** Step 11
+
+**Summary:** The "Koppeling toevoegen" wizard step 4 (review/check) must display specific text allowing the user to verify their input before submitting.
+
+**Acceptance Criteria:**
+- [ ] [UI] Form header title displays exactly: "Een koppeling toevoegen"
+- [ ] [UI] Form header subtitle displays exactly: "Vul dit formulier in om de koppeling toe te voegen aan uw applicatielandschap"
+- [ ] [UI] Section header displays exactly: "Controleer uw gegevens"
+- [ ] [UI] Section text displays exactly: "Controleer of het overzicht van de koppeling volledig en juist is voordat u verder gaat.\n\nU kunt met Vorige terug naar de eerdere stappen.\n\nNa het registreren van de koppeling kunt u via uw \"Dashboard\" de koppeling opzoeken en indien gewenst aanpassen."
+- [ ] [UI] Blue info box text displays exactly: "De koppeling wordt toegevoegd aan uw applicatielandschap.\n\nUw gebruiksinformatie is zichtbaar voor andere gemeenten en samenwerkingen om kennisdeling te bevorderen. De leverancier ziet dat u de koppeling gebruikt.\n\nDe interne notitie is uitsluitend voor intern gebruik."
+
+**Key Context from Comments:** Source text from PowerPoint in #329. Final review step of the "Koppeling toevoegen" wizard (issues #319-#322).
+
+---
+
+### #323: Applicatie toevoegen: Stap 1 Applicatie zoeken
+
+**Labels:** Gebruik, Wizard, Applicatie toevoegen
+**Test Step:** Step 10
+
+**Summary:** The "Applicatie toevoegen" wizard step 1 must display specific text and search functionality for finding applications to add to the municipality's application landscape.
+
+**Acceptance Criteria:**
+- [ ] [UI] Form header title displays exactly: "Een applicatie toevoegen"
+- [ ] [UI] Form header subtitle displays exactly: "Vul dit formulier in om de applicatie toe te voegen aan uw applicatielandschap"
+- [ ] [UI] Section header displays exactly: "Toevoegen applicatie"
+- [ ] [UI] Section text displays exactly: "Selecteer de applicatie door te zoeken op de applicatie- en leveranciersnaam.\nAls u de applicatie niet vind, dan kan deze worden toegevoegd aan de centrale lijst"
+- [ ] [UI] Blue info box title displays exactly: "Zoekpagina"
+- [ ] [UI] Blue info box text displays exactly: "U kunt ook de zoekpagina gebruiken. Open de detailpagina van de gevonden applicatie en klik op 'Applicatie toevoegen'."
+- [ ] [UI] A button with text "Ik kan de gewenste applicatie niet vinden" is present (opens step 1.1, see #328)
+
+**Key Context from Comments:** Source text from PowerPoint in #329. Part of the "Applicatie toevoegen" wizard flow (issues #323-#327). The "Ik kan de gewenste applicatie niet vinden" button triggers the sub-step defined in #328.
+
+---
+
+### #324: Applicatie toevoegen: Stap 2 Gebruiksinformatie
+
+**Labels:** Gebruik, Wizard, Applicatie toevoegen
+**Test Step:** Step 10
+
+**Summary:** The "Applicatie toevoegen" wizard step 2 must display specific text and fields for entering usage information about the application.
+
+**Acceptance Criteria:**
+- [ ] [UI] Form header title displays exactly: "Een applicatie toevoegen"
+- [ ] [UI] Form header subtitle displays exactly: "Vul dit formulier in om de applicatie toe te voegen aan uw applicatielandschap"
+- [ ] [UI] Section header displays exactly: "Gebruiksinformatie"
+- [ ] [UI] Section text displays exactly: "Selecteer de gebruikte hosting en versie. Ook kunt u een interne notitie toevoegen voor uw collega's."
+- [ ] [UI] Blue info box title displays exactly: "Interne notitie"
+- [ ] [UI] Blue info box text displays exactly: "De interne notitie is alleen zichtbaar voor de eigen organisatie. Gebruikers van buiten de organisatie zien deze niet."
+- [ ] [UI] A "Hosting" field is present
+- [ ] [UI] A "Versie" field is present (only shown for On-premise; SaaS uses default)
+- [x] [API] A "Status" field is present with default value "in productie" and is required (verplicht)
+- [x] [API] A "Startdatum status" field is present
+- [ ] [UI] An "Interne notitie" field is present
+- [x] [API] "Startdatum status" field defaults to today's date (timestamp) but allows manual entry of past dates
+
+**Key Context from Comments:** Source text from PowerPoint in #329. Part of the "Applicatie toevoegen" wizard flow (issues #323-#327).
+
+---
+
+### #325: Applicatie toevoegen: Stap 3 Referentiecomponenten
+
+**Labels:** Gebruik, Wizard, Applicatie toevoegen
+**Test Step:** Step 10
+
+**Summary:** The "Applicatie toevoegen" wizard step 3 must display specific text and fields for linking the application to GEMMA reference components.
+
+**Acceptance Criteria:**
+- [ ] [UI] Form header title displays exactly: "Een applicatie toevoegen"
+- [ ] [UI] Form header subtitle displays exactly: "Vul dit formulier in om de applicatie toe te voegen aan uw applicatielandschap"
+- [ ] [UI] Section header displays exactly: "Koppel de applicatie aan referentiecomponenten"
+- [ ] [UI] Section text displays exactly: "Door de applicatie te koppelen aan referentiecomponenten, maakt u inzichtelijk waarvoor u de applicatie gebruikt. Dit bevordert kennisdeling met andere gemeenten. Een overzicht van alle referentiecomponenten vindt u op GEMMA Online."
+- [ ] [UI] The link https://www.gemmaonline.nl/wiki/Overzicht_alle_referentiecomponenten is present (either behind the text "alle referentiecomponenten" as a clickable link, or visible)
+- [ ] [UI] A "Selecteer referentiecomponenten" field is present (multi-select from leverancier's list)
+- [ ] [UI] A "Referentiecomponenten toevoegen" field is present (multi-select from GEMMA)
+
+**Key Context from Comments:** Source text from PowerPoint in #329. Part of the "Applicatie toevoegen" wizard flow (issues #323-#327). Related to #187 item 7 (referentiecomponenten link formatting).
+
+---
+
+### #326: Applicatie toevoegen: Stap 4 Deelnemer
+
+**Labels:** Gebruik, Wizard, Applicatie toevoegen
+**Test Step:** Step 10
+
+**Summary:** The "Applicatie toevoegen" wizard step 4 (participants) is ONLY shown for samenwerkingen (collaborations) and must display specific text for selecting participants.
+
+**Acceptance Criteria:**
+- [ ] [UI] This step is ONLY visible for samenwerkingen (collaborations), not for individual gemeenten
+- [ ] [UI] Form header title displays exactly: "Een applicatie toevoegen"
+- [ ] [UI] Form header subtitle displays exactly: "Vul dit formulier in om de applicatie toe te voegen aan uw applicatielandschap"
+- [ ] [UI] Section header displays exactly: "Deelnemers toevoegen"
+- [ ] [UI] Section text displays exactly: "Selecteer de deelnemers van de samenwerking, die gebruik maken van deze applicatie.\n\nDe applicatie wordt getoond in het applicatielandschap van de geselecteerde deelnemer(s)."
+- [ ] [UI] A "Selecteer alle" button is present
+- [ ] [UI] A "Deselecteer alle" button is present
+
+**Key Context from Comments:** Source text from PowerPoint in #329. This step only applies to samenwerkingen. Part of the "Applicatie toevoegen" wizard flow (issues #323-#327).
+
+---
+
+### #327: Applicatie toevoegen: Stap 5 Controleren
+
+**Labels:** Gebruik, Wizard, Applicatie toevoegen
+**Test Step:** Step 10
+
+**Summary:** The "Applicatie toevoegen" wizard step 5 (review/check) must display specific text allowing the user to verify their input before submitting.
+
+**Acceptance Criteria:**
+- [ ] [UI] Form header title displays exactly: "Een applicatie toevoegen"
+- [ ] [UI] Form header subtitle displays exactly: "Vul dit formulier in om de applicatie toe te voegen aan uw applicatielandschap"
+- [ ] [UI] Section header displays exactly: "Controleer uw gegevens"
+- [ ] [UI] Section text displays exactly: "Controleer of het overzicht van de applicatie volledig en juist is voordat u verder gaat.\n\nU kunt met Vorige terug naar de eerdere stappen.\n\nNa het registreren van de applicatie kunt u via uw \"Dashboard\" de applicatie opzoeken en indien gewenst aanpassen."
+- [ ] [UI] Blue info box text displays exactly: "De applicatie wordt toegevoegd aan uw applicatielandschap.\n\nUw gebruiksinformatie is zichtbaar voor andere gemeenten en samenwerkingen om kennisdeling te bevorderen. Daarnaast kan de leverancier zien dat u hun applicatie gebruikt.\n\nDe interne notitie is uitsluitend voor intern gebruik."
+
+**Key Context from Comments:** Source text from PowerPoint in #329. Final review step of the "Applicatie toevoegen" wizard (issues #323-#327).
+
+---
+
+### #328: Applicatie toevoegen: Stap 1.1 Nieuwe applicatie opvoeren
+
+**Labels:** Gebruik, Bevinding, Wizard, Applicatie toevoegen
+**Test Step:** Step 10
+
+**Summary:** The "Applicatie toevoegen" wizard sub-step 1.1 is triggered when the user cannot find the desired application in step 1. It must display specific text and fields for creating a new application entry.
+
+**Acceptance Criteria:**
+- [ ] [UI] Form header title displays exactly: "Een nieuwe applicatie toevoegen"
+- [ ] [UI] Form header subtitle displays exactly: "Vul dit formulier in om een nieuwe applicatie toe te voegen aan uw applicatielandschap"
+- [ ] [UI] Section header displays exactly: "Publiceren applicatie"
+- [ ] [UI] Section text displays exactly: "Vul de gegevens in voor de applicatie. Na het opvoeren van de applicatie is deze ook zichtbaar voor andere gemeenten, zodat zij deze ook kunnen opnemen in hun applicatielandschap. Hiermee worden dubbele registraties voorkomen."
+- [ ] [UI] Blue info box title displays exactly: "Applicatie zoeken"
+- [ ] [UI] Blue info box text displays exactly: "Weet je zeker dat de applicatie niet al bestaat?\nGa naar de zoekpagina en zoek op de naam van applicatie of leverancier. Zoek ook op andere schrijfwijzen."
+- [ ] [UI] A "Selecteren van leverancier" field is present
+- [ ] [UI] A "Naam leverancier" field is present (shown when creating a new leverancier)
+- [ ] [UI] A "Website leverancier" field is present (shown when creating a new leverancier)
+
+**Key Context from Comments:** Source text from PowerPoint in #329. This sub-step is reached from #323 when the user clicks "Ik kan de gewenste applicatie niet vinden". After creating the application, it becomes visible for other municipalities as well.
+
+---
+
+### #329: TekstenSWC.2.5.laasteversie.defintief.
+
+**Labels:** Tekstuele wijzigingen
+**Test Step:** General
+
+**Summary:** This is the SOURCE DOCUMENT for all wizard text, containing the PowerPoint file (Wizards applicaties.pptx) from December 11 that is the authoritative source for all text in the Softwarecatalogus wizards.
+
+**Acceptance Criteria:**
+- [ ] [UI] All text in the "Dienst toevoegen" wizard (steps 1-3) matches the attached PowerPoint from 11 December 2025 (verified via #316, #317, #318)
+- [ ] [UI] All text in the "Koppeling toevoegen" wizard (steps 1-4) matches the attached PowerPoint from 11 December 2025 (verified via #319, #320, #321, #322)
+- [ ] [UI] All text in the "Applicatie toevoegen" wizard (steps 1-5 + step 1.1) matches the attached PowerPoint from 11 December 2025 (verified via #323, #324, #325, #326, #327, #328)
+- [ ] [UI] All form header titles, subtitles, section headers, section texts, blue info box titles, and blue info box texts match their respective issue specifications
+
+**Key Context from Comments:** Issues #316-#328 are the individual implementations of this source document. When verifying, cross-reference each wizard step against this PowerPoint. Any discrepancies between the PowerPoint and the individual issues should be flagged.
+
+---
+
+### #415: Vraag: Spelling "Applicatie informatie" vs "Applicatieinformatie"
+
+**Labels:** help wanted, Aanbod
+**Test Step:** Step 7
+
+**Summary:** Decision on the correct spelling of "applicatie-informatie". The PowerPoint says "Applicatie informatie" (two words), but the correct Dutch spelling is with a hyphen. Decision from Makkmetp comment: use "applicatie-informatie" (hyphenated, lowercase).
+
+**Acceptance Criteria:**
+- [ ] [UI] All instances in the UI use "applicatie-informatie" (hyphenated, lowercase)
+- [ ] [UI] No instances of "Applicatie informatie" (two words without hyphen) appear anywhere
+- [ ] [UI] No instances of "Applicatieinformatie" (one word without hyphen) appear anywhere
+- [ ] [UI] This applies to wizard steps, detail pages, management tables, and any other location where this term appears
+
+**Key Context from Comments:** Decision from Makkmetp: use "applicatie-informatie" (hyphenated, lowercase). This overrides the PowerPoint which says "Applicatie informatie" (two words).
+
+---
+
+### #420: [Bug] Applicaties die door gemeenten zijn aangemaakt verschijnen niet in het aanbod-endpoint
+
+**Labels:** Gebruik, Testbevindingen Wilco
+**Test Step:** Step 12
+
+**Summary:** Applications created by municipalities do not appear in the supply (aanbod) endpoint.
+
+**Acceptance Criteria:**
+- [x] [API] Applications created by gemeenten are visible via the aanbod endpoint
+- [x] [API] Adjustment implemented so these applications also appear in the aanbod-endpoint
+
+**Key Context from Comments:** Bug reported during testing. Municipality-created applications are missing from the aanbod endpoint entirely.
+
+---
+
+### #419: Standaarden en standaard-versie lijken niet goed gekoppeld
+
+**Labels:** bug
+**Test Step:** Step 16
+
+**Summary:** Standards (Standaard) and standard versions (Standaard-versie) are not properly linked, causing version IDs to display instead of names.
+
+**Acceptance Criteria:**
+- [x] [API] Standard version is linked to its parent standard
+- [ ] [UI] Standard names display correctly (not version IDs)
+- [x] [API] Connection chain: ReferentieComponent -> Standaard -> Standaard-versie is complete
+
+**Key Context from Comments:** Example: AI-verordering standard has no connection with "AI-verordering (Actueel)" version. But the connection exists between ReferentieComponent "AI-Component" and Standaard "AI-verordering". Unclear if the issue is in GEMMA data or the data model.
+
+---
+
+### #418: [Bug] Performance: applicaties dropdown laadt traag bij toevoegen dienst wizard + onnodige product call (404)
+
+**Labels:** Testbevindingen Wilco
+**Test Step:** Step 10
+
+**Summary:** N+1 API calls pattern for the applications dropdown in the "dienst toevoegen" wizard causes slow loading. Plus a spurious 404 call to a product endpoint.
+
+**Acceptance Criteria:**
+- [x] [API] Applications dropdown loads in a single API call (not N+1 pattern)
+- [x] [API] No 404 error for product endpoint
+- [ ] [UI] Dropdown loads within reasonable time (<2 seconds)
+- [x] [API] Applications dropdown uses batch API call instead of individual calls per application
+- [ ] [UI] Total dropdown load time for 6+ applications is under 3 seconds
+
+**Key Context from Comments:** Performance issue with applications dropdown making excessive API calls when loading in the dienst wizard.
+
+---
+
+### #405: Applicatie: applicatie verwijderen die door dienst ondersteund wordt
+
+**Labels:** Aanbod, Wijziging
+**Test Step:** Step 7
+
+**Summary:** When deleting an application that is used by a service, the delete dialog incorrectly says the application is not used anywhere. After deletion, inconsistencies appear.
+
+**Acceptance Criteria:**
+- [ ] [UI] Delete dialog correctly warns when an application is used by a dienst
+- [x] [API] If deletion proceeds, the application is removed consistently from both overview table and detail pages
+- [ ] [UI] Dependencies are clearly shown before deletion
+- [ ] [UI] After deleting an application, it is consistently removed from BOTH the diensten management table AND detail pages (no orphan references)
+- [x] [API] A flow decision is implemented for: should an application be deletable when diensten from OTHER leveranciers reference it?
+
+**Key Context from Comments:** Related to #403 (deletion text changes). The delete confirmation should show which diensten reference the application.
+
+---
+
+### #398: Zoeken: Filter met UUID's onder leveranciers
+
+**Labels:** Aanbod
+**Test Step:** Step 14
+
+**Summary:** UUIDs appear instead of supplier names in the leveranciers (suppliers) search filter.
+
+**Acceptance Criteria:**
+- [ ] [API] Leveranciers filter shows human-readable supplier names, not UUIDs
+- [ ] [UI] All suppliers in the filter have proper names
+- [x] [API] No empty or UUID-only entries in the leveranciers dropdown
+- [x] [API] All modules in the import reference existing organizations (no orphan references to non-existent organization UUIDs)
+- [x] [API] Frontend does not make extra API calls to resolve missing organization names
+- [x] [API] If an organization UUID cannot be resolved, a human-readable fallback is shown (not the raw UUID)
+
+**Key Context from Comments:** Related to #333 (UUIDs in filters). Part of the broader UUID-in-filters problem affecting search experience. VNG confirms UUID for an existing leverancier still appears in the filter (2026-03-02).
+
+---
+
+### #339: Activeren gebruikers
+
+**Labels:** Bevinding
+**Test Step:** Step 3
+
+**Summary:** Findings about user activation process.
+
+**Acceptance Criteria:**
+- [ ] [UI] User activation process works correctly
+- [ ] [UI] Activated users can log in
+- [x] [API] Activation status is reflected in the UI
+- [x] [API] Activating a user does NOT produce a 500 error
+- [ ] [UI] After activating an organization, the contact person is correctly converted to a user without disappearing from the contacts list
+- [ ] [UI] Activated user receives correct roles (no duplicate role assignments)
+- [x] [API] After organization activation, the filter state is preserved (organizations list does not reload unfiltered)
+- [x] [API] User activation works for both newly created organizations AND imported/data-migrated organizations
+
+**Key Context from Comments:** Findings during testing of user activation flow.
+
+---
+
+### #338: Dashboard en Inloggen
+
+**Labels:** Bevinding
+**Test Step:** Step 4
+
+**Summary:** Dashboard and login findings.
+
+**Acceptance Criteria:**
+- [ ] [UI] Dashboard loads correctly after login
+- [ ] [UI] All dashboard elements are visible and functional
+- [ ] [UI] Dashboard suggestions are shown correctly after first login (not inverted/empty)
+- [ ] [UI] Page load time after login is reasonable (within a few seconds)
+- [ ] [UI] Accepting/adopting a suggestion does not produce a 404 error
+
+**Key Context from Comments:** Findings during testing of dashboard and login flow.
+
+---
+
+### #336: Views
+
+**Labels:** Bevinding
+**Test Step:** Step 19
+
+**Summary:** Findings about architecture views display.
+
+**Acceptance Criteria:**
+- [ ] [UI] Architecture views load and display correctly
+- [ ] [UI] View content matches expected data
+- [x] [API] "Identificatie" column shows consistent values (either names or UUIDs, not a mix of both)
+- [ ] [UI] Views are accessible from the correct menu location
+
+**Key Context from Comments:** Findings during testing of architecture views (AMEFF).
+
+---
+
+### #335: Diensten Wizards
+
+**Labels:** Bevinding
+**Test Step:** Step 9
+
+**Summary:** Findings about the service (diensten) wizard flow.
+
+**Acceptance Criteria:**
+- [ ] [UI] Diensten wizard flow works correctly from start to finish
+- [ ] [UI] All wizard steps are accessible and functional
+
+**Key Context from Comments:** Findings during testing of the diensten wizard.
+
+---
+
+### #333: UUID uit filters Refcomp en standaarden opzoeken en corrigeren in datamigratie
+
+**Labels:** Aanbod, Datamigratie
+**Test Step:** Step 14
+
+**Summary:** UUIDs appearing in reference component and standards filters need to be corrected in data migration.
+
+**Acceptance Criteria:**
+- [ ] [UI] Reference component filter shows human-readable names
+- [ ] [UI] Standards filter shows human-readable names
+- [ ] [UI] No UUIDs visible in any filter dropdown
+- [x] [API] Referentiecomponent UUIDs for Regelbeheercomponent, Wkpb-component, and Sonderingsregistercomponent are resolved (merged or removed)
+- [x] [API] Standaardversie UUIDs for "StUF Geo IMGeo (actueel)", "Samenwerken (actueel)", and "StUF LVBAG 2.06" are removed from compliancy data
+- [x] [API] Leverancier filter no longer contains UUIDs (only readable supplier names)
+
+**Key Context from Comments:** Data migration needs correction to resolve UUIDs to readable names in filters. Related to #398.
+
+---
+
+### #331: Koppeling relatie Applicatie
+
+**Labels:** (none)
+**Test Step:** Step 11
+
+**Summary:** Connection (koppeling) relationship with application needs verification.
+
+**Acceptance Criteria:**
+- [ ] [UI] Koppelingen correctly reference their linked applications
+- [ ] [UI] Application names display correctly in koppeling views
+
+**Key Context from Comments:** Verifying the connection relationship between koppelingen and applicaties.
+
+---
+
+### #311: Altijd inlog-account en -organisatie tonen
+
+**Labels:** Gebruik
+**Test Step:** Step 4
+
+**Summary:** Always show the logged-in account and organization in the UI.
+
+**Acceptance Criteria:**
+- [ ] [UI] Logged-in user name is always visible in the UI
+- [ ] [UI] Active organization name is always visible
+- [ ] [UI] Both are shown consistently across all pages
+
+**Key Context from Comments:** Users need to always see which account and organization they are working under.
+
+---
+
+### #261: Wizards: pas te testen na RBAC
+
+**Labels:** Gebruik, Wizard
+**Test Step:** Step 7
+
+**Summary:** Wizard testing was blocked pending RBAC implementation. Now that RBAC is in place, wizards should be fully testable.
+
+**Acceptance Criteria:**
+- [ ] [UI] Wizards appear for authorized users based on their role
+- [ ] [UI] Aanbod-beheerder sees supplier wizards
+- [ ] [UI] Gebruik-beheerder sees municipality wizards
+- [ ] [UI] Unauthorized users do not see wizards they shouldn't access
+
+**Key Context from Comments:** Testing was previously blocked on RBAC. Now that RBAC is implemented, wizards can be fully tested.
+
+---
+
+### #255: Beheer pagina's -- Dashboard: Tekst: welkom in de softwarecatalogus opleveren
+
+**Labels:** Afschalen Producten, Bevinding, Tekstuele wijzigingen, Beheer
+**Test Step:** Step 21
+
+**Summary:** Dashboard welcome text needs to be delivered and displayed correctly. Cross-references #187 for exact text.
+
+**Acceptance Criteria:**
+- [ ] [UI] Dashboard shows "Welkom in de Softwarecatalogus" as heading
+- [ ] [UI] Dashboard shows: "Dit is de centrale plek om producten, applicaties, diensten en koppelingen te beheren. Door applicaties te koppelen aan GEMMA-referentiecomponenten wordt uw applicatielandschap gemapped op de GEMMA-referentiearchitectuur."
+- [ ] [UI] Text is consistent with #187 text proposals
+
+**Key Context from Comments:** Related to #187 (Tekstvoorstellen) and #268 (Dashboard text). Also resolved by #410 (supplier-specific welcome text). The exact text was defined in #187 with screenshots.
+
+---
+
+### #268: Tekst aanleveren: Na inloggen: Dashboard tekst aanpassen
+
+**Labels:** Tekstuele wijzigingen, Cms
+**Test Step:** Step 21
+
+**Summary:** Dashboard text after login needs adjustment. Cross-references #187 for exact text.
+
+**Acceptance Criteria:**
+- [ ] [UI] Post-login dashboard displays the correct welcome text (per #187)
+- [ ] [UI] Dashboard text is role-specific if applicable
+- [ ] [UI] Text styling matches the design
+
+**Key Context from Comments:** Related to #187 (Tekstvoorstellen) and #255 (Dashboard welcome text). Part of the broader text alignment effort.
+
+---
+
+### #209: [Bug] De help knop op de NC dashboard organisaties pagina gaat naar een niet bestaande pagina toe
+
+**Labels:** bug, Testbevindingen Wilco
+**Test Step:** Step 3
+
+**Summary:** Help button on NC dashboard organizations page links to a non-existent page.
+
+**Acceptance Criteria:**
+- [ ] [UI] Help button links to an existing, accessible page
+- [ ] [UI] Help content is relevant to the organizations overview
+
+**Key Context from Comments:** Reported by Wilco during testing. The help button navigates to a 404 page.
+
+---
+
+### #208: [Bug] NC Dashboard organisatie overzicht (table ipv cards) laat alleen het veld ID zien
+
+**Labels:** bug, Testbevindingen Wilco
+**Test Step:** Step 3
+
+**Summary:** When switching to table view for organizations in NC Dashboard, only the ID field is shown.
+
+**Acceptance Criteria:**
+- [x] [API] Table view shows relevant fields (name, type, status) not just ID
+- [ ] [UI] Table view is usable for managing organizations
+
+**Key Context from Comments:** Reported by Wilco. Table view regression showing only ID column.
+
+---
+
+### #205: [Bug] Een gedepubliceerde applicatie is nog te vinden als je er naar zoekt
+
+**Labels:** bug, Testbevindingen Wilco
+**Test Step:** Step 14
+
+**Summary:** A depublished application can still be found via search.
+
+**Acceptance Criteria:**
+- [ ] [UI] Depublished applications do NOT appear in public search results
+- [ ] [UI] Only published applications are searchable by the general public
+- [ ] [UI] Admin/owner can still find depublished applications in their management view
+
+**Key Context from Comments:** Reported by Wilco. Depublished applications should be hidden from public search but remain accessible in management views.
+
+---
+
+### #231: [Bug] AMEFF exports (van views) geven een foutmelding als je deze probeert te importeren in Archi
+
+**Labels:** bug, Testbevindingen Wilco
+**Test Step:** Step 24
+
+**Summary:** AMEFF exports of views produce errors when importing into Archi.
+
+**Acceptance Criteria:**
+- [x] [API] AMEFF export generates valid ArchiMate exchange format files
+- [x] [API] Exported files can be imported into Archi without errors
+- [x] [API] All expected elements and relationships are present in the export
+
+**Key Context from Comments:** Reported by Wilco. The AMEFF export format is not fully compatible with Archi import requirements.
+
+---
+
+### #188: Aanmeldproces
+
+**Labels:** Organisatie en configuratie, Restpunt
+**Test Step:** Step 2
+
+**Summary:** Registration/sign-up process findings.
+
+**Acceptance Criteria:**
+- [ ] [UI] Registration process works end-to-end
+- [ ] [UI] User receives confirmation after registration
+- [ ] [UI] Admin can activate registered organizations
+
+**Key Context from Comments:** Outstanding findings from the registration process testing.
+
+---
+
+### #182: [Taak] 'Algemene voorwaarden', 'Privacyverklaring', 'Disclaimer' and FAQ
+
+**Labels:** nonblock, Testbevindingen Wilco
+**Test Step:** Step 21
+
+**Summary:** Legal pages (Terms, Privacy, Disclaimer, FAQ) need to be created and accessible.
+
+**Acceptance Criteria:**
+- [ ] [UI] "Algemene voorwaarden" page exists and is accessible
+- [ ] [UI] "Privacyverklaring" page exists and is accessible
+- [ ] [UI] "Disclaimer" page exists and is accessible
+- [ ] [UI] FAQ page exists and is accessible
+- [ ] [UI] All pages are linked from the footer or relevant navigation
+- [ ] [UI] Content is filled in (not placeholder text)
+
+**Key Context from Comments:** Reported by Wilco. Legal pages are required for go-live. Related to #397 (CMS page editing) and #409 (footer links).
+
+---
+
+### #342: Zoeken: op kaartjes aantal referentiecomponenten duidelijk maken
+
+**Labels:** Gebruik, Zoeken
+**Test Step:** Step 14
+
+**Summary:** When an application card has more reference components than can be displayed, the overflow should be handled clearly.
+
+**Acceptance Criteria:**
+- [ ] [UI] When an application card has more referentiecomponenten than can be displayed, a total count is shown (e.g., "+5 meer")
+- [ ] [UI] A "Meer" link or count navigates to the application detail page where all referentiecomponenten are visible
+- [ ] [UI] All referentiecomponenten are visible on the detail page
+
+---
+
+### #411: Vraag: Required eisen uitgezet voor dataimport
+
+**Labels:** Vraag, Data Import
+**Test Step:** Step 19
+
+**Summary:** Required fields (beschrijvingKort, website for module; naam, moduleA, moduleB for koppeling; website for organisatie) were set to non-required to accommodate import data with null values. These constraints should be re-enabled after a clean re-import.
+
+**Acceptance Criteria:**
+- [x] [API] A new data import file fills in beschrijvingKort and website for all modules
+- [x] [API] A new data import file provides naam, moduleA, and moduleB for all koppelingen
+- [x] [API] A new data import file provides website for all organisaties
+- [x] [API] After re-import with complete data, the required constraints on these fields are re-enabled in the schema
+- [ ] [UI] Wizards properly enforce required field validation after constraints are restored
+
+---
+
+### #412: Vraag: Niet alle AMEF views hebben documentatie
+
+**Labels:** Vraag, Architectuur
+**Test Step:** Step 19
+
+**Summary:** 5 specific AMEF views lack descriptions and display "geen beschrijving beschikbaar voor deze view" in the frontend.
+
+**Acceptance Criteria:**
+- [ ] [UI] Referentiecomponentenlandschap view has a description
+- [ ] [UI] Test extra componenten view has a description
+- [ ] [UI] Basisbeveiligingsniveau views (both) have descriptions
+- [ ] [UI] Referentiecomponenten en ondersteuning BIO maatregelen view has a description
+- [ ] [UI] No view displays "geen beschrijving beschikbaar voor deze view" after descriptions are provided
+
+---
+
+### #413: Vraag: Views testen vs softwarecatalogus scope
+
+**Labels:** Vraag, Architectuur
+**Test Step:** Step 19
+
+**Summary:** Clarification on which AMEF views should be included in the softwarecatalogus. 22 views match the agreed filter. Test views should not appear in production.
+
+**Acceptance Criteria:**
+- [x] [API] Only the 22 views matching the agreed filter `publiceren=Softwarecatalogus+en+GEMMA+Online+en+redactie` are displayed
+- [ ] [UI] Views with duplicate titelViewSwc are clearly distinguishable in the UI
+- [x] [API] Test views not matching the production filter do not appear in the published softwarecatalogus
+
+---
+
+### #414: Vraag: Mogen deelnemers gebruiksobjecten lezen
+
+**Labels:** Vraag, RBAC
+**Test Step:** Step 12
+
+**Summary:** Whether participants (deelnemers) in usage objects can read those objects when they are not the owner.
+
+**Acceptance Criteria:**
+- [x] [API] Deelnemers (participants) in a gebruiksobject can read the object even when they are not the owner
+- [x] [API] Gemeenten and samenwerkingen can view each other's usage data where they are deelnemers
+
+---
+
+### #417: Vraag: Andere email adressen voor contactpersonen
+
+**Labels:** Vraag, Data Import
+**Test Step:** Step 3
+
+**Summary:** Imported contact person email addresses use Gmail aliases (e.g., test.vng.swc+Bre@gmail.com) which are OAuth-incompatible. Proper email addresses should be provided for the definitive import.
+
+**Acceptance Criteria:**
+- [x] [API] Imported contact persons have OAuth-compatible email addresses (not Gmail aliases with + notation)
+- [x] [API] When activating a contact person with an invalid/incompatible email, a clear error message is shown
+- [x] [API] Email can be changed before activation as a workaround
+
+**Key Context from Comments:** Priority is low since the definitive import will not have invalid emails. This is a data quality issue, not a code issue.
+
+---
+
+### #431: Aanmeldproces: tussenvoegsel niet meer aanwezig
+
+**Labels:** IGS nieuw
+**Test Step:** Step 3
+
+**Summary:** The "tussenvoegsel" (middle name prefix) field is no longer present in the registration/signup process. It was available in an earlier phase (see issue 139) and needs to be restored.
+
+**Acceptance Criteria:**
+- [ ] [UI] The registration form includes a "Tussenvoegsel" field between Voornaam and Achternaam
+- [x] [API] The tussenvoegsel is saved correctly when registering a new account
+- [ ] [UI] The tussenvoegsel appears in the user's profile after registration
+- [ ] [UI] Existing users with a tussenvoegsel still display it correctly
+
+**Key Context from Comments:** Screenshots show the field is completely missing from the current signup form.
+
+---
+
+### #432: Koppeling: Naamgeving van koppeling niet consistent
+
+**Labels:** Organisatie en configuratie, IGS nieuw
+**Test Step:** Step 11
+
+**Summary:** Koppeling names are displayed inconsistently across different pages. The registered name, koppelingen overview, applicatie overview column, and delete dialog all show different names for the same koppeling. Related to import issues in 433.
+
+**Acceptance Criteria:**
+- [x] [API] Koppeling name in the koppelingen overview table matches the registered name
+- [ ] [UI] Koppeling name in the applicatie overview "Koppelingen" column is consistent (no "undefined")
+- [ ] [UI] Koppeling name in the delete confirmation dialog matches the registered name
+- [ ] [UI] Koppeling names do not show "undefined" or empty values for imported koppelingen
+- [ ] [UI] The koppeling detail/review form shows the correct applicatie A and applicatie B
+
+**Key Context from Comments:** Likely caused by import process filling wrong fields. The applicatie overview shows "undefined" for some imported koppelingen.
+
+---
+
+### #433: Import: koppelingen lijkt niet goed te gaan
+
+**Labels:** Organisatie en configuratie, IGS nieuw
+**Test Step:** Step 11
+
+**Summary:** Imported koppelingen have fields populated incorrectly. The second application (applicatie B) shows as "Select..." in the edit form but appears in the review form. The import appears to map fields to the wrong locations.
+
+**Acceptance Criteria:**
+- [x] [API] Imported koppelingen have both applicatie A and applicatie B correctly populated
+- [ ] [UI] The koppeling edit form shows the correct applicatie B (not "Select...")
+- [ ] [UI] The koppeling review form matches the edit form data
+- [x] [API] Imported koppeling names in the overview match the import source data
+- [ ] [UI] The applicatie overview "Koppelingen" column shows correct koppeling names (not values from wrong fields)
+
+**Key Context from Comments:** Related to #432 and #401. Examples show Key2Betalen koppelingen with incorrect field mapping. May be caused by errors in the import file itself.
+
+---
+
+### #434: Contactpersoon: eerste account van leveranciers niet beschikbaar als contactpersoon
+
+**Labels:** IGS nieuw
+**Test Step:** Step 7
+
+**Summary:** When a new leverancier registers, their first account does not create a corresponding contactpersoon object. They cannot add themselves as a contactpersoon to an application. A second added contactpersoon IS visible and selectable.
+
+**Acceptance Criteria:**
+- [x] [API] When a leverancier registers their first account, a contactpersoon object is automatically created
+- [x] [API] The first account holder appears in beheer > contactpersonen
+- [ ] [UI] The first account holder can be selected as contactpersoon when creating/editing an applicatie
+- [ ] [UI] A second added contactpersoon is also visible and selectable (already works)
+
+**Key Context from Comments:** This is a registration flow issue — the auto-creation of a contactpersoon for the initial account does not happen.
+
+---
+
+### #435: Import applicatie: niet alle geimporteerde applicaties zichtbaar
+
+**Labels:** IGS nieuw
+**Test Step:** Step 7
+
+**Summary:** Not all imported applicaties are visible after import. Centric has 39 packages in the old catalogus and in the CSV import file, but only 32 appear in the new catalogus (both authenticated and unauthenticated). Shift2 (26) and Horlings (11) are correct.
+
+**Acceptance Criteria:**
+- [x] [API] The number of applicaties per leverancier matches the import CSV count
+- [x] [API] Centric shows 39 applicaties (currently shows 32 — 7 are missing)
+- [x] [API] Shift2 shows 26 applicaties (already correct)
+- [x] [API] Horlings & Eerbeek shows 11 applicaties (already correct)
+- [x] [API] No applicaties are lost during import
+- [x] [API] Both authenticated and unauthenticated views show the same count
+
+**Key Context from Comments:** VNG manual test marked this as FAIL. The discrepancy is specifically for Centric — 7 applicaties are missing.
+
+---
+
+### #436: Error bij het ophalen van het applicatie overzicht
+
+**Labels:** IGS nieuw
+**Test Step:** Step 7
+
+**Summary:** An error occurs when fetching the applicatie overview. Screenshot shows an error message on the beheer applicatie overview page.
+
+**Acceptance Criteria:**
+- [x] [API] The beheer applicatie overview loads without errors
+- [x] [UI] All applicaties are displayed in the table
+- [x] [UI] No error banners or messages appear on the page
+- [x] [API] The page works for both aanbod-beheerder and gebruik-beheerder roles
+- [x] [UI] Publieke zoekpagina laadt zonder redirect of authenticatiefouten
+- [x] [UI] Publieke detailpagina's laden volledig zonder 401-redirect
+- [x] [UI] 401-errors op publieke pagina's worden stil afgehandeld (geen redirect naar login)
+
+**Key Context from Comments:** Screenshot shows error on the overview page. Root cause: de globale axios 401-interceptor stuurde alle 401-responses door naar `/login`, ook op publieke pagina's. Fix: 401-redirect gescoped tot `/beheer` pagina's. Beheer werkte al correct; publieke pagina's zijn nu ook stabiel.
+
+**Resolution (2026-03-01):** Opgelost. Zie [reactie 436](reacties/436.md).
+
+---
+
+### #437: Geimporteerde leverancier: nieuwe koppeling opslaan geeft foutmelding
+
+**Labels:** Aanbod, Koppeling, IGS nieuw
+**Test Step:** Step 11
+
+**Summary:** When a user of an imported leverancier tries to add a new koppeling to an imported applicatie via Applicaties > Acties > Koppeling publiceren, saving results in a 400-error. Multiple errors appear in the server log.
+
+**Acceptance Criteria:**
+- [x] [API] A user of an imported leverancier can create a new koppeling for an imported applicatie
+- [x] [API] Saving the koppeling via the wizard does not produce a 400-error
+- [x] [API] No PHP errors or warnings appear in the server log when saving a koppeling for imported applicaties
+- [x] [API] The saved koppeling appears correctly in the koppelingen overview
+- [x] [API] The koppeling is properly linked to both applicaties (A and B)
+
+**Key Context from Comments:** Tested under a new user account created for Centric. Multiple attempts via Applicaties > Acties > Koppeling publiceren all resulted in 400-errors. Server log shows multiple errors.
+
+---
+
+### #438: Zoeken: verschillende vormgeving Diensten na filteren
+
+**Labels:** Aanbod, Zoeken, IGS nieuw
+**Test Step:** Step 14
+
+**Summary:** Search results for diensten show inconsistent formatting depending on which filters are applied. When filtering on Leverancier + Type: Dienst, the diensttype is shown on the search result card. But with other filter combinations or no filters, the diensttype is missing from the card.
+
+**Acceptance Criteria:**
+- [ ] [UI] Search result cards for diensten always show the diensttype, regardless of which filters are applied
+- [ ] [UI] Filter combination Leverancier + Type: Dienst shows same card layout as other filter combinations
+- [ ] [UI] Filter combination Leverancier + Diensttype shows same card layout as other filter combinations
+- [ ] [UI] Filtering only on Leverancier shows diensten with diensttype on the card
+- [ ] [UI] Unfiltered search results show diensten with diensttype on the card
+- [ ] [UI] All search result cards for diensten have consistent formatting/layout
+
+**Key Context from Comments:** Related to #345 (filter testing). The inconsistency suggests the diensttype field is only included in results under certain query paths.
+
+---
+
+### #439: Error na het openen van Applicatie-overzicht
+
+**Labels:** IGS nieuw
+**Test Step:** Step 7
+
+**Summary:** After opening the applicatie overview for the new leverancier Fortuna, an error appears. The server log also shows multiple PHP warnings. This may be a transient issue but the PHP warnings indicate underlying problems.
+
+**Acceptance Criteria:**
+- [x] [API] The applicatie overview for any leverancier (including Fortuna) loads without errors
+- [ ] [UI] No error banners or messages appear on the beheer applicatie overview page
+- [x] [API] No PHP warnings related to the applicatie overview appear in the server log
+- [x] [API] The connection to the server is consistent and reliable during overview loading
+- [x] [API] The page handles edge cases (empty data, missing fields) gracefully without errors
+
+**Key Context from Comments:** Similar to #436 (also an error on the applicatie overview). May be related to imported data quality or RBAC scoping for new leveranciers.
+
+---
+
+### #440: Zoeken: Organisatietype teveel aan opties
+
+**Labels:** IGS nieuw
+**Test Step:** Step 14
+
+**Summary:** The search facet "Organisatietype" shows too many options. It should only display: gemeente, samenwerking, leverancier, and community. Currently, additional unwanted options are shown.
+
+**Acceptance Criteria:**
+- [ ] [UI] The Organisatietype filter on the search page shows exactly 4 options: gemeente, samenwerking, leverancier, community
+- [ ] [UI] No additional or unexpected organisation types appear in the filter dropdown
+- [x] [API] Filtering by each of the 4 organisatietypes returns correct results
+- [ ] [UI] The filter options are displayed in a consistent, user-friendly format (no UUIDs, no technical names)
+
+**Key Context from Comments:** Screenshot shows extra options in the Organisatietype facet. The schemas Koppeling, Organisatie, and Dienst all have a property `Type` in addition to `koppelingType`, `organisatieType`, and `dienstType`. The filter appears to show values from all `Type` properties across schemas. This may be a faceting aggregation issue where properties with the same name across schemas are merged incorrectly.
+
+---
+
+### #441: Applicaties: mapping van de versies gaat niet goed bij geimporteerde applicaties
+
+**Labels:** IGS nieuw
+**Test Step:** Step 7
+
+**Summary:** For imported applicaties, the version mapping is incorrect. The "Applicatie Versies" column in the overview shows only "-", and when editing an imported applicatie, the version status and startdatum status fields are empty. Also, the correct spelling should be "applicatieversies" (one word, lowercase).
+
+**Acceptance Criteria:**
+- [ ] [UI] Imported applicaties show their version information in the "Applicatieversies" column (not just "-")
+- [ ] [UI] When editing an imported applicatie, the version status field is populated correctly
+- [ ] [UI] When editing an imported applicatie, the startdatum status field is populated correctly
+- [ ] [UI] The column header uses correct Dutch spelling: "Applicatieversies" (one word)
+- [x] [API] Version data from the import (module.csv) is correctly mapped to the version fields in the new system
+- [ ] [UI] Version information is consistent between the overview table and the edit/detail view
+
+**Key Context from Comments:** Tested while logged in as a new user of Centric. The version data exists in the import but is not correctly mapped to the display fields.
+
+---
+
+### #442: Applicaties: opgevoerd document wijzigt van naam naar bewijs_
+
+**Labels:** IGS nieuw
+**Test Step:** Step 16
+
+**Summary:** When uploading a document (e.g., "Rapport webrichtlijnen 2026.docx") for a standaardversie while publishing an applicatie, the document's original filename is replaced by "bewijs_.docx" on the applicatie detail page under standaarden. The original filename should be preserved and displayed.
+
+**Acceptance Criteria:**
+- [x] [API] Uploaded documents for standaardversies retain their original filename
+- [x] [UI] The document name shown on the applicatie detail page matches the originally uploaded filename
+- [x] [UI] The document name shown in the wizard/upload step matches the detail page display
+- [x] [API] Documents can be downloaded with their original filename (lokaal: upload een bewijsdocument via de wizard, verifieer dat de download-link de originele bestandsnaam behoudt)
+- [x] [UI] The file naming is consistent across all views (wizard, beheer table, public detail page)
+
+**Key Context from Comments:** Reproduced on accept environment. Document uploaded as "Rapport webrichtlijnen 2026.docx" but displayed as "bewijs_.docx" on the standaarden tab. URL: https://softwarecatalogus.accept.opencatalogi.nl/beheer/applicaties/931d1a0f-92e5-4111-b619-9a894062854e
+
+**Resolution (2026-03-01):** Opgelost. Ontbrekende `onChangeFileName` callback toegevoegd aan het representatieve LogoUploadField in de product standaarden stage, waardoor de originele bestandsnaam correct wordt doorgegeven. Zie [reactie 442](reacties/442.md).
+
+---
+
+### #430: Applicaties: beheertabel toont kolom Compliancy met applicatienamen
+
+**Labels:** IGS nieuw
+**Test Step:** Step 7
+
+**Summary:** In the applicatie management table, the "Compliancy" column incorrectly shows application names instead of compliance information. The column should be removed entirely since per-applicatie compliance cannot be meaningfully summarized in a single table column — it only makes sense on the applicatie detail page in the standaarden table.
+
+**Acceptance Criteria:**
+- [ ] [UI] The "Compliancy" column is removed from the applicatie management table (beheer overzicht)
+- [ ] [UI] Compliancy information remains available on the applicatie detail page under the standaarden tab
+- [ ] [UI] No application names are displayed in any column where they don't belong
+- [ ] [UI] The beheer table only shows relevant and meaningful columns
+
+**Key Context from Comments:** The VNG explicitly states the solution is to remove the Compliancy column from the management table. Per-applicatie compliance is too complex for a single column.
+
+---
+
+### #443: Dienst pagina: diensttypen aan elkaar geschreven
+
+**Labels:** IGS nieuw
+**Test Step:** Step 9
+
+**Summary:** On service detail pages, the service types (diensttypen) are concatenated without separators. They should be separated by commas for readability.
+
+**Acceptance Criteria:**
+- [ ] [UI] On the dienst detail page, multiple diensttypen are separated by commas (not concatenated)
+- [ ] [UI] The comma-separated display works for diensten with 2, 3, or more types
+- [ ] [API] The API response for a dienst returns diensttypen as an array (not a single concatenated string)
+
+**Key Context from Comments:** Screenshot shows "Diensttype:" field with types running together. Simple formatting fix.
+
+---
+
+### #444: Vormgeving veranderd bij te lange URL's
+
+**Labels:** IGS nieuw
+**Test Step:** Step 7
+
+**Summary:** When a very long URL is entered for an Organisation, Application, or Service, the grey info block expands beyond its intended width, breaking the page layout. The grey block should be max 400px and long URLs should be truncated with ellipsis.
+
+**Acceptance Criteria:**
+- [ ] [UI] The grey info block on detail pages does not exceed 400px width regardless of URL length
+- [ ] [UI] Long URLs are truncated with "..." after a reasonable number of characters
+- [ ] [UI] The truncated URL is still accessible (e.g., as a clickable link or with a tooltip showing the full URL)
+- [ ] [UI] The layout fix applies to Organisatie, Applicatie, and Dienst detail pages
+
+**Key Context from Comments:** Screenshots show the grey block expanding massively with long URLs on organisation, applicatie, and dienst pages. Affects public-facing pages.
+
+---
+
+### #445: Nieuwe dienst verkeerde afsluitende pagina
+
+**Labels:** Aanbod, IGS nieuw
+**Test Step:** Step 9
+
+**Summary:** After publishing a NEW service through the wizard (via the "Nieuwe dienst aanmelden" button on the completion page of a previous edit), the completion page incorrectly shows "Dienst succesvol geupdatet!" instead of the new service success message.
+
+**Acceptance Criteria:**
+- [ ] [UI] After creating a NEW dienst, the completion page shows a "new service" success message (not "updated")
+- [ ] [UI] After UPDATING an existing dienst, the completion page correctly shows "Dienst succesvol bijgewerkt"
+- [ ] [UI] The "Nieuwe dienst aanmelden" button on the completion page starts a fresh wizard flow (not an edit flow)
+
+**Key Context from Comments:** The bug occurs specifically when navigating from a completed edit to a new creation via the "Nieuwe dienst aanmelden" link. Related to #446 which covers additional text inconsistencies in the same wizard.
+
+---
+
+### #446: Dienst publiceren: tekstuele inconsistenties
+
+**Labels:** Aanbod, Tekstuele wijzigingen, IGS nieuw
+**Test Step:** Step 9
+
+**Summary:** Multiple text inconsistencies in the "Dienst publiceren" wizard: wrong button label on control page, wrong success message text, and wrong "new service" button text.
+
+**Acceptance Criteria:**
+- [ ] [UI] The blue button on the wizard control/review page reads "Dienst publiceren" (not "Dienst registreren")
+- [ ] [UI] The completion page after updating a dienst shows "Dienst succesvol bijgewerkt" (not "Dienst succesvol geüpdatet!")
+- [ ] [UI] The button to create a new service reads "Nieuwe dienst publiceren" (not "Nieuwe dienst aanmelden")
+
+**Key Context from Comments:** Three specific text changes requested by VNG with screenshots. Consistent naming with the wizard title "Dienst publiceren".
+
+---
+
+### #447: Zoeken: nieuwe leverancier zonder tussenkomst VNG direct vindbaar
+
+**Labels:** Organisatie en configuratie, IGS nieuw
+**Test Step:** Step 3
+
+**Summary:** A newly registered supplier (via the registration form) is immediately visible and searchable in the public search, even while still in "Concept" status in the backend. This is a security concern — suppliers should require VNG triage/approval before becoming publicly visible.
+
+**Acceptance Criteria:**
+- [ ] [HYBRID] A newly registered supplier in "Concept" status is NOT visible in public search results
+- [ ] [API] The search API excludes organisations with status "Concept" from unauthenticated search results
+- [ ] [API] The search API excludes organisations with status "Concept" from authenticated search results (other users)
+- [ ] [UI] Only after VNG admin approval (status change from "Concept" to published), the supplier becomes searchable
+- [ ] [HYBRID] A VNG admin can see concept suppliers in the backend management view and approve them
+
+**Key Context from Comments:** Security issue — allows malicious actors to publish visible content without moderation. Related to #139 (similar concern). Supplier "Theekop" shown as example of immediately visible concept organisation.
+
+---
+
+### #448: Overzichtspagina's: verschillende vormgeving en acties
+
+**Labels:** Aanbod, IGS nieuw
+**Test Step:** Step 7
+
+**Summary:** Detail pages for Diensten and Koppelingen have different styling than Organisatie, Applicatie, and Applicatieversie pages. All detail pages should follow a consistent layout: description on the left, grey info block on the right with entity type label and actions, and relevant tabs below.
+
+**Acceptance Criteria:**
+- [ ] [UI] Dienst detail page follows the same layout as Applicatie: description left, grey info block right, tabs below
+- [ ] [UI] Koppeling detail page follows the same layout as Applicatie: description left, grey info block right, tabs below
+- [ ] [UI] Grey info blocks have no separate headers between them (clean layout)
+- [ ] [UI] Dienst tabs include: Applicaties, Organisaties
+- [ ] [UI] Koppeling tabs include: Applicaties
+- [ ] [UI] Applicatie tabs include: Standaarden, Geschikt voor, Organisaties, Applicatieversies, Diensten, Koppelingen
+- [ ] [UI] Actions for Applicaties of other suppliers show: "Dienst publiceren", "Koppeling publiceren"
+- [ ] [UI] Actions for Diensten/Koppelingen/Applicatieversies of other suppliers show no actions (n.v.t.)
+
+**Key Context from Comments:** Comprehensive layout standardization request. Organisatie/Applicatie/Applicatieversie pages are the reference. Related to #101 (usability). May have additional missing tabs or actions to discover.
+
+---
+
+### #449: Handleiding facets configureren klopt niet
+
+**Labels:** IGS nieuw
+**Test Step:** Step 21
+
+**Summary:** The user manual for configuring facets gives incorrect navigation instructions. The documented path doesn't lead to clickable elements. The correct path is via Schemas > Actions > Edit > property options > Facet Title.
+
+**Acceptance Criteria:**
+- [ ] [UI] The facet configuration documentation/manual shows the correct navigation path
+- [ ] [UI] The documented navigation path leads to a page where facet settings are editable
+- [ ] [UI] Facet Title can be configured via Schema > property > actions menu and changes are saved correctly
+
+**Key Context from Comments:** @markbacker reported the issue with screenshots showing the incorrect path vs the correct path. The correct path goes through Schemas in the left menu.
+
+---
+
+### #450: Back-end: Icoon voor publiceren verwijderen
+
+**Labels:** Organisatie en configuratie, IGS nieuw
+**Test Step:** Step 6
+
+**Summary:** In the Nextcloud Softwarecatalogus app, the organisation overview shows an orange triangle with white exclamation mark icon, which was part of the now-removed publish process. This icon should be removed as it causes confusion.
+
+**Acceptance Criteria:**
+- [ ] [UI] The orange triangle warning icon is not shown next to organisations in the Nextcloud backend
+- [ ] [UI] The organisation overview in the Nextcloud app shows a clean list without legacy publish-status indicators
+- [ ] [API] No publish-related status flags affect the display of organisations in the backend list
+
+**Key Context from Comments:** The publish process for organisations has been removed, but the visual indicator remains. Simple cleanup task.
+
+---
+
+### #451: Koppeling: UUID's zichtbaar bij standaardversies
+
+**Labels:** Aanbod, IGS nieuw
+**Test Step:** Step 11
+
+**Summary:** When creating a koppeling via the wizard, UUIDs are displayed for standard versions ("standaardversies") instead of readable names, both during creation and when viewing the koppeling details.
+
+**Acceptance Criteria:**
+- [ ] [UI] When creating a koppeling via the wizard, standard versions show readable names (not UUIDs)
+- [ ] [UI] The koppeling detail page shows standard version names instead of UUIDs in the "Standaardversies" section
+- [ ] [API] The koppeling API response resolves standaardversie references to their display names
+- [ ] [UI] Newly created koppelingen (not imported) always display resolved standard version names
+
+**Key Context from Comments:** Imported koppelingen may contain invalid standaardversie IDs (see #401). This issue specifically concerns newly created koppelingen which should have valid references. Related to #401 (imported data quality).
+
+---
+
+### #452: Applicaties overzicht: toont niet alle koppelingen
+
+**Labels:** Aanbod, IGS nieuw
+**Test Step:** Step 7
+
+**Summary:** The Applicaties overview table's "Koppelingen" column does not show all koppelingen. An application with 3 koppelingen only shows 2 in the overview. The reporter requests a "+N meer" indicator when there are more than 2 koppelingen.
+
+**Acceptance Criteria:**
+- [ ] [API] The applicatie overview API returns the correct total count of koppelingen per application
+- [ ] [UI] The Koppelingen column in the applicatie overview shows all koppelingen or indicates the total count
+- [ ] [UI] When more than 2 koppelingen exist, a "+N meer" indicator is shown below the visible items
+- [ ] [UI] Clicking the "+N meer" indicator or the application row navigates to the full koppeling list
+
+**Key Context from Comments:** Reporter observed 3 koppelingen created for application "Korf" but only 2 displayed in the overview column. Suggests a consistent pattern with "+N meer" suffix.
+
+---
+
+### #453: Zoeken: filters van slag met filter Type=Koppeling
+
+**Labels:** IGS nieuw
+**Test Step:** Step 14
+
+**Summary:** Search facets break when filtering by Type=Koppeling. Other filters don't adjust to the filtered results (still show counts from all types). Selecting a second filter causes the Type=Koppeling filter to disappear. Text search combined with Type=Koppeling also causes filter inconsistencies.
+
+**Acceptance Criteria:**
+- [ ] [UI] After selecting Type=Koppeling filter, other facets update to reflect only koppeling-related values and counts
+- [ ] [UI] Selecting a second filter (e.g., Licentievorm) does not remove the Type=Koppeling filter
+- [ ] [UI] Combining text search with Type=Koppeling filter shows correct results with properly scoped facets
+- [ ] [API] The search API with `_search` + `type=koppeling` returns facet counts scoped to the filtered result set
+- [ ] [UI] Filter counts (e.g., "Licentievorm=Closed source (N)") reflect the actual number within the current filtered view
+
+**Key Context from Comments:** This is related to the faceting architecture. Non-aggregated facets (like "type") should scope subsequent facets to the selected schema. The bug suggests facets are being aggregated across all schemas instead of being scoped.
+
+---
+
+### #454: Wizard koppelingen: Reeds bestaande koppelingen voor worden niet gevonden
+
+**Labels:** Aanbod, IGS nieuw
+**Test Step:** Step 11
+
+**Summary:** When a supplier creates a koppeling for an application they didn't register themselves, the "Reeds bestaande koppelingen voor..." section in the wizard shows no results. This affects cross-supplier koppeling creation.
+
+**Acceptance Criteria:**
+- [ ] [UI] When opening the koppeling wizard from another supplier's application, existing koppelingen for that application are shown
+- [ ] [API] The koppeling search for existing koppelingen is not scoped by organisation/supplier (koppelingen from all suppliers are visible)
+- [ ] [UI] The "Reeds bestaande koppelingen voor [App]" section is populated when koppelingen exist for the target application
+- [ ] [HYBRID] A newly registered supplier can see koppelingen created by other suppliers when creating a new koppeling
+
+**Key Context from Comments:** Use case: supplier creates koppeling for "Centric Betalen" which was registered by a different supplier. The existing koppelingen for that app should be visible regardless of who created them. May be an RBAC scoping issue.
+
+---
+
+### #455: Tabblad koppelingen en contactpersonen worden publiekelijk niet getoond. RBAC?
+
+**Labels:** Aanbod, IGS nieuw
+**Test Step:** Step 12
+
+**Summary:** When viewing an application publicly (not logged in), the tabs "Koppelingen" and "Contactpersonen" are not shown. These should be visible to the public for supplier applications. When logged in as a different supplier, the Koppelingen tab IS shown.
+
+**Acceptance Criteria:**
+- [ ] [HYBRID] The "Koppelingen" tab is visible on application detail pages when not logged in
+- [ ] [HYBRID] The "Contactpersonen" tab is visible on application detail pages when not logged in
+- [ ] [API] Public (unauthenticated) API requests for application koppelingen return data
+- [ ] [API] Public (unauthenticated) API requests for application contactpersonen return data
+- [ ] [UI] Public view shows koppelingen and contactpersonen data matching what authenticated users see (minus edit controls)
+
+**Key Context from Comments:** This appears to be an RBAC issue where the public API does not return related objects (koppelingen, contactpersonen) for applications, even though these should be public data for supplier applications.
+
+---
+
+### #456: Consistentie in werking van wizards
+
+**Labels:** Aanbod, IGS nieuw
+**Test Step:** Step 7
+
+**Summary:** Wizard completion pages are inconsistent across application, dienst, and koppeling wizards. Issues include: button text ("aanmelden" vs "publiceren"), button styling (white vs blue), intermediate page before restarting wizard, and missing text on koppeling completion page.
+
+**Acceptance Criteria:**
+- [ ] [UI] All wizard completion pages use "Nieuw(e) [object] publiceren" button text (not "aanmelden")
+- [ ] [UI] The "Nieuwe [object] publiceren" button is blue (filled) on all wizard types including Koppeling and Gebruik
+- [ ] [UI] Clicking "Nieuwe applicatie publiceren" starts the wizard directly without an intermediate "Kies het type applicatie" page
+- [ ] [UI] The koppeling wizard completion page includes the text "Organisaties kunnen de koppeling bekijken en beoordelen"
+- [ ] [UI] All wizard completion pages have consistent layout and messaging structure
+
+**Key Context from Comments:** Related to #445 (dienst wizard issues). The reporter found 4 specific inconsistencies between the Applicatie, Dienst, and Koppeling wizard flows.
+
+---
+
+### #187: Tekstvoorstellen
+
+**Labels:** Aanbod, Tekstuele wijzigingen
+**Test Step:** Step 7
+
+**Summary:** Collection of text change proposals across multiple pages and wizards. Several items were completed but multiple remain unimplemented as of the latest comment (2026-03-04).
+
+**Acceptance Criteria:**
+- [ ] [UI] Registration success page text matches the specified template with dynamic fields (naam, organisatienaam, mailadres)
+- [ ] [UI] Contact person description text reads: "De geregistreerde contactpersoon is het eerste aanspreekpunt van de organisatie en beheerder van de gebruikers van de softwarecatalogus namens uw organisatie."
+- [ ] [UI] After registering an application, the success page shows "Uw applicatie is succesvol geregistreerd!" with the specified follow-up text
+- [ ] [UI] Search page includes explanatory text: "De zoekfunctie doorzoekt de naam en beschrijvingen van items..."
+- [ ] [UI] Left menu shows "Gebruikers" instead of "Contactpersonen"
+- [ ] [UI] User creation success page shows "Gebruiker toevoegen - De gebruiker is succesvol toegevoegd"
+- [ ] [UI] Referentiecomponenten link in wizard is clickable (not shown as raw URL)
+- [ ] [UI] Dienst wizard title is "Dienst registreren" with correct explanatory text
+
+**Key Context from Comments:** Long-running issue (since Oct 2025). Latest comment (2026-03-04) by @Makkmetp lists 5 remaining unimplemented text changes marked with ❌. Some earlier text proposals are no longer applicable due to workflow changes.
+
+---
+
+## Other Issues (In Review) — Additional Non-Testable Issues
+
+The following issues are questions, PvE requirements, infrastructure tasks, test result collections, verzamelissues, or architecture/development tasks that are not directly testable via the IGS flow but remain in review.
+
+| Issue # | Title | Labels | Test Step |
+|---------|-------|--------|-----------|
+| [#417](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/417) | Vraag: Kunnen we andere email adressen krijgen voor bestaande contactpersonen? | help wanted | Step 3 |
+| [#416](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/416) | Vraag: Issue met twee delen (gebruikerstest + observaties) splitsen | (none) | General |
+| [#414](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/414) | Vraag: Mogen deelnemers gebruiksobjecten lezen waar ze inzitten als deelnemer | Gebruik | Step 12 |
+| [#413](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/413) | Vraag: De views waarmee we testen zijn geen onderdeel van de softwarecatalogus, moeten ze dat wel worden? | Referentiearchitectuur | Step 19 |
+| [#412](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/412) | Vraag: Niet alle AMEF views hebben documentatie, kunnen deze worden aangeleverd? | Referentiearchitectuur | Step 19 |
+| [#411](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/411) | Vraag: Required eisen uitgezet ivm dataimport, kan de data export worden uitgebreid? | (none) | Step 19 |
+| [#341](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/341) | Tabelweergave: Optie toevoegen (optioneel) (out of scope) | Zoeken, Wijziging | Step 14 |
+| [#181](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/181) | Testresultaten Mark donderdag 9 oktober | (none) | General |
+| [#179](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/179) | Testresultaten 2025-10-05 | Aanbod, PvE eis, Bevinding | General |
+| [#175](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/175) | Testresultaten 2025-09-24 (blok 3) | (none) | General |
+| [#171](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/171) | Bevindingen fase 3 Aanbod | (none) | General |
+| [#170](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/170) | Bevindingen importeren organisaties | (none) | Step 19 |
+| [#167](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/167) | Bugs Open Register | (none) | General |
+| [#166](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/166) | Verzamelissue wensen Open Register | (none) | General |
+| [#165](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/165) | Verzamelissue reparaties fase 2 organisaties | (none) | Step 6 |
+| [#164](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/164) | Verzamelissue grafische todo | (none) | General |
+| [#162](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/162) | OpenAPI specificatie: compliance, documentatie, locatie | (none) | Infra |
+| [#161](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/161) | (VNGR) Externe Koppelvlakken publiceren | Referentiearchitectuur | Step 19 |
+| [#156](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/156) | (VNGR) Relatie standaard en standaardversie | Referentiearchitectuur | Step 16 |
+| [#151](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/151) | Als functioneel beheerder wil ik GEMMA-referentiedata synchroniseren | Referentiearchitectuur | Step 19 |
+| [#147](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/147) | Als architectuur-expert wil ik views vanuit GEMMA Online kunnen inladen | Referentiearchitectuur | Step 19 |
+| [#146](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/146) | Als architectuur-expert wil ik de applicatielaag op een ArchiMate view kunnen plotten | Referentiearchitectuur | Step 19 |
+| [#145](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/145) | Als architectuur-expert wil ik een ArchiMate view kunnen exporteren naar AMEFF | Referentiearchitectuur | Step 24 |
+| [#143](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/143) | Als architectuur-expert wil ik de relatie zien tussen referentiecomponenten en standaarden | Referentiearchitectuur | Step 16 |
+| [#138](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/138) | Als IBD wil ik basisbeveiligingsniveaus voor referentiecomponenten kunnen inzien | IBD | Step 19 |
+| [#137](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/137) | Als IBD wil ik BIO-maatregelen per referentiecomponent inzien | IBD | Step 19 |
+| [#136](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/136) | Als IBD wil ik de relatie zien tussen referentiecomponenten en BIO-beheersmaatregelen | IBD | Step 19 |
+| [#124](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/124) | Als ontwikkelaar wil ik een geautomatiseerde datamigratie tool bouwen | Conduction ontwikkeling | Infra |
+| [#104](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/104) | Het systeem moet beschikbaar zijn met minimaal 99,5% uptime | | Infra |
+| [#103](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/103) | Het systeem moet responsief zijn en presteren binnen 2 seconden laadtijd | | General |
+| [#102](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/102) | Het systeem moet voldoen aan WCAG 2.1 AA richtlijnen | | General |
+| [#100](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/100) | Het systeem moet voldoen aan de relevante privacy- en beveiligingswetgeving (AVG) | | Infra |
+| [#99](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/99) | Het systeem moet voldoen aan de API Design Rules (ADR) | | Infra |
+| [#98](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/98) | De applicatie moet ondersteuning bieden voor meerdere talen (i18n) | | General |
+| [#97](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/97) | De componenten moeten open source zijn en gepubliceerd onder een EUPL-licentie | | Infra |
+| [#96](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/96) | Er moet uitgebreide documentatie beschikbaar zijn voor gebruikers en ontwikkelaars | | General |
+| [#95](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/95) | De broncode moet beheerd worden op GitHub | | Infra |
+| [#93](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/93) | De API moet voldoen aan de NL GOV API Design Rules | | Infra |
+| [#88](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/88) | Componenten moeten draaien op een Kubernetes-omgeving | | Infra |
+| [#86](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/86) | Als ontwikkelaar wil ik een schaalbare en onderhoudbare architectuur opzetten | | Infra |
+| [#82](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/82) | Als ontwikkelaar wil ik een openbare API aanbieden voor softwaregegevens en GEMMA-informatie | Aanbod, PvE wens | Step 12 |
+| [#72](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/72) | Als gebruik-raadpleger wil ik een ArchiMate-export maken vanuit mijn applicatielandschap | Gebruik, PvE eis | Step 24 |
+| [#70](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/70) | Als functioneel beheerder wil ik koppelingen naar GEMMA Online beheren | Referentiearchitectuur | Step 19 |
+| [#67](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/67) | Als IBD wil ik dat leveranciers BIO/NEN compliance-informatie kunnen registreren | IBD, PvE wens | Step 16 |
+| [#58](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/58) | Als gebruik-beheerder wil ik koppelingen tussen applicaties kunnen registreren | Gebruik, PvE eis | Step 11 |
+| [#56](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/56) | Als gebruik-beheerder wil ik standaardversies per koppeling kunnen registreren | Gebruik, PvE eis | Step 19 |
+| [#53](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/53) | Als gebruik-raadpleger wil ik inzicht in de kwaliteit van het pakketoverzicht | Gebruik, PvE eis | Step 18 |
+| [#52](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/52) | Als gebruik-raadpleger wil ik ArchiMate referentiecomponenten in context zien | Gebruik, PvE eis | Step 19 |
+| [#51](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/51) | Als gebruik-raadpleger wil ik de koppeling zien tussen mijn applicaties en referentiecomponenten | Gebruik, PvE eis | Step 19 |
+| [#50](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/50) | Als gebruik-beheerder wil ik referentiecomponenten koppelen aan mijn applicaties | Gebruik, PvE eis | Step 10 |
+| [#49](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/49) | Als aanbod-beheerder wil ik de compliance van mijn pakket met standaarden bijhouden | Aanbod, PvE wens | Step 16 |
+| [#48](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/48) | Als aanbod-beheerder wil ik versies van mijn pakket bijhouden | Aanbod, PvE wens | Step 7 |
+| [#47](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/47) | Als aanbod-beheerder wil ik koppelingen met andere pakketten registreren | Aanbod, PvE wens | Step 11 |
+| [#46](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/46) | Als aanbod-beheerder wil ik diensten bij mijn pakketten registreren | Aanbod, PvE wens | Step 9 |
+| [#45](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/45) | Als aanbod-beheerder wil ik referentiecomponenten koppelen aan mijn pakket | Aanbod, PvE wens | Step 7 |
+| [#44](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/44) | Als aanbod-beheerder wil ik contactpersonen koppelen aan mijn pakketten | Aanbod, PvE wens | Step 5 |
+| [#42](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/42) | Als aanbod-beheerder wil ik kwetsbaarheden en beoordelingen bij mijn pakketten plaatsen | IBD, PvE wens | Step 7 |
+| [#40](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/40) | Als aanbod-beheerder wil ik een pakket als SaaS of On-Premise kunnen registreren | Aanbod, PvE wens | Step 7 |
+| [#37](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/37) | Als gebruik-beheerder wil ik de contactpersoon registreren verantwoordelijk voor een pakket | Gebruik, PvE wens | Step 5 |
+| [#36](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/36) | Als gebruik-beheerder wil ik de versie en hosting-informatie van pakketten registreren | Gebruik, PvE wens | Step 10 |
+| [#34](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/34) | Als gebruik-beheerder wil ik pakketten uit mijn pakketoverzicht kunnen verwijderen | Gebruik, PvE wens | Step 10 |
+| [#32](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/32) | Als gebruik-beheerder wil ik applicaties van leveranciers selecteren voor mijn pakketoverzicht | Gebruik, PvE eis | Step 10 |
+| [#31](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/31) | Als gebruik-beheerder wil ik een overzicht van alle pakketten in mijn pakketoverzicht | Gebruik, PvE eis | Step 10 |
+| [#27](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/27) | Als functioneel beheerder wil ik GEMMA-referentiecomponenten beheren | Referentiearchitectuur | Step 21 |
+| [#26](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/26) | Als functioneel beheerder wil ik standaarden en standaardversies beheren | Referentiearchitectuur | Step 21 |
+| [#25](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/25) | Als functioneel beheerder wil ik organisatie-typen en -statussen configureren | Organisatie en configuratie | Step 21 |
+| [#24](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/24) | Als functioneel beheerder wil ik de softwarecatalogus configureren | Organisatie en configuratie | Step 21 |
+| [#21](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/21) | Als gebruik-raadpleger wil ik een ArchiMate-view van het applicatielandschap zien | Gebruik, PvE eis | Step 19 |
+| [#18](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/18) | Als gebruik-raadpleger wil ik zoeken en filteren op pakketten | Gebruik, PvE eis | Step 14 |
+| [#17](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/17) | Als bezoeker wil ik informatie zien over een specifiek pakket | PvE eis | Step 14 |
+| [#16](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/16) | Als bezoeker wil ik zoeken en filteren op pakketten | PvE eis, Zoeken | Step 14 |
+| [#14](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/14) | Als aanbod-beheerder wil ik referentiecomponenten koppelen aan mijn pakket | Aanbod, PvE eis | Step 7 |
+| [#13](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/13) | Als aanbod-beheerder wil ik diensten bij mijn pakket registreren | Aanbod, PvE eis | Step 9 |
+| [#12](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/12) | Als aanbod-beheerder wil ik een pakket met modules kunnen registreren | Aanbod, PvE eis | Step 7 |
+
+---
+
+## Previously Listed Other Issues (In Review) — 38 issues
+
+| Issue # | Title | Labels | Test Step |
+|---------|-------|--------|-----------|
+| [#8](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/8) | Als aanbod-beheerder zie ik de door gebruik-beheerders onder mijn organisatie toegevoegde pakketten | Gebruik, PvE eis | Step 18 |
+| [#10](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/10) | Als aanbod-beheerder wil ik kunnen registreren welke organisaties mijn pakket gebruiken | Gebruik, PvE eis | Step 18 |
+| [#11](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/11) | Als functioneel beheerder wil ik een overzicht kunnen opvragen van alle door gebruik-beheerders geregistreerde pakketten en/of aanbieders | Gebruik, PvE eis, nonblock | Step 23 |
+| [#19](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/19) | Als gebruik-raadpleger wil ik bij het bekijken van een pakket kunnen zien welke gemeenten het gebruiken | Gebruik, PvE eis | Step 17 |
+| [#20](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/20) | Als gebruik-raadpleger wil ik kunnen "gluren bij de buren" om te bekijken welke pakketten bij een andere gemeente in gebruik zijn | Gebruik, PvE eis | Step 17 |
+| [#22](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/22) | Als gebruik-raadpleger wil ik mijn pakketoverzicht kunnen filteren op meerdere eigenschappen | Gebruik, PvE eis | Step 17 |
+| [#35](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/35) | Als gebruik-beheerder wil ik kunnen registreren welke diensten ik afneem voor de pakketten in mijn pakketoverzicht | Gebruik, PvE wens | Step 10 |
+| [#38](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/38) | Als aanbod-beheerder wil ik bij SaaS voorzieningen kunnen aangeven bij welke cloud-provider deze draait | Aanbod, PvE wens | Step 7 |
+| [#41](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/41) | Als gebruik-beheerder wil ik relevante documenten zoals DPIA's, verwerkersovereenkomsten en pentesten kunnen delen | IBD, PvE wens | Step 12 |
+| [#43](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/43) | Als aanbod-beheerder wil ik documenten kunnen toevoegen aan mijn pakketten met een conceptstatus | IBD, PvE wens | Step 7 |
+| [#54](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/54) | Als gebruik-raadpleger wil ik statistieken kunnen zien over mijn pakketoverzicht | Gebruik, PvE eis, nonblock | Step 18 |
+| [#55](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/55) | Als gebruik-beheerder wil ik bij een koppeling kunnen aangeven of er gebruik wordt gemaakt van een standaardversie | Gebruik, PvE eis | Step 19 |
+| [#57](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/57) | Als gebruik-beheerder van een samenwerkingsverband wil ik softwarepakketten kunnen opvoeren voor de gemeenten waarvoor we werken | Gebruik, PvE eis | Step 20 |
+| [#59](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/59) | Als gebruik-beheerder wil ik alle informatie over mijn applicaties in de softwarecatalogus kunnen invoeren | Gebruik, PvE eis | Step 22 |
+| [#60](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/60) | Als gebruik-beheerder wil ik voor meerdere organisaties met één account de pakketoverzichten kunnen bewerken | Gebruik, PvE eis | Step 20 |
+| [#71](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/71) | (VNGR) Importeren ArchiMate ID-73 | help wanted, Referentiearchitectuur, PvE eis | Step 19 |
+| [#74](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/74) | Als gebruik-raadpleger wil ik een overzicht met zoek- en filteropties van alle organisaties die pakketten of diensten gebruiken | question, Gebruik, PvE eis, Zoeken | Step 17 |
+| [#75](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/75) | Als functioneel beheerder wil ik rapportages maken over de data in de softwarecatalogus | Management Informatie, PvE eis, nonblock | Step 21 |
+| [#83](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/83) | Als ontwikkelaar wil ik via een beveiligde, besloten API aanbodinformatie kunnen registreren | Aanbod, PvE wens | Step 12 |
+| [#84](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/84) | Als ontwikkelaar wil ik via een beveiligde, besloten API toegang hebben tot gebruiksinformatie | Gebruik, PvE wens | Step 12 |
+| [#87](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/87) | Als gebruiker wil ik op een juiste manier geïnformeerd worden door het systeem wanneer er een fout optreedt | | General |
+| [#89](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/89) | Componenten zijn out-of-the-box geschikt voor installatie binnen een gestandaardiseerde (cloud-)infrastructuur platform | | Infra |
+| [#92](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/92) | Als functioneel beheerder wil ik inzicht in het gebruik van de softwarecatalogus via een open source webstatistiekenpakket | | Step 21 |
+| [#101](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/101) | De gebruikersinterface moet intuïtief en eenvoudig te gebruiken zijn | | General |
+| [#105](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/105) | Als gebruik-beheerder willen we dat aanbieders onze applicatielandschappen en koppelingen niet zien | Gebruik, PvE eis | Step 12 |
+| [#109](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/109) | Als ontwikkelaar wil ik organisatie- en softwaregegevens integreren zodat deze beschikbaar zijn in de export | Conduction ontwikkeling | Step 13 |
+| [#117](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/117) | Als ontwikkelaar wil ik robuuste foutafhandeling implementeren zodat mislukte imports netjes worden afgehandeld | Conduction ontwikkeling | Step 19 |
+| [#121](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/121) | Als ontwikkelaar wil ik versioning implementeren zodat we kunnen rollbacken bij problemen | Conduction ontwikkeling | Infra |
+| [#130](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/130) | Als ontwikkelaar wil ik CI/CD pipelines hebben zodat code automatisch wordt getest | Restpunt, Conduction ontwikkeling | Infra |
+| [#135](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/135) | (VNGR) Valideren van non-functionele eisen voor component Referentiearchitectuur | Referentiearchitectuur | Step 22 |
+| [#183](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/183) | [Feature] Als gebruiker wil ik een 'wachtwoord vergeten' optie | Testbevindingen Wilco | Step 4 |
+| [#192](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/192) | [Taak] Organisatie adres info in organisatie cards (NC dashboard) | nonblock, Testbevindingen Wilco | Step 6 |
+| [#195](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/195) | [Bug] In NC dashboard doen niet alle opties in de organisatie 'acties dropdown' wat ze moeten doen | bug, Testbevindingen Wilco | Step 3 |
+| [#205](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/205) | [Bug] Een gedepubliceerde applicatie is nog te vinden als je er naar zoekt | bug, Testbevindingen Wilco | Step 14 |
+| [#208](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/208) | [Bug] NC Dashboard organisatie overzicht (table ipv cards) laat alleen het veld ID zien | bug, Testbevindingen Wilco | Step 3 |
+| [#209](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/209) | [Bug] De help knop op de NC dashboard organisaties pagina gaat naar een niet bestaande pagina toe | bug, Testbevindingen Wilco | Step 3 |
+| [#231](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/231) | [Bug] AMEFF exports (van views) geven een foutmelding als je deze probeert te importeren in Archi | bug, Testbevindingen Wilco | Step 24 |
+| [#342](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/342) | Zoeken: op kaartjes aantal referentiecomponenten duidelijk maken | Zoeken, Wijziging | Step 14 |
+
+---
+
+## Issue Distribution by Test Step
+
+| Test Step | Description | IGS | Other | Total |
+|-----------|-------------|-----|-------|-------|
+| Step 3 | Organisatie activatie + gebruikersbeheer | 3 | 3 | 6 |
+| Step 4 | Eerste inlog | 4 | 1 | 5 |
+| Step 5 | Collega's uitnodigen / Contactpersonen | 6 | 0 | 6 |
+| Step 6 | Organisatie profiel | 2 | 1 | 3 |
+| Step 7 | Product aanmaken (applicaties) | 36 | 2 | 38 |
+| Step 9 | Dienst wizard | 17 | 0 | 17 |
+| Step 10 | Gebruik melden en beheren | 9 | 1 | 10 |
+| Step 11 | Koppeling wizard | 13 | 0 | 13 |
+| Step 12 | Privacy en zichtbaarheid | 4 | 4 | 8 |
+| Step 13 | Excel export | 2 | 1 | 3 |
+| Step 14 | Zoeken en resultaten | 12 | 2 | 14 |
+| Step 16 | Standaarden beheer | 2 | 0 | 2 |
+| Step 17 | "Gluren bij de buren" | 0 | 4 | 4 |
+| Step 18 | Leverancier gebruik beheer | 0 | 3 | 3 |
+| Step 19 | Geavanceerde koppelingen | 2 | 3 | 5 |
+| Step 20 | Samenwerkingen | 0 | 2 | 2 |
+| Step 21 | Beheer en configuratie | 12 | 2 | 14 |
+| Step 22 | Geavanceerde zoek en filter | 1 | 2 | 3 |
+| Step 23 | Functioneel beheer overzicht | 0 | 1 | 1 |
+| Step 24 | AMEFF export | 0 | 1 | 1 |
+| General | Cross-cutting / multiple steps | 3 | 2 | 5 |
+| Infra | Infrastructure / non-testable | 1 | 3 | 4 |
+| **Total** | | **136** | **38** | **174** |
+
+> Note: Some issues with broad scope (General/Infra) are not mapped to a specific test step.
+
+---
+
+## Legend
+
+| Value | Meaning |
+|-------|---------|
+| **Step N** | Mapped to test step N in `testen.md` |
+| **General** | Cross-cutting issue affecting multiple steps |
+| **Infra** | Infrastructure issue, not directly testable via UI |
diff --git a/issues/141.md b/issues/141.md
new file mode 100644
index 00000000..ec438b53
--- /dev/null
+++ b/issues/141.md
@@ -0,0 +1,128 @@
+# #141 — Als functioneel beheerder wil ik, naar aanleiding van gemeentelijke herindeling of een leveranciersovername, organisaties en al hun relaties (aanbod en/of gebruik) kunnen samenvoegen met een bestaande of nieuwe organisatie
+
+**Status:** OPEN | **Labels:** Aanbod, PvE eis, nonblock, Bevinding
+**Auteur:** @Rem-Dam | **Datum:** 2025-02-19
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/141
+
+---
+
+## Beschrijving
+
+zodat de gemeente of leverancier het bestaande aanbod en gebruik niet opnieuw hoeft in te voeren.
+
+---
+
+## Reacties (12)
+
+### Reactie 1 — @github-actions (2025-02-19)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2025-04-09)
+
+@Makkmetp Dit vraag natuurlijk eigenlijk om een 'mini' proceesje waarin we beschrijven hoe dit gaat. Komt het puur neer op het anapassen van de organistie id van de onderliggende objecten.
+
+### Reactie 3 — @Makkmetp (2025-04-16)
+
+@rubenvdlinde nu wordt dit via meerdere script uitgevoerd. We zouden dit graag zelf kunnen uitvoeren. Bij voorkeur met zo min mogelijk handelingen.
+
+Het komt per jaar wel een aantal keer voor dat leveranciers overgenomen worden.
+Dat gemeenten samen opgaan in een nieuwe gemeente gebeurt minder vaak, maar gebeurt wel eens. In 2023 is de laatste geweest: [bron CBS](https://www.cbs.nl/nl-nl/onze-diensten/methoden/classificaties/overig/gemeentelijke-indelingen-per-jaar)
+
+### Reactie 4 — @rubenvdlinde (2025-05-12)
+
+@matthiasoliveiro deze is nu in Jira opgenomen onder https://conduction.atlassian.net/browse/REGISTERS-181, dit vraagt om de volgende stappen
+
+- Ik selecteer via tables een object dat ik wil samenvoegen
+- Ik krijg een merge modal, in deze modal zie ik drie collomen (object a, object b, result)
+- In de result colum kies ik van welke object ik de waarde vastleg in het nieuwe object (by default de waarde van het object waar naartoe word gemerged)
+- Na opslaan wordt
+- - Het nieuwe object bijgewerkt met gekozen waarden (if any)
+- - Alle relaties van object A worden opgehaald en omgezet naar object B
+- - Object A wordt verwijderd
+
+### Reactie 5 — @rubenvdlinde (2025-07-01)
+
+
+
+### Reactie 6 — @rubenvdlinde (2025-07-03)
+
+Naar aanleiding van feedback hebben de object cards nu leesbare titels
+
+
+
+### Reactie 7 — @Makkmetp (2025-07-09)
+
+In plaats van opslaan heb ik geklikt op Audit trails. Daarna begon de pagina met audit trails erg lang te laden zonder enige filters. Na het toevoegen van filters (bijvoorbeeld datum velden vandaag) werd het audit trail sneller geladen.
+De gemeenten zijn in ieder geval samengevoegd en de gemeente Groningen bestaat niet meer in de lijst.
+Na de merge krijg ik wel regelmatigs een melding van "" timeout of 30000ms exceeded". Dat was o.a. bij het activeren van de samengevoegde gemeente en het ophalen van alle organisaties.
+Wat wel bijzonder is, is dat de Group voor de gemeente Almere groningen_1 is geworden. Dit kan komen omdat Almere in eerste instantie niet geactiveerd was en geen group had. In de merge is het ook aangegeven als target, maar dit gaat in de toekomst nog wel voor verwarring zorgen na herindelingen of het samenvoegen van leveranciers, wanneer niet de correcte group wordt gebruikt. Dat is vooral aan de achterkant.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+### Reactie 8 — @rubenvdlinde (2025-08-19)
+
+Eerst door conduction opnieuw testen
+
+### Reactie 9 — @Makkmetp (2025-12-18)
+
+Deze moet nog getest worden.
+
+### Reactie 10 — @Makkmetp (2026-02-04)
+
+@rubenvdlinde ik wil deze graag testen, maar is er ergens een handleiding te vinden? Als ik vanuit de back-end of op Docusaurus naar documentatie daarover zoek, dan kan ik niks vinden over het mergen van de verschillende objecten.
+Daarnaast dient dit ook getest te worden met de geimporteerde data.
+
+### Reactie 11 — @rubenvdlinde (2026-02-23)
+
+Handleidingen staan op https://vng-realisatie.github.io/Softwarecatalogus/docs/Handleidingen :)
+
+---
+
+**Comment by @Makkmetp** — 2026-03-02
+
+:x:Ondanks de handleiding is het niet gelukt om te mergen.
+
+Dit is getest met de nieuw aangemaakt leverancier Fortuna naar de geimporteerde leverancier Centric. Hierbij zijn alle default waardes gebruikt en geklikt op Merge Objects. Het merge report is bijgevoegd:
+
+
+
+
+
+Daarnaast mis ik in de handleiding nog het volgende:
+Onder Configure Merge kan je in de laatste kolom de waarde bepalen. Daar staat geen uitleg over.
+
+:x:De lijst van koppelingen lijkt ook onvolledig. Er missen 2 koppelingen in het overzicht.
+:x:De namen van de koppelingen zijn niet volledig.
+
+
+
+Dit is alleen getest met leveranciers.
diff --git a/issues/144.md b/issues/144.md
new file mode 100644
index 00000000..03977c92
--- /dev/null
+++ b/issues/144.md
@@ -0,0 +1,271 @@
+# #144 — Als gebruiker van de Softwarecatalogus wil ik een overzicht met zoek- en filteropties van alle organisaties die pakketten of diensten aanbieden
+
+**Status:** OPEN | **Labels:** Aanbod, PvE eis, Bevinding, Restpunt, Zoeken
+**Auteur:** @Rem-Dam | **Datum:** 2025-02-19
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/144
+
+---
+
+## Beschrijving
+
+opdat ik deze kan selecteren en het aanbod kan bekijken.
+
+---
+
+## Reacties (23)
+
+### Reactie 1 — @github-actions (2025-02-19)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2025-03-18)
+
+Dit is dus inclusief de organisatie detail pagina, @Makkmetp waarop willen we filteren?
+
+### Reactie 3 — @rubenvdlinde (2025-03-18)
+
+@Makkmetp kan jij de facetten aanvullen?
+
+### Reactie 4 — @Makkmetp (2025-03-18)
+
+Als gemeente wil ik dat Leveranciers niet de contactgegevens van mij zien. Als gemeente wil ik alle contactgegevens zien inclusief een filter optie op Leveranciers, dienstverlener, gemeente, samenwerking.
+
+Filteren op verklaringen, certificaten, verwerkersovereenkomsten Ja/Nee, ....
+
+
+
+### Reactie 5 — @matthiasoliveiro (2025-03-26)
+
+@Makkmetp is deze refined?
+
+### Reactie 6 — @matthiasoliveiro (2025-04-07)
+
+@Makkmetp is er een update te geven?
+
+### Reactie 7 — @Makkmetp (2025-04-08)
+
+@matthiasoliveiro Nee, nog niet. Deze bespreek ik morgen met Mark.
+
+### Reactie 8 — @Makkmetp (2025-04-09)
+
+Een concept is uitgewerkt op https://vng-realisatie.github.io/Softwarecatalogus-Archi-repository/?view=id-811e1ce341d54da7b057b71a7d554b7f
+
+We willen dit graag verder verfijnen met jullie.
+
+We denken nu aan de Lijst van organisatie:
+
+- Organisatienaam
+- Diensten (comma seperated of bullets) - Functioneel beheer, Technisch beheer, Applicatie beheer, Implementatieondersteuning, Opleidingen
+- Contactpersonen van organisatie
+- Type organisatie (gemeente, samenwerking, leverancier)
+- Documenttype(gedeelde verklaringen: ISO gecertificeerd, Convenanten (zoals Common Ground), ...)
+
+### Reactie 9 — @matthiasoliveiro (2025-05-19)
+
+@Makkmetp is deze nu in afgelopen sessie verfijnd?
+
+### Reactie 10 — @rubenvdlinde (2025-06-03)
+
+@matthiasoliveiro deze is bij mijn weten verfijnt in het organisatie object
+
+### Reactie 11 — @rubenvdlinde (2025-07-03)
+
+Dit kan worden ingeregeld via facatable in open registers en is dan zichtbaar op de voorkan
+
+
+
+
+
+### Reactie 12 — @Makkmetp (2025-07-07)
+
+Zie #74 voor feedback. De filtering werkt nog niet goed. De aantallen komen niet overeen met het aantal resultaten.
+
+### Reactie 13 — @Makkmetp (2025-07-14)
+
+De zoekresultaten mogen wat meer inhoud krijgen. (Zie voorstel verderop)
+Bij applicaties en diensten (voorzieningen):
+- [ ] Voeg de naam van de leverancier toe aan de zoekresultaten en maak deze aanklikbaar.
+- [x] De korte beschrijving (max. 100 karakters(?))
+- [ ] De 2 meest aangevinkte|opgevoerde referentiecomponenten door gemeenten bij de applicatie
+- [ ] Wanneer een afnemer zoekt, dan het aantal gemeenten wat de applicatie heeft afgenomen.
+- [x] Iconen gebruiken voor applicatie, dienst en aanbieders (leveranciers of community of gemeentelijke)
+
+Aanvullingen en verbetervoorstellen zijn hierin welkom.
+Om een indruk te geven op het aantal zoekopdrachten op de huidige Softwarecatalogus:
+- 15% van de zoekopdrachten is naar een leverancier
+- 85% naar een applicatie
+
+
+
+
+
+### Reactie 14 — @rubenvdlinde (2025-07-22)
+
+Deze is in afronding
+
+### Reactie 15 — @remko48 (2025-07-24)
+
+Afgerond en is te zien op https://vng.opencatalogi.nl/zoeken
+
+
+### Reactie 16 — @Makkmetp (2025-08-11)
+
+Dat ziet er weer beter uit. Nog een aantal verbeterpunten:- [ ] Een knop waarmee alle filters in één keer verwijderd kunnen worden.
+- [x] Placeholder in het zoekvenster aanpassen naar "Zoek op naam, organisatie of product"
+- [x] De term Organisation bij de filters graag aanpassen naar Organisatie
+- [x] De laatste bijgewerkt datum altijd onderaan zetten. Nu staat deze bijvoorbeeld bij applicaties boven "Geschikt voor: "
+- [x] De resultaten (kaarten) zijn niet te bereiken via een toetsenbord navigatie (tab).
+- [x] Bij een korte beschrijving hoeven geen aanhalingsteken in de resultaten.
+- [x] De beschrijvingen van applicaties worden nog gemist in de resultaten.
+
+
+
+
+
+### Reactie 17 — @Makkmetp (2025-09-01)
+
+- [x] Wanneer je vanuit de homepage zonder een zoekterm zoekt, dan blijven de zoekresultaten lang grijs.
+- [x] De organisatie namen zijn nu UID's. Graag daar de naam tonen.
+- [x] Er worden geen filters getoond bij het zoeken.
+- [x] Wanneer je de zoekterm aan het typen bent, dan gaat het zoeken al beginnen. Wellicht wachten totdat er op de knop wordt gedrukt, aangezien dit nu vertragend lijkt te werken.
+- [x] De organisatie omschrijving mist in de zoekresultaten
+Zie de meldingen van de vorige keer:
+- [x] Placeholder in het zoekvenster aanpassen naar "Zoek op naam, organisatie of trefwoord"
+- [x] De term Organisation bij de filters graag aanpassen naar Organisatie
+- [x] De laatste bijgewerkt datum altijd onderaan zetten. Nu staat deze bijvoorbeeld bij applicaties boven "Geschikt voor: "
+- [x] De resultaten (kaarten) zijn niet te bereiken via een toetsenbord navigatie (tab).
+- [x] Bij een korte beschrijving hoeven geen aanhalingsteken in de resultaten.
+- [x] De beschrijvingen van applicaties worden nog gemist in de resultaten.
+
+
+
+
+
+
+
+
+
+
+
+
+### Reactie 18 — @remko48 (2025-09-02)
+
+De organisatie namen zijn nu UID's. Graag daar de naam tonen.
+Komt doordat de organisatie objecten opnieuw geimporteerd moeten worden zodat de juiste configuratie gebruikt wordt op het object.
+Of alle objecten moeten handmatig opnieuw opgeslagen worden
+
+### Reactie 19 — @Makkmetp (2025-09-10)
+
+- [x] Graag de eerdere melding nog doornemen en verwerken. https://github.com/VNG-Realisatie/Softwarecatalogus/issues/144#issuecomment-3242278265 en https://github.com/VNG-Realisatie/Softwarecatalogus/issues/144#issuecomment-3173985984
+- [x] Wanneer je vanaf de homepage zoekt door een zoekterm in te voeren en nergens op te klikken, dan gaat de zoekmachine zonder verder interactie al aan de slag met zoeken. Graag van de homepage deze actie pas starten wanneer er op zoeken wordt geklikt. (@remko48)
+- [x] Na bovenstaande actie verdwijnt ook de zoekterm die je zojuist hebt opgevoerd. De zoekmachine wordt dus automatisch gestart vanaf home, daarna opent de zoekresultaten pagina met de gevonden pagina's die bij de zoekterm horen. Daarna lijkt de zoekpagina zich te resetten met als resultaat alle pagina's. (@remko48)
+- [ ] De gevraagde filters ontbreken (@rubenvdlinde)
+
+
+
+
+
+### Reactie 20 — @remko48 (2025-09-10)
+
+Bevindingen uit comment https://github.com/VNG-Realisatie/Softwarecatalogus/issues/144#issuecomment-3173985984 3173985984
+> - [x] Placeholder in het zoekvenster aanpassen naar "Zoek op naam, organisatie of product"
+
+Dit moeten we nog aanpassen en gaan we veranderen naar "Zoek op naam of trefwoord."
+
+> - [x] De term Organisation bij de filters graag aanpassen naar Organisatie
+
+Organisations willen we niet tonen in de facets omdat dit dan een checkbox lijst maakt van minimaal 342 checkboxes plus alle leveranciers. Tevens gaan we kijken als we een vertaler kunnen gebruiken om dit standaard op te lossen
+
+> - [x] De laatste bijgewerkt datum altijd onderaan zetten. Nu staat deze bijvoorbeeld bij applicaties boven "Geschikt voor: "
+
+De cards gebruiken nu een generieke template en moeten nog specifieke eigenschappen krijgen.
+
+> - [x] De resultaten (kaarten) zijn niet te bereiken via een toetsenbord navigatie (tab).
+
+Dit komt doordat de tab navigatie eerst door alle filters heen gaat. We moeten nog uitzoeken wat volgens WCAG de manier is om dit op te lossen, of door alle filters eerst of door alle cards eerst.
+
+> - [x] Bij een korte beschrijving hoeven geen aanhalingsteken in de resultaten.
+
+Dit gebeurt alleen als dit specifiek in de beschrijving staat. Dit zou dan de gebruiker zelf weg moeten halen
+
+> - [x] De beschrijvingen van applicaties worden nog gemist in de resultaten.
+
+We hebben nu de mogelijkheid om de samenvatting weer te geven uit het object. Dit gaan we toepassen
+
+---
+
+Bevindingen uit comment https://github.com/VNG-Realisatie/Softwarecatalogus/issues/144#issuecomment-3242278265 3242278265
+
+> - [x] De organisatie namen zijn nu UID's. Graag daar de naam tonen.
+
+Dit zou recent opgelost moeten zijn. Dit gaan we controleren als dat echt opgelost is.
+
+> - [x] De organisatie omschrijving mist in de zoekresultaten
+
+We hebben nu de mogelijkheid om de samenvatting weer te geven uit het object. Dit gaan we toepassen
+
+>- [x] De term Organisation bij de filters graag aanpassen naar Organisatie
+
+Organisations willen we niet tonen in de facets omdat dit dan een checkbox lijst maakt van minimaal 342 checkboxes plus alle leveranciers. Tevens gaan we kijken als we een vertaler kunnen gebruiken om dit standaard op te lossen
+
+> - [x] De laatste bijgewerkt datum altijd onderaan zetten. Nu staat deze bijvoorbeeld bij applicaties boven "Geschikt voor: "
+
+De cards gebruiken nu een generieke template en moeten nog specifieke eigenschappen krijgen.
+
+> - [x] De resultaten (kaarten) zijn niet te bereiken via een toetsenbord navigatie (tab).
+
+Dit komt doordat de tab navigatie eerst door alle filters heen gaat. We moeten nog uitzoeken wat volgens WCAG de manier is om dit op te lossen, of door alle filters eerst of door alle cards eerst.
+
+> - [x] Bij een korte beschrijving hoeven geen aanhalingsteken in de resultaten.
+
+Dit gebeurt alleen als dit specifiek in de beschrijving staat. Dit zou dan de gebruiker zelf weg moeten halen
+
+> - [x] De beschrijvingen van applicaties worden nog gemist in de resultaten.
+
+We hebben nu de mogelijkheid om de samenvatting weer te geven uit het object. Dit gaan we toepassen
+
+---
+Bevindingen uit comment https://github.com/VNG-Realisatie/Softwarecatalogus/issues/144#issuecomment-3273558905 3273558905
+
+> - [x] Wanneer je vanaf de homepage zoekt door een zoekterm in te voeren en nergens op te klikken, dan gaat de zoekmachine zonder verder interactie al aan de slag met zoeken. Graag van de homepage deze actie pas starten wanneer er op zoeken wordt geklikt.
+
+Hier ben ik naar aan het kijken
+
+> - [x] Na bovenstaande actie verdwijnt ook de zoekterm die je zojuist hebt opgevoerd. De zoekmachine wordt dus automatisch gestart vanaf home, daarna opent de zoekresultaten pagina met de gevonden pagina's die bij de zoekterm horen. Daarna lijkt de zoekpagina zich te resetten met als resultaat alle pagina's.
+
+Hier ben ik naar aan het kijken
+
+> - [x] De gevraagde filters ontbreken
+
+Hier gaat Ruben naar kijken.
+
+
+### Reactie 21 — @markbacker (2026-01-29)
+
+Op het zoeken staan nog diverse issues open. Zie o.a
+- #346
+- #315
+
+### Reactie 22 — @rubenvdlinde (2026-02-23)
+
+Het is goed om even scherp te hebben dat een aantal VNG issues hebben geleid tot ander gedrag van bijvoorbeeld de zoekfilters. Dat is dus geen regressie maar een bedoelde wijziging ;)
+
+- Er zijn nu minder leveranciers in de facets, dat komt doordat leveranciers een filter is op aanbieders van modules, er worden dus alleen leveranciers getoond die modules aanbieden (voorkomt ook kluitje riet). Maar doordat we alleen modules tonen die zijn aangemaakt door leveranciers (RBAC) zie je maar 280 leveranciers. Totdat je bent ingelogd dan zie je wel alle leveranciers, dit is een gevolg van https://github.com/VNG-Realisatie/Softwarecatalogus/issues/315
+- In lijn met https://github.com/VNG-Realisatie/Softwarecatalogus/issues/398 worden onder leveranciers nog uuid's getoond van applicaties die verwijzen naar een niet bestaande aanbieder.
+- Er is nog een UUID onder standaard versies, dat komt door de import. Uitleg daarover staat hier: https://github.com/VNG-Realisatie/Softwarecatalogus/issues/349
+- Bij https://github.com/VNG-Realisatie/Softwarecatalogus/issues/343 is het goed om te weten dat koppelingen vallen onder RBAC, je ziet ze dus alleen als je bent ingelogd met de juiste rol.
+
+---
+
+**Comment by @Makkmetp** — 2026-03-02
+
+Bedankt voor de uitleg.
+
+:x: Het aantal leveranciers komt nog niet overeen met het aantal leveranciers in de huidige softwarecatalogus. Deze kan meerdere oorzaken hebben, zoals de import of het niet goed filteren voor de facets. In de huidige softwarecatalogus staan er nu 341. Het kan zijn dat de laatste import nog niet de meest recent aangemaakt leveranciers in zich heeft. Het aantal is te vinden op https://www.softwarecatalogus.nl/leveranciers
diff --git a/issues/148.md b/issues/148.md
new file mode 100644
index 00000000..7d155e86
--- /dev/null
+++ b/issues/148.md
@@ -0,0 +1,125 @@
+# #148 — (VNGR) De GEMMA-architectuur is opvraagbaar met een API
+
+**Status:** OPEN | **Labels:** Referentiearchitectuur, IGS review
+**Auteur:** @markbacker | **Datum:** 2025-03-05
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/148
+
+---
+
+## Beschrijving
+
+## Bevindingen API algemeen
+
+Verbetervoorstellen ontwerp API
+- [ ] API moet geschikt zijn voor opvragen meerdere ArchiMate-modellen
+ - Zie in het informatiemodel voorzieningencatalogus paragraaf [Referentieconcept](https://alliantie.github.io/voorzieningencatalogus/#graph_EAID_19FEFA67_EB1C_4a8a_BDE6_C2B9090CFA22)
+ - Hernoem de API van GEMMA API naar ArchiMate API
+ - Aan alle ArchiMate-API's een query-parameter `model-id` toevoegen.
+ - ipv een query-parameter zou het `model-id` in de URL kunnen. Gebruik niet de model-naam in de URL, namen veranderen
+ - [endpoint/model](https://vng.accept.commonground.nu/apps/openconnector/api/endpoint/model) wordt [endpoint/models](https://vng.accept.commonground.nu/apps/openconnector/api/endpoint/models) en levert een lijst van beschikbare ArchiMate-modellen.
+- [ ] Iedere API kent nu meerdere id's, maar het is onduidelijk wat deze betekenen.
+ - [ ] Kan het met minder id's?
+ - [ ] Dan id's die een ontwikkelaar nooit gaat gebruiken weglaten uit de API
+ - [ ] Alle id's zijn noodzakelijk vanwege technische redenen? Maak in de API dan onderscheid tussen 'externe' id's en 'interne' id's (de Open Register id's lijken mij bijvoorbeeld intern cq technisch)
+ - [ ] GEMMA model kent voor alle objecten twee id's
+ - **Archi id**, door de Archi tool gegenereerde GUID waarmee de interne relaties worden gelegd. Dit zijn de `identifiers` in het AMEFF XML bestand
+ - **Object ID**, door GEMMA beheerde property van alle objecten (met script gegeneerde GUID)
+ - Ga er niet vanuit dat deze in alle ArchiMate-modellen beschikbaar. API moet ook werken zonder `Object ID`
+ - `Object ID` te gebruiken als query-parameter
+ - `Object-id` als onderdeel van stabiele URI naar GEMMA objecten. Redenen hiervoor
+ - Stabiel, want onder controle van VNGR
+ - Ook onderdeel van URL's GEMMA online `https://gemmaonline.nl/wiki/GEMMA/id-{Object ID}`
+ - Met `Object ID` kan je zoeken in Archi, met `Archi id` niet
+ - Deeplinken naar filters van de nieuwe Softwarecatalogus, bijvoorbeeld alle pakketten die een bepaalde referentiecomponent ondersteunen
+ - [ ] Documenteer in de OAS de betekenis
+- [ ] [ArchiMate/Model doc](https://vng-realisatie.github.io/Softwarecatalogus/docs/GEMMA/model) => Models
+ - vraag niet het hele model op, maar alleen het model-object met de model properties
+ - Model-object properties ontbreken nu
+ - Release => om versie te tonen
+ - Object ID => om te kunnen linken naar GEMMA online
+ - Publiceren
+ - Type model
+- [ ] [Archimate/Elementen doc](https://vng-realisatie.github.io/Softwarecatalogus/docs/GEMMA/elementen)
+ - het ArchiMate-type ontbreekt
+ - query-parameters, zie #146
+- [ ] [Archimate/Relations doc](https://vng-realisatie.github.io/Softwarecatalogus/docs/GEMMA/relations)
+ - het ArchiMate-type ontbreekt
+ - query-parameters op ArchiMate-type, nodig om relaties in het GEMMA-model af te lopen
+- [ ] [ArchiMate/eigenschapsdefinities doc](https://vng-realisatie.github.io/Softwarecatalogus/docs/GEMMA/eigenschapsdefinities)
+ - Waarom NL eigenschappen in de documentatie. Overal EN properties vind ik duidelijker
+ - Alleen intern gebruiken. Expand eigenschapnamen overal
+ - als de property-namen overal expanded zijn, dan lijkt deze API mij overbodig (voor de scope SWC).
+ - kanttekening. Een propertydef bevat ook type-definities. BiZZdesign gebruikt dit, Archi niet (alles string)
+
+### Testen
+Goed
+- Aantallen kloppen toch met csv export (tijdens sessie vergeleken met export van een andere versie van het GEMMA-model)
+
+Fouten
+- [ ] [endpoint/model](https://vng.accept.commonground.nu/apps/openconnector/api/endpoint/model)
+ - error; "No matching endpoint found for path and method: model GET"
+- [ ] [endpoint/views](https://vng.accept.commonground.nu/apps/openconnector/api/endpoint/views)
+ - count 240, aantal klopt
+ - traag, ik zie geen use case voor het ophalen van alle viewdefinities. Zonder id een lijst van view-id's ophalen, met id de viewdefinitie ophalen?
+- [ ] [endpoint/elements](https://vng.accept.commonground.nu/apps/openconnector/api/endpoint/elements)
+ - aantal klopt
+ - propertydefs expanden
+ - element met lege properties gevonden. lege properties weglaten.
+
+| | |
+| ------------- | -------------------------------------------------- |
+| 2763 | |
+| @self | {…} |
+| identifier | "id-c1b9e7ce-e544-4b26-8816-cac3e6973cf7" |
+| name | "GEMMA domeinen" |
+| documentation | "Groepering van domeinen …nen het GEMMA-raamwerk." |
+| properties | '{ "":"","":"" }' |
+| id | "1b6ff6ee-c565-44ad-a2e9-f21bc9696ad9" |
+- [endpoint/relations](https://vng.accept.commonground.nu/apps/openconnector/api/endpoint/relations)
+ - werkt niet, bad gateway??? of forward naar views endpoint.
+
+
+
+
+---
+
+## Reacties (6)
+
+### Reactie 1 — @github-actions (2025-03-05)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @matthiasoliveiro (2025-03-13)
+
+@rubenvdlinde is de refinement volledig helder?
+
+### Reactie 3 — @rubenvdlinde (2025-04-15)
+
+@matthiasoliveiro Deze komt mee in de Open Registers Configuratie refactor, althans de benodigde code om hetm uit te rollen. Daarna is het slechts een kwestie van configuratie.
+
+### Reactie 4 — @rubenvdlinde (2025-12-18)
+
+Deze dingen even dubbel checken maar de archimate API zou het gewoon moeten doen aangezien we die contnue gebruiken
+
+### Reactie 5 — @rubenvdlinde (2026-02-20)
+
+In het werkend krijgen van de AMEF platen (waarvoor we deze api hebben) hebben we er voor moeten kiezen om zo min mogenlijk wijzigingen aan het datamodal te doen. (Dat is natuurlijk ook in lijn met de gedachte om het datamodel uberhaupt zo min mogenlijk aan te passen, maar goed). We hebben daarom nu meegenomen in het issue wat we konden, het wijzigen van de naam was echter in deze periode niet mogenlijk zonder de AMEF platen in gevaar te brengen op de voorkant.
+
+Wel hebben we de API vereenvoudigd, en nu het volledige amef object opgenomen onder de property xml, om zo geod mogenlijk uitt e lijnen met de voorstelen uit dit issue binnen de kaders die we hadden, Dat betekend dat dit issue eigenlijk op nieuw moet worden gerefined.
+
+Automatisch gegenereerde documentatie is hersteld en te vinden op https://redocly.github.io/redoc/?url=https%3A%2F%2Fbackend.accept.opencatalogi.nl%2Findex.php%2Fapps%2Fopenregister%2Fapi%2Fregisters%2F3%2Foas of onder
+
+
+
+### Reactie 6 — @markbacker (2026-02-26)
+
+Onduidelijk wat met bovenstaande punt is gedaan. Ik vind geen URL waarmee ik de API kan aanroepen en kan dit dus niet beoordelen.
+
+Wat is de status van de gegenereerde documentatie naast de eerder opgeleverde documentatie?
diff --git a/issues/15.md b/issues/15.md
new file mode 100644
index 00000000..4be74a8e
--- /dev/null
+++ b/issues/15.md
@@ -0,0 +1,127 @@
+# #15 — Als aanbod- en gebruik-beheerder wil ik data vanuit de softwarecatalogus kunnen exporteren
+
+**Status:** OPEN | **Labels:** Aanbod, Gebruik, PvE eis, Bevinding, Wijziging
+**Auteur:** @rubenvdlinde | **Datum:** 2025-02-06
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/15
+
+---
+
+## Beschrijving
+
+zodat ik deze in een spreadsheet kan bewerken en/of kan gebruiken voor een andere toepassing.
+
+---
+
+## Reacties (16)
+
+### Reactie 1 — @github-actions (2025-02-06)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2025-05-12)
+
+@remko48 deze raken we nu grappig genoeg aan via de export functie, laten we er even op letten dat we het hier hebben over beheerders. Ofwel ingelogde gebruikers die EXPORTEREN wat ze kunnen zien (dat laatste is wel cruciaal). Het issue is mij verder duidenlijk maar wordt bij Conduction reeds inhouden afgedekt door -> https://conduction.atlassian.net/browse/VSC-266
+
+### Reactie 3 — @rubenvdlinde (2025-07-22)
+
+@rubenvdlinde controleren of live en dan ter test aanbieden
+
+### Reactie 4 — @rubenvdlinde (2025-07-22)
+
+Exporteren één schema (naar bijvoorbeeld csv werkt nog)
+
+### Reactie 5 — @rubenvdlinde (2025-09-03)
+
+De excel export functie op tabbelen doet het, dus deze kan gewoon getest worden :)
+
+### Reactie 6 — @Makkmetp (2025-09-04)
+
+Ik ben ingelogd als een leverancier. En wanneer ik dan naar mijn productoverzicht ga en ga exporteren zonder iets te selecteren, wordt een CSV gemaakt met daarin alle producten uit de Softwarecatalogus. Het is de bedoeling dat alleen de producten van het eigen ICT landschap geexporteerd kunnen worden. Dit gebeurt ook wanneer ik alleen een opgevoerd product exporteer.
+URL: https://softwarecatalogus.accept.opencatalogi.nl/beheer/product
+
+
+
+### Reactie 7 — @rubenvdlinde (2025-10-29)
+
+In de laatste tests van ons komt alleen het eigen landschap terug
+
+### Reactie 8 — @WilcoLouwerse (2025-12-08)
+
+_Dit issue is al even niet "aangeraakt", in hoe verre is het nog relevant?
+Kan dit issue gesloten worden (accepted?) en zo niet wat moet er nog gedaan worden?_
+
+Ik heb getest en het exporteren van een CSV of Excel werkt.
+En op het moment zie je alleen de applicaties (voorheen producten) van je eigen organisatie (Gemeente/Leverancier) op het beheer overzicht, vanaf waar je dus kan exporteren.
+
+### Reactie 9 — @Makkmetp (2025-12-16)
+
+Wordt nog getest met RBAC en of de juiste informatie geexporteerd wordt.
+
+### Reactie 10 — @rubenvdlinde (2025-12-17)
+
+Export komt overeen met applicaties die ik mag zien
+
+
+
+
+
+### Reactie 11 — @Makkmetp (2025-12-18)
+
+Dit betreft Aanbod en Gebruik.
+Van Aanbod is gezien dat er een export is en dat deze een export oplevert. De opmaak van de CSV was toentertijd niet optimaal na het omzetten van Tekst naar kolommen aangezien sommige kolommen overschreven werden.
+
+Gebruik is nog niet getest.
+
+### Reactie 12 — @Makkmetp (2026-01-29)
+
+Het exporteren van mijn applicaties levert allemaal UUID's op.
+Er zijn meerdere opties denkbaar:
+- 1 leesbare optie zonder UUID's en een export en import optie met UUID's
+- Of een combinatie met leesbare tekst en UUID's, welke ook geëxporteerd en geïmporteerd kan worden.
+
+Zie #355
+
+
+
+
+### Reactie 13 — @markbacker (2026-02-04)
+
+De exports van de huidige Softwarecatalogus exporteren van objecten zowel de namen als de id's. Redenen hiervoor zijn deze
+- direct bruikbaar in excel
+- door de id's ook geschikt om te importeren
+
+In exports dus zowel kolommen met namen als met id's
+
+### Reactie 14 — @rubenvdlinde (2026-02-12)
+
+Deze subleer met #355
+
+### Reactie 15 — @rubenvdlinde (2026-02-12)
+
+Extra collom toegeveogd met _ ervoor zo als mondeling afgesproken
+
+### Reactie 16 — @Makkmetp (2026-02-26)
+
+#355 is een gerelateerd issue.
+
+✅Export werkt en er zijn kolommen toegevoegd waar naast de UUID's de tekstuele verklaring staat van het UUID
+
+❌Wat betreft: "Ofwel ingelogde gebruikers die EXPORTEREN wat ze kunnen zien (dat laatste is wel cruciaal)". Er staan kolommen in die niet zichtbaar zijn voor de ingelogde gebruikers. Dit gaat bijvoorbeeld over de kolommen: omvat, _omvat, onderdeelVan,_onderdeelVan,standaardenGEMMA, moduleVersies, _moduleVersies, beoordelingen, _beoordelingen, kwetsbaarheden. _kwetsbaarheden, geregistreerdDoor
+❌Wanneer je een export Excel uitvoert bij een nieuw aangemaakte leverancier, dan worden Diensten wel gevuld. _Diensten staat dan gevuld met UUID. Koppelingen is wel gevuld. Compliancy en _Compliancy zijn beiden gevuld met UUID's. Dit geldt voor meerdere kolommen.
+
+❌Wanneer je een export Excel uitvoert bij een geïmporteerde leverancier, dan worden Diensten niet gevuld.
+
+
+[voorzieningen_module_2026-02-26_140948_nieuwe leverancier.xlsx](https://github.com/user-attachments/files/25578356/voorzieningen_module_2026-02-26_140948_nieuwe.leverancier.xlsx)>
+
+[voorzieningen_module_2026-02-26_142750_geimporteerde leverancier.xlsx](https://github.com/user-attachments/files/25578357/voorzieningen_module_2026-02-26_142750_geimporteerde.leverancier.xlsx)>
+
+
+
diff --git a/issues/155.md b/issues/155.md
new file mode 100644
index 00000000..8f99a531
--- /dev/null
+++ b/issues/155.md
@@ -0,0 +1,121 @@
+# #155 — (VNGR) Definities worden weergegeven via een interactieve optie binnen de softwarecatalogus
+
+**Status:** OPEN | **Labels:** Organisatie en configuratie, Cms, IGS review
+**Auteur:** @Makkmetp | **Datum:** 2025-03-18
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/155
+
+---
+
+## Beschrijving
+
+Glossary lijst zoals nu ook in de softwarecatalogus
+
+---
+
+## Reacties (14)
+
+### Reactie 1 — @github-actions (2025-03-18)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @Makkmetp (2025-03-18)
+
+@Makkmetp nagaan of dit in PvE staat.
+
+### Reactie 3 — @Makkmetp (2025-03-19)
+
+@rubenvdlinde in PVE ID-75 #106 wordt de glossary genoemd.
+Als gebruiker van de Softwarecatalogus wil ik waar GEMMA-concepten worden getoond, wil ik dat GEMMA-concepten worden uitgelegd of verklaard, zodat ik in één keer kan zien wat deze inhoud. (Glossary)
+
+Binnen de huidige softwarecatalogus hebben we een lexicon waar we woorden en definities kunnen opvoeren. Wanneer dan iemand met zijn muis over een woord gaat, dan wordt daar de definitie van getoond:
+
+
+
+### Reactie 4 — @rubenvdlinde (2025-04-09)
+
+@Makkmetp we hebben voor dimpact nu onderstaande gebouwd, voordeel is dat je daarmee je begrippen over de lopende tekst legt. Het bestaat uit twee delen
+
+- Begrippen die voorkomen op de pagina die je nu bekijkt
+- Zoeken in begrippen
+
+Voordeel is dat je dan ook begrippen kan 'detecteren' en klikbaar maken in de tekst. Nadeel is dat je wat minder ruimte hebt voor de uitleggende tekst bij begrippen. Je zal wat bondiger moeten zijn en eerder een link naar buiten (wikipedia ofzo) moeten geven.
+
+Zouden we dit voor de software catalogus kunnen overnemen?
+
+
+
+
+
+
+### Reactie 5 — @Makkmetp (2025-12-18)
+
+@rubenvdlinde kan je aangeven wat de status is van dit issue?
+
+
+### Reactie 6 — @rubenvdlinde (2026-01-06)
+
+@Makkmetp kan jij deze aanleveren?
+
+### Reactie 7 — @Makkmetp (2026-02-04)
+
+Op https://www.softwarecatalogus.nl/lexicon#Leverancier staan de definities van de huidige softwarecatalogus.
+
+### Reactie 8 — @rubenvdlinde (2026-02-04)
+
+@Makkmetp gaan we verwerken, ik noteer hem dan als feature.
+
+### Reactie 9 — @rubenvdlinde (2026-02-11)
+
+@remko48 Ruben stelt (conform aangeleverde lexicon van @Makkmetp ) het glossery endpoint op de test omgeving beschickbaar. Daarna kan deze in de frontend worden geactiveer in lijn met bovenstaande functionaliteit van dimpact.
+
+@Makkmetp is het mogenlijk de lexicon aan te leveren als cvs? dan kan ik hem namelijk importeren.
+
+### Reactie 10 — @rubenvdlinde (2026-02-12)
+
+@remko48 endpoint is toegevoegd onder apps/opencatalogi/api/glossary
+
+@Makkmetp csv is niet langer nodig waren dus danig weing dingen dat ik ze zelf even heb gekopierd
+
+### Reactie 11 — @rubenvdlinde (2026-02-16)
+
+@remko48 ik pak deze wel even van je over
+
+### Reactie 12 — @rubenvdlinde (2026-02-18)
+
+Graag een eerste review https://github.com/user-attachments/assets/d3ca18e9-ae08-4de3-9f32-b0922e087756
+
+### Reactie 13 — @rubenvdlinde (2026-02-18)
+
+Afspraak: We beordelen deze functioneel, grafische opmerkingen worden nieuwe issues.
+
+### Reactie 14 — @Makkmetp (2026-02-23)
+
+✅de button met "Begrippenlijst" opent en toont de begrippen van de pagina
+✅wanneer je met je muis over een begrip hovered dan wordt er een pop-up getoond met de tekst "Bekijk de definitie van ".
+✅wanneer je klikt op een onderstreept woord, dan opent de begrippenlijst zich
+❌In de documentatie staat dat de externe link niet verplicht is. Bij het opvoeren mag de link niet leeg blijven anders kan je het begrip niet toevoegen. Graag de optie "External link" optioneel te maken.
+❌Add keywords levert in eerste instantie een lijst met UUID's op. Door tekst op te voeren is er te zoeken naar "keywords".
+❔Zijn de keywords hoofdlettergevoelig? Bijvoorbeeld het begrip is API. Is het toevoegen van api dan nodig?
+❔Op wat voor type pagina's werkt dit? Alle, alleen van de CMS of ook die van een organisatie, applicatie, dienst of koppeling? Bij een organisatie-pagina wordt de link niet geactiveerd.
+❔Alleen de summary van de invoervelden wordt getoond in de begrippenlijst. Wat is dan de toegevoegde waarde van het veld description.
+
+
+
+
+
+
+
+
+
+
+
+
+
+Een standaardmanier waarop systemen gegevens en functionaliteit aanbieden aan andere systemen.
diff --git a/issues/160.md b/issues/160.md
new file mode 100644
index 00000000..826f6c61
--- /dev/null
+++ b/issues/160.md
@@ -0,0 +1,153 @@
+# #160 — (VNGR) Performance plotten views tbv ID-77
+
+**Status:** OPEN | **Labels:** Referentiearchitectuur
+**Auteur:** @matthiasoliveiro | **Datum:** 2025-04-15
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/160
+
+---
+
+## Beschrijving
+
+### Sprint
+
+Geen sprint (backlog)
+
+### User Story
+
+Een gebruiker wil een soepele ervaring bij het laden van views.
+
+### Acceptatiecriteria
+
+Test casus is Zeist 261 pakketen in 11 seconden
+
+### Context
+
+_No response_
+
+---
+
+## Reacties (15)
+
+### Reactie 1 — @github-actions (2025-04-15)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @matthiasoliveiro (2025-04-15)
+
+@Makkmetp @markbacker kunnen jullie het issue aanvullen?
+
+### Reactie 3 — @Makkmetp (2025-04-15)
+
+Performance test uitgevoerd.
+Plotten van view "Referentiecomponentencatalogus" van gemeente Zeist: 271 pakketten 79 koppelingen: deze view is beschikbaar binnen 11 seconden.
+
+### Reactie 4 — @rubenvdlinde (2025-04-17)
+
+Label referntie architectuur toegevoegd t.b.v van overzicht.
+
+### Reactie 5 — @Makkmetp (2025-04-29)
+
+Nog mee bezig.
+
+### Reactie 6 — @rubenvdlinde (2025-05-06)
+
+Oke intressante tussen fase, de performance van de backend is aanzienlijk verbeterd (dat kan je ook in postman aftesten)
+
+alle views (gebruik je in de praktijk niet):
+https://vng.test.commonground.nu/apps/openconnector/api/endpoint/views
+Enkele grote view (0,5 seconde):
+https://vng.test.commonground.nu/apps/openconnector/api/endpoint/views/bb58ed8d-085f-4885-969b-8dff6238855a
+
+Waar we nu achter komen dit dat er ook nog eens stuk browser afhankenlijkheid in zet, dat wil zeggen. Het maakt door de grote van de view in de browser cash nogal uit wat voor laptop en broewer de gebruiker heeft in hoe snel de view in laad. Uiteraard gaan we nog even kijken of we ook dit iets kunnen optimaliseren zodat die consequent onder de 14 seconde blijf maar we zien bij ons dat sommige "kantoor" laptops de 17 seconde aantikken. Hoe zullen we hier eens mee omgaan? Acceptatie creteria koppelen aan een bepaalde machine?
+
+### Reactie 7 — @Makkmetp (2025-05-06)
+
+@rubenvdlinde hierbij de cijfers van gebruikte software en browsers door bezoekers van de softwarecatalogus.
+Microsoft Edge, Google Chrome en FireFox zijn de drie meest gebruikte browsers door gemeenten. Het lijkt me goed om deze drie browsers zo goed mogelijk te ondersteunen.
+Edge en Chrome worden door 70% van de bezoekers gebruikt. Laten we ons vooral daarop focussen.
+
+
+
+### Reactie 8 — @rubenvdlinde (2025-05-06)
+
+CHecks, dan is nog even de vraag wat het "gewicht" van de laptop is qua performance want dat maakt dus uit
+
+
+### Reactie 9 — @Makkmetp (2025-05-07)
+
+Een basis laptop lijkt op dit moment een Intel Core i5 met 16 GB DDR4 of DDR 5 en 512 GB SSD te zijn. Deze lijkt me geschikt voor bezoekers van de softwarecatalogus, die informatie zoeken en verwerken.
+Voor degene die aan de slag gaan met modelleren is een zwaardere laptop geschikter. Denk daarbij aan een Intel Core i7 met 32 GB DDR4 of DDR 5 en 512 GB SSD.
+
+### Reactie 10 — @matthiasoliveiro (2025-05-12)
+
+@ruben kan je deze oppakken?
+
+### Reactie 11 — @markbacker (2025-05-14)
+
+Vanochtend nog enkele testen gedaan om te achterhalen waarom het plotten op mijn laptop zoveel langer duurde.
+
+Nu getest op mijn desktop.
+- Plotten van de view Basisbeveiligingsniveau van referentiecomponenten:
+ - 6 sec (Edge zonder extensies)
+ - 14 sec (Edge met uBlock Origin)
+ - 30 tot 40 sec (Firefox met uBlock en Privacy Badger)
+ - 16 sec (firefox zonder extensies)
+- Plotten view Betrouwbaarheidscriteria van referentiecomponenten
+ - 26 sec (firefox zonder extensies)
+ - 9 sec (Edge zonder extensies)
+ - 20 sec (Edge met uBlock Origin)
+
+resultaat
+- mijn desktop halveert de responsetijden die we gisteren zagen.
+- maar, meer power is niet de oplossing
+ - plus firefox is voor het plotten heel veel trager dan Edge (en waarschijnlijk andere chromium browsers)
+ - de add-blocker uBlock origin verdubbeld de rendertijd
+
+Conclusie:
+De performance is goed in op Chrome-gebaseerde browsers. Ook met de extensie uBlock Origin blijft de performance in deze browsers acceptabel.
+In Firefox is de performance acceptabel, maar ronduit slecht wanneer ook de extensie uBlock Origin is ingeschakeld.
+
+Bovenstaande conclusie is dan nog wel afhankelijk van de gebruikte hardware. Voor grote views moeten we een waarschuwing gaan tonen dat het lang kan duren.
+
+### Reactie 12 — @Makkmetp (2025-05-19)
+
+Testresultaten:
+
+- Applicatieservices generiek en buitengemeentelijke voorzieningen < 7 seconden
+- ZGW Componenten geplot op GA02 < 6 seconden
+- Referentiecomponentenlandschap < 19 sec. Pas na nog eens 20 seconden wordt de plaat interactief: tooltips, inzoomen, e.d. is daarvoor niet mogelijk. Bij de 2e keer direct actief. Je ziet dat daar de https://vng.opencatalogi.nl/static/js/2048.1d5c688c.chunk.js nog aan het laden is, terwijl dat de view al wel beschikbaar is, maar niet interactief is.
+
+Zoals hierboven aangegeven: voor grote views is een waarschuwing dat dat even kan duren voor dat deze volledig geladen is, geen overbodige luxe.
+
+### Reactie 13 — @rubenvdlinde (2026-02-16)
+
+We dachten al langer dat er nog winst te behalen was met het herschrijven van de packadge en dat hebben we gedaan, onderstaand de test resultaten op chromium (waar we auto tests op kunnen draaien) voor de archimate plaat "Poster basisbeveiligingsniveau van referentiecomponenten" met 388 nodes de grootste plaat in de software catalogus op dit moment.
+
+
+
+Dat is dan nog wel zonder het plotten van gebruik, applicaties of deelnemingen. Dat is de volgende stap.
+
+Vraag: @Makkmetp en @markbacker we hebben een accepatie creterium nodig voor de perfomance. Uit mijn eerdere aantekeningen komt 11 seconde naar voeren? klopt dat.
+
+Eigenlijk zijn dit 4 stappen, waarvan 3 na het openen van de view door het aanklikken van een checkbox
+- Openen en laden view
+- Laden van gebruik
+- Laden van applicaties
+- Laden van deelnemingen
+
+Geld 11 seconde voor allles bij elkaar? Dus 3 seconde per stap pak een beet?
+
+### Reactie 14 — @rubenvdlinde (2026-02-18)
+
+Afspraak: We gaan voor 3 seconde, per fase gemiddeld en 11 seconde in totaal.
+
+### Reactie 15 — @Makkmetp (2026-02-18)
+
+#154 is gesloten aangezien deze verder gaat in dit issue.
diff --git a/issues/169.md b/issues/169.md
new file mode 100644
index 00000000..400054f9
--- /dev/null
+++ b/issues/169.md
@@ -0,0 +1,104 @@
+# #169 — Rest issues van Organisatie en Configuratie
+
+**Status:** OPEN | **Labels:** Organisatie en configuratie, Restpunt
+**Auteur:** @Makkmetp | **Datum:** 2025-07-21
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/169
+
+---
+
+## Beschrijving
+
+Dit issue is een vervolg van de issues van Organisatie en Configuratie. Deze is reeds geaccepteerd met een aantal restpunten.
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2025-07-21)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @Makkmetp (2025-07-21)
+
+Restpunten vanuit issue #139
+- [x] Door het traag opslaan wordt een organisatie soms dubbel aangemaakt. Het gaat goed, wanneer de registratie binnen 10 seconden wordt verwerkt. Al is dit ook al erg lang.
+- [ ] Het registratieformulier lijkt niet gekoppeld te zijn aan de velden van het formulier Mijn Account. Bij het registratieformulier wordt niet gevraagd om een tussenvoegsel welke wel aanwezig is in Mijn Account. In het Mijn Account formulier worden de Voornaam en Achternaam uit het registratieformulier als Weergavenaam samengevoegd.
+- [x] In Mijn Account wordt de organisatie van de gebruiker gemist.
+- [x] Ondanks het opvoeren van een KVKNummer wordt deze niet getoond in het Organisatie bewerken- formulier.
+- [ ] Graag een eenduidige schrijfwijze van alle titels bij invoervelden en met hoofdletters. Stem het Organisatie bewerken- formulier af met het registratieformulier.
+- [x] Maak de default sector gemeente in het Organisatie bewerken- formulier. Beter nog is om dit default te maken en het veld niet te tonen.
+- [x] Na een eerste keer inloggen krijg ik vaker de melding Nextcloud autorisatie - De tijd is verstreken. Probeer het opnieuw. Als dit elke keer gebeurt, haken bezoekers sneller af. Graag dit verbeteren.
+- [ ] Na op activeren te klikken via organisaties, veranderd de status niet in het overzicht. Deze blijft op Concept staan, terwijl via het menu onder acties nu de opties is veranderd in x Deactiveren.
+- [x] Na het activeren van een organisatie, wordt de gebruiker niet direct actief gemaakt.
+- [x] Na het handmatig activeren wordt de gebruiker alleen aan de groep Aanbod-beheerder toegevoegd. Moet er dan ook niet direct een nieuwe groep "Organisatienaam" aangemaakt worden?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+### Reactie 3 — @remko48 (2025-10-29)
+
+@rubenvdlinde
+Nextcloud account moet gesynchroniseerd zijn met het gekoppelde contactpersoon
+
+### Reactie 4 — @remko48 (2025-10-29)
+
+@SudoThijn
+
+
+
+- [x] Op de pagina zelf mogen weergavenaam en E-mail geverifieerd weg.
+- [x] Wel moet Organisatie terug komen met daarin de naam van de Organisatie wat ook een link is naar /my-organisation
+
+
+
+- [x] In het formulier mag de Weergavenaam invoer weg. Die is niet belangrijk voor de gebruiken en is alleen belangrijk voor Nextcloud en is alleen te zien in Nextcloud.
+- [x] Hiervoor mag Functie in de plaats komen.
+
+De data gaat gesynchroniseerd worden met het contactpersoon object dus als er nu nog geen data in zit is dat geen probleem
+
+
+### Reactie 5 — @Makkmetp (2026-02-26)
+
+❌Het registratieformulier lijkt niet gekoppeld te zijn aan de velden van het formulier Mijn Account. Bij het registratieformulier wordt niet gevraagd om een tussenvoegsel welke wel aanwezig is in Mijn Account. In het Mijn Account formulier worden de Voornaam en Achternaam uit het registratieformulier als Weergavenaam samengevoegd. Zie daarvoor #431
+
+
+
+✅ Graag een eenduidige schrijfwijze van alle titels bij invoervelden en met hoofdletters. Stem het Organisatie bewerken- formulier af met het registratieformulier. > De schrijfwijze op beide formulieren is gelijk. Het veld tussenvoegsel ontbreekt alleen bij het aanmeldproces. Zie daarvoor #431
+
+
+✅ Na op activeren te klikken via organisaties, veranderd de status niet in het overzicht. Deze blijft op Concept staan, terwijl via het menu onder acties nu de opties is veranderd in x Deactiveren. > De status wijzigt nu in de configuratie
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/issues/172.md b/issues/172.md
new file mode 100644
index 00000000..518b6859
--- /dev/null
+++ b/issues/172.md
@@ -0,0 +1,160 @@
+# #172 — Testresultaten Jeroen de Ruig 5/9/2025 acceptatietest
+
+**Status:** CLOSED | **Labels:** Aanbod, Bevinding, Restpunt
+**Auteur:** @rubenvdlinde | **Datum:** 2025-09-05
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/172
+
+---
+
+## Beschrijving
+
+
+
+
+
+
+- [x] Nice to have: Hier een icoon in de vorm van een ‘plus’ waarmee duidelijk wordt dat je nog een referentiecomponent kan toevoegen. (@SudoThijn )
+
+
+
+
+- [X] Ik zie nu codes staan. Op test zie ik teksten staan. Dit werkt dus niet goed.
+@rubenvdlinde : Wontfix: Want dit werkt correct, na de import hebben objecten nog geen metadata (daarvoor moet eerst een verijking worden gedraaid) en word de id als fallback verwerkt. Ik zal de verijking zo draaien
+
+
+
+- [x] Waarom staat hier ‘Ge…zen’ naast bestand kiezen? (@SudoThijn kan jij deze bekijken? ik weet niet wat hier de gewenste tekst is voor de gebruiker maar je kan beter geen tekst hebben dan dit)
+
+
+
+
+- [x] Als ik klik op bewijs, dan krijg ik het geuploade document niet te zien. Blijft heel lang laden. (@remko48 deze moet dus worden hertest aan conduction zijde, @jderuig1968 kan je ons het bewijsstuk sturen dat je hebt gebruikt?)
+
+
+
+
+- [x] Ik voer nog een product op en ik krijg opeens geen referentiecomponenten meer, maar standaarden bij referentiecomponenten. Ra ra hoe kan dat? Na het legen van de cache komen de referentiecomponenten weer. (@jderuig1968 had jij je cashe al geleegd voordat je ging testen? @remko48 dit is hetzelfde issue als wat @Makkmetp ook had gemeld maar we niet hertest kregen
+
+
+
+- [X] Waarom zijn hier twee velden voor. Als ik linkstype dan zie ik zowel rechts als links het resultaat. Eén veld lijkt mij voldoende.
+
+@rubenvdlinde: Hier zijn twee velden voor omdat je die optie (rechts boven in) aan hebt staan. Dat is ook de default waarde op verzoek VNG ;) ik vind zelf een veld ook duidenlijk dus ga graag mee in de default waarde één veld
+
+
+
+
+- [X] Ik zie allemaal contacten staan die volgens mij geen deel uitmaken van mijn organisatie. Rechten dingetje denk ik.
+@rubenvdlinde : Wontfix, dee contacten maken onderdeel uit van je organisatie omdat je bent ingelogd met default organisatie. Waarom je acount daaraan hangt is hang ik even aan een vraag hieronder.
+
+
+
+- [x] Mijn opgevoerde applicatie is niet module 1, maar VNG. Module 1 is de enige die ik kan selecteren. (@SudoThijn kan jij naar deze kijken?)
+
+
+
+
+- [x] Groter veld om in te voeren. Dit werkt niet prettig. (@SudoThijn eens, kan jij dee oppaken, dan mag diensttype dus kleiner)
+
+
+
+- [x] Lange tekst in het resultaat wordt niet afgebroken. Dat moet wel. Nu zie je niet dat je op product opslaan moet klikken. (@SudoThijn ook deze meenemen in opschonen wizards)
+
+(geen afbeelding)
+
+- [x] Het opslaan van een product duurt een aantal seconde, zou iets sneller mogen. (@MWest2020 laten we kijken of de NFS update op Test het gewenste effect op perfomance heeft deze dan naar acc brengen)
+
+
+
+
+- [x] Ik heb net het product VNG opgevoerd. Als ik vervolgens hierop zoek vind ik niets.(@jderuig1968 heb je het product ook gepubliceerd)
+
+
+
+
+- [x] Ik selecteer de bovenstaande referentiecomponenten. Ik krijg de volgende standaarden, met speciale aandacht voor de referentiecomponenten, die komen dus niet overeen: (@remko48 zou jij deze kunnen repliceren)
+
+
+
+- [x] In het overzicht staat bij diensten alleen de toelichting. Je zou hier ook de geselecteerde dienst verwachten. Ik mis de standaarden, maar misschien wil je die hier niet zien (te lange lijst). (@SudoThijn mee nemen in opschonen wizard)
+
+
+
+
+- [x] Korte beschrijving is niet gevuld, terwijl is daar wat had ingevoerd. Ik mis ook de lange beschrijving in het overzicht. (@SudoThijn mee nemen in opschonen wizard)
+
+
+
+
+- [x] Ik zie ‘default organisation’ staan. Mijn organisatie is inmiddels bekend toch? Dan verwacht ik daar de naam. (@rubenvdlinde uitzoeken waarom Jeroen aan de default org hangt das gek, zijn er admin rechten toegekent?)
+
+
+
+---
+
+## Reacties (7)
+
+### Reactie 1 — @github-actions (2025-09-05)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2025-09-05)
+
+Let op! Jeroen heeft twee keer achter elkaar de wizard doorlopen, het zou kunnen dat bij het starten van de wizard de cach niet word geleegd @SudoThijn checken!
+
+### Reactie 3 — @remko48 (2025-09-05)
+
+> Waarom staat hier ‘Ge…zen’ naast bestand kiezen? (@SudoThijn kan jij deze bekijken? ik weet niet wat hier de gewenste tekst is voor de gebruiker maar je kan beter geen tekst hebben dan dit)
+
+Hier staat "Geen bestand gekozen" maar doordat het veld in elkaar gekrompen is is dat niet te zien. Dus het veld zou verbreed moeten worden
+
+### Reactie 4 — @remko48 (2025-09-05)
+
+> - [X] Waarom zijn hier twee velden voor. Als ik linkstype dan zie ik zowel rechts als links het resultaat. Eén veld lijkt mij voldoende. @rubenvdlinde: Hier zijn twee velden voor omdat je die optie (rechts boven in) aan hebt staan. Dat is ook de default waarde op verzoek VNG ;) ik vind zelf een veld ook duidenlijk dus ga graag mee in de default waarde één veld
+
+Dat hier twee velden staan is omdat we markdown ondersteuen op verzoek van de VNG Als je in het rechter veld markdown waardes invoerd is dit direct te zien in de rechterkant
+
+
+
+### Reactie 5 — @Makkmetp (2025-12-18)
+
+Alle gevonden punten moeten nog doorgenomen worden.
+
+### Reactie 6 — @Makkmetp (2026-02-03)
+
+
+- [x] Nice to have: Hier een icoon in de vorm van een ‘plus’ waarmee duidelijk wordt dat je nog een referentiecomponent kan toevoegen. (@SudoThijn ) ✅Nice to have.
+- [X] Ik zie nu codes staan. Op test zie ik teksten staan. Dit werkt dus niet goed.
+@rubenvdlinde : Wontfix: Want dit werkt correct, na de import hebben objecten nog geen metadata (daarvoor moet eerst een verijking worden gedraaid) en word de id als fallback verwerkt. Ik zal de verijking zo draaien ✅Standaardversies worden nu getoond in tekst.
+ - [x] Waarom staat hier ‘Ge…zen’ naast bestand kiezen? (@SudoThijn kan jij deze bekijken? ik weet niet wat hier de gewenste tekst is voor de gebruiker maar je kan beter geen tekst hebben dan dit) ✅Label staat nu voluit geschreven in beeld.
+- [x] Als ik klik op bewijs, dan krijg ik het geuploade document niet te zien. Blijft heel lang laden. (@remko48 deze moet dus worden hertest aan conduction zijde, @jderuig1968 kan je ons het bewijsstuk sturen dat je hebt gebruikt?) ✅ Getest en opgelost. Klik op de link naar een website wordt in een ander issue opgelost
+- [x] Ik voer nog een product op en ik krijg opeens geen referentiecomponenten meer, maar standaarden bij referentiecomponenten. Ra ra hoe kan dat? Na het legen van de cache komen de referentiecomponenten weer. (@jderuig1968 had jij je cashe al geleegd voordat je ging testen? @remko48 dit is hetzelfde issue als wat @Makkmetp ook had gemeld maar we niet hertest kregen. ✅Opgelost. Was nog bij producten
+- [X] Waarom zijn hier twee velden voor. Als ik linkstype dan zie ik zowel rechts als links het resultaat. Eén veld lijkt mij voldoende. ✅Uitleg is voldoende en er is hiervoor gekozen. Nog wel een goede handleiding voor gebruikers maken.
+@rubenvdlinde: Hier zijn twee velden voor omdat je die optie (rechts boven in) aan hebt staan. Dat is ook de default waarde op verzoek VNG ;) ik vind zelf een veld ook duidenlijk dus ga graag mee in de default waarde één veld
+- [X] Ik zie allemaal contacten staan die volgens mij geen deel uitmaken van mijn organisatie. Rechten dingetje denk ik.
+@rubenvdlinde : Wontfix, dee contacten maken onderdeel uit van je organisatie omdat je bent ingelogd met default organisatie. Waarom je acount daaraan hangt is hang ik even aan een vraag hieronder. ✅Getest en is opgelost.
+- [x] Mijn opgevoerde applicatie is niet module 1, maar VNG. Module 1 is de enige die ik kan selecteren. (@SudoThijn kan jij naar deze kijken?) ✅ Betreft nog producten
+- [x] Groter veld om in te voeren. Dit werkt niet prettig. (@SudoThijn eens, kan jij dee oppaken, dan mag diensttype dus kleiner)
+✅Dienst is verwijderd uit de wizards om applicaties te publiceren.
+- [x] Lange tekst in het resultaat wordt niet afgebroken. Dat moet wel. Nu zie je niet dat je op product opslaan moet klikken. (@SudoThijn ook deze meenemen in opschonen wizards) ✅Was onderdeel van producten
+- [x] Het opslaan van een product duurt een aantal seconde, zou iets sneller mogen. (@MWest2020 laten we kijken of de NFS update op Test het gewenste effect op perfomance heeft deze dan naar acc brengen) ✅Was onderdeel van producten
+- [x] Ik heb net het product VNG opgevoerd. Als ik vervolgens hierop zoek vind ik niets.(@jderuig1968 heb je het product ook gepubliceerd) ✅ Was onderdeel van producten en publiceren. In nieuwe versies niet meer aanwezig.
+- [x] Ik selecteer de bovenstaande referentiecomponenten. Ik krijg de volgende standaarden, met speciale aandacht voor de referentiecomponenten, die komen dus niet overeen: (@remko48 zou jij deze kunnen repliceren) ✅Laatste tests laten standaarden zien. Er dient nog wel een vergelijk gemaakt te worden met de huidige softwarecatalogus wanneer men ingelogd is. Hier is een ander issue voor.
+- [x] In het overzicht staat bij diensten alleen de toelichting. Je zou hier ook de geselecteerde dienst verwachten. Ik mis de standaarden, maar misschien wil je die hier niet zien (te lange lijst). (@SudoThijn mee nemen in opschonen wizard) ✅Getest en opgelost
+- [x] Korte beschrijving is niet gevuld, terwijl is daar wat had ingevoerd. Ik mis ook de lange beschrijving in het overzicht. (@SudoThijn mee nemen in opschonen wizard) ✅In ander issue opgenomen om te testen.
+- [x] Ik zie ‘default organisation’ staan. Mijn organisatie is inmiddels bekend toch? Dan verwacht ik daar de naam. (@rubenvdlinde uitzoeken waarom Jeroen aan de default org hangt das gek, zijn er admin rechten toegekent?) ✅ Is inmiddels opgelost
+
+
+
+### Reactie 7 — @markbacker (2026-02-04)
+
+Dit is een oud issue. Issue die nog niet gefixt zijn, staan in andere issue.
+
+Deze wodt hiermee gesloten
diff --git a/issues/185.md b/issues/185.md
new file mode 100644
index 00000000..f6df86d8
--- /dev/null
+++ b/issues/185.md
@@ -0,0 +1,84 @@
+# #185 — Detailpagina's
+
+**Status:** CLOSED | **Labels:** Aanbod, Bevinding, Restpunt
+**Auteur:** @Makkmetp | **Datum:** 2025-10-16
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/185
+
+---
+
+## Beschrijving
+
+- [X] Het is onduidelijk op wat voor pagina je bent: Organisatie, Product, Applicatie of versie, koppeling en diensten ? Graag de naam tonen (@remko48 ).
+- [x] Toon de naam van de leverancier op een applicatie of productpagina (@remko48 ) ✅
+- [X] Standaarden verplaatsen naar tabblad (@SudoThijn ) ✅
+- [X] Geschikt voor verplaatsen naar tabblad (@SudoThijn ) ✅
+- [x] Naam tabblad Producten aanpassen naar Onderdeel van product(en) (@remko48 ) ✅
+- [ ]
+
+
+
+- [X] Bug: Ik mis beschrijving lang en kort (bestaan allebei in data) (@SudoThijn ) ✅
+- [X] Bug: Bij deze applicatie mis ik tabs voor Producten, Applicatie versies, Gebruik, .. (zie vorige sheet waar deze wel staan) (@rubenvdlinde ) ✅
+
+- [x] Productpagina's hebben meerdere vormgevingen. Via zoeken of via myn organisatie vind je twee verschillende vormgeving. Graag maar één optie beschikbaar maken (@remko48 ).
+
+
+
+- [x] Uitlijning regel van een product niet gelijk. Selectbox staat lager (@remko48 ) ✅
+
+
+
+---
+
+## Reacties (8)
+
+### Reactie 1 — @github-actions (2025-10-16)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2025-10-16)
+
+Het niet tonen van elge tabladen is een design keuze
+
+### Reactie 3 — @remko48 (2025-10-17)
+
+> Toon de naam van de leverancier op een applicatie of productpagina
+
+Is toegevoegd. Let op Module heeft geen directe relatie met een organisatie in het dataModel.
+Dit is opgelost door de data te gebruiken dat wij zelf op de objecten zetten
+
+### Reactie 4 — @remko48 (2025-10-17)
+
+> Prodcutpagina's hebben meerdere vormgevingen. Via zoeken of via myn organisatie vind je twee verschillende vormgeving. Graag maar één optie beschikbaar maken
+
+Als besproken gisteren. We kunnen de detailpagina's niet uniform maken in verband met scherm grootte van de Zoekkant en de Beheerkant
+
+### Reactie 5 — @Makkmetp (2025-10-29)
+
+- [x] Wanneer ik via zoeken de pagina van mijn applicatie wil bewerken is de URL en het menu-item Applicaties (links) is niet highlighted/dik gedrukt: https://softwarecatalogus.accept.opencatalogi.nl/beheer/module/f866ecd6-7a70-477f-b727-2524fc1fb26e
+
+Wanneer ik de applicatie via Applicaties open en dan bewerk is de URL:
+https://softwarecatalogus.accept.opencatalogi.nl/beheer/applicaties/f866ecd6-7a70-477f-b727-2524fc1fb26e
+Is het mogelijk om de URL's en het kruimelpad ten alle tijden gelijk te houden? Qua vormgeving zijn ze hetzelfde. (@remko48)
+
+
+### Reactie 6 — @remko48 (2025-10-29)
+
+Ja zeker en dat hoort ook zelfs.
+Deze alleen net gemist en gaat aangepast worden.
+De functionaliteit werkt wel dus ik ga er vanuit dat hij niet brekend is
+
+### Reactie 7 — @rubenvdlinde (2025-12-18)
+
+@remko48 controleren of deze is opgelost
+
+### Reactie 8 — @Makkmetp (2026-02-24)
+
+✅Deze is opgelost aangezien alle bewerkingen voor applicaties, diensten en koppelingen via de wizards gaan.
diff --git a/issues/186.md b/issues/186.md
new file mode 100644
index 00000000..9c2d8c7f
--- /dev/null
+++ b/issues/186.md
@@ -0,0 +1,52 @@
+# #186 — Koppelingen
+
+**Status:** OPEN | **Labels:** Aanbod, Bevinding, Restpunt, Koppeling
+**Auteur:** @Makkmetp | **Datum:** 2025-10-16
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/186
+
+---
+
+## Beschrijving
+
+- [x] Bug: koppeling met een buitengemeentelijke voorziening. Toon de buitengemeentelijkVoorziening waarnaar wordt verwezen. (Element met GEMMA type=buitengemeentelijke voorziening) > Na aanbod afronden
+- [x] Koppelingen hebben geen title. Toon koppelingen in een tabel > Na aanbod afronden
+
+
+https://softwarecatalogus.accept.opencatalogi.nl/publicatie/548a5193-
+4f20-556a-b20f-cddb6322934c
+
+
+- [x] Er zijn koppelingen met niet bestaande applicaties.
+Zie applicatie: https://softwarecatalogus.accept.opencatalogi.nl/publicatie/0b1bd48b-eb07-
+5486-9f65-d09085a46be3
+
+En koppeling: https://softwarecatalogus.accept.opencatalogi.nl/publicatie/35bf8a40-70ba-
+5c07-91c2-23406b83ae7d
+
+
+- [x] Koppeling: buitengemeentelijk niet getoond. Koppelingen hebben geen titel
+- [x] Bug: koppeling met een buitengemeentelijke voorziening. Toon de buitengemeentelijkVoorziening waarnaar wordt verwezen. (Element met GEMMA type=buitengemeentelijke voorziening)
+https://softwarecatalogus.accept.opencatalogi.nl/publicatie/548a5193-
+4f20-556a-b20f-cddb6322934c
+
+
+
+- [x] Detalpagina koppelingen verder uitwerken
+[https://softwarecatalogus.accept.opencatalogi.nl/public](https://softwarecatalogus.accept.opencatalogi.nl/publicatie/000a73b0-51fc-5d35-9c19-8f1cad12d3e5)[atie/000a73b0-51fc-5d35-9c19-8f1cad12d3e5](https://softwarecatalogus.accept.opencatalogi.nl/publicatie/000a73b0-51fc-5d35-9c19-8f1cad12d3e5)
+
+
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2025-10-16)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/187.md b/issues/187.md
new file mode 100644
index 00000000..b01fedec
--- /dev/null
+++ b/issues/187.md
@@ -0,0 +1,98 @@
+# #187 — Tekstvoorstellen
+
+**Status:** OPEN | **Labels:** Aanbod, Tekstuele wijzigingen
+**Auteur:** @Makkmetp | **Datum:** 2025-10-16
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/187
+
+---
+
+## Beschrijving
+
+- [x] Wat is de rol software-catalog-user? Wat zijn de rechten? Leveranciergebruikers hebben rol aanbod-beheerder (@Makkmetp -> dat is een oude rol die kan er uit)
+- [x] Aanmeldproces: tekstvoorstel Contactpersoon (@SudoThijn )
+Het betreft de volgende tekst:
+De geregistreerde contactpersoon is het eerste aanspreekpunt van de organisatie en beheerder van de gebruikers van de softwarecatalogus namens uw organisatie. Dit kan op een later moment nog gewijzigd worden.
+
+
+
+- [ ] Aanmelding succesvol tekst aanpassen
+Graag de tekst aanpassen naar:
+
+Aanmelding succesvol!
+Beste van ,
+
+Uw aanmelding voor de softwarecatalogus is in goede orde ontvangen. We hebben een bevestigingsmail gestuurd naar . Controleer uw inbox (en eventueel uw spam folder) voor deze bevestiging.
+
+Een beheerder beoordeeld de aanmelding. Zodra de aanmelding is goedgekeurd, ontvangt u een nieuwe e-mail met daarin uw inloggegevens en verdere instructies voor het gebruik van de softwarecatalogus.
+
+Heeft u vragen? Neem dan contact op met via [softwarecatalogus@vng.nl](mailto:softwarecatalogus@vng.nl)
+
+
+- [x] Organisatie tekst aanpassen (@SudoThijn )
+
+- [ ] Tekst aanpassen Welkom in de softwarecatalogus
+
+**Contactpersoon toevoegen**
+Na het succesvol aanmelden van een contactpersoon wordt onderstaande tekst getoond. De tekst graag aanpassen naar:
+**Gebruiker toevoegen**
+De gebruiker is succesvol toegevoegd. (+ de acties die nodig zijn om de gebruiker te activeren)
+
+**Contactpersoon depubliceren**
+Graag de tekst aanpassen naar:
+**Gebruiker uitschakelen**
+Weet u zeker dat u deze gebruiker wilt depubliceren?
+
+**Referentiecomponenten**
+Graag de link "https://www.gemmaonline.nl/wiki/Overzicht_alle_referentiecomponenten" in de tekst van de wizard product aanmelden achter de tekst "alle referentiecomponenten" plaatsen, zodat deze aanklikbaar is.
+
+**Contactpersonen**
+Graag Contactpersonen in het menu aan de linkerkant aanpassen naar "Gebruikers".
+
+**Wizard Diensten registreren**
+Graag de volgende tekst aanpassen in de wizard Diensten registreren (see issue for full details).
+
+---
+
+## Reacties (7)
+
+### Reactie 1 — @github-actions (2025-10-16)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+### Reactie 2 — @rubenvdlinde (2025-10-28)
+
+- Nog toevoegen testk voorstell: uitleg zoeken / helpt texts
+
+### Reactie 3 — @remko48 (2025-10-28)
+
+@Makkmetp @markbacker @jderuig1968
+Bij onze interne testen kwamen we tegen dat we nog 4 pagina's hebben waar wij de tekst invulling hebben gedaan.
+https://softwarecatalogus.accept.opencatalogi.nl/privacyverklaring
+https://softwarecatalogus.accept.opencatalogi.nl/algemene-voorwaarden
+https://softwarecatalogus.accept.opencatalogi.nl/disclaimer
+https://softwarecatalogus.accept.opencatalogi.nl/faq
+
+Kan hier ook teksten voor worden aangeleverd?
+
+### Reactie 4 — @Makkmetp (2025-10-29)
+
+Onderstaande teksten zijn op 29-10-2025 nog doorgegeven
+- [ ] Product succesvol aangemeld! is niet overgenomen.
+
+### Reactie 5 — @Makkmetp (2025-11-18)
+
+Hierbij de tekstvoorstellen voor de wizards van aanbod.
+
+### Reactie 6 — @rubenvdlinde (2026-02-15)
+
+@remko48 kan jij controleren of alle teksten die hier boven genoemd zijn ook zijn verwerkt?
+
+### Reactie 7 — @Makkmetp (2026-03-04)
+
+Door verloop van tijd zijn sommige tekstuele wijzigingen niet meer van toepassingen. Degene die hieronder staan zijn ook eerder genoemd in dit issue en verdienen nog aandacht:
+
+❌De tekst bij een contactpersoon is niet goed overgenomen.
+❌Product succesvol aangemeld! is niet overgenomen.
+❌Toelichting voor bij het zoeken
+❌Contactpersonen aanpassen naar Gebruikers in het linkermenu
+❌Gebruiker toevoegen tekst aanpassen
diff --git a/issues/189.md b/issues/189.md
new file mode 100644
index 00000000..36228a9e
--- /dev/null
+++ b/issues/189.md
@@ -0,0 +1,108 @@
+# #189 — Organisatie
+
+**Status:** CLOSED | **Labels:** help wanted, Organisatie en configuratie, Bevinding, Restpunt, Beheer
+**Auteur:** @Makkmetp | **Datum:** 2025-10-16
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/189
+
+---
+
+## Beschrijving
+
+- [x] Teveel velden op het formulier om je organisatie te bewerken. CBS Nummer alleen bij gemeenten zichtbaar. KVK Nummer laten vervallen (@SudoThijn). ✅
+- [x] Logo uploaden gaf een foutmelding (@rubenvdlinde ) ✅
+
+
+
+- [x] Er zijn meerdere formulieren om je organisatie te bewerken. Via zoeken > ...Bewerken > bewerken of via mijn organisatie > bewerken. Graag alleen de korte variant gebruiken van My Dashboard gebruiken voor consistentie (@remko48 ).
+
+
+
+- [x] Consistentie in veldnamen en menu items (@Makkmetp -> kan deze eits verder worden uitgewerkt)? ✅
+
+
+
+- [x] Deelnames onderdeel van samenwerkingen of communities. Niet bij leveranciers. Een leverancier kan alleen deelnames hebben aan een community (@SudoThijn ) ✅
+
+
+
+## Contactpersonen
+- [x] Teveel opties bij contactpersoon. (@Makkmetp -> welke moeten weg?) ✅
+
+
+
+- [x] Formulier aanpassen van contactpersoon (@Makkmetp -> welke moeten weg?) ✅
+- [x] Gebruikers aanmaken voor een leverancier(aanbieder) krijgen in alle gevallen de rechten van aanbod-beheerder (@Makkmetp dus begrijpen we denk ik niet goed, een levarncier beheerd aanbod toch?) ✅
+- [x] Situatie: wanneer je een Contactpersoon(gebruiker voor aanbod) aanmaakt en het volledige formulier hebt ingevuld. Wanneer je dan met je muis naast de knoppen(annuleren of opslaan of ergens anders naast het formulier klikt), dan sluit het formulier zonder waarschuwing. En dien je het contactpersoon/gebruiker nogmaals op te voeren. (@jderuig1968 dit was op aanvraag van VNG)
+- [x] Er zit geen optie tussen de acties om de gebruiker te activeren/publiceren. Graag ervoor zorgen dat deze er is. (@Makkmetp even voor de helderheid, je kan niet publiceren maar wel depubliceren?) ❌
+
+
+
+- [ ] Onder acties bij een contactpersoon kan je een gebruiker depubliceren. Het is raar dat in het venster wat opent dan alleen de achternaam verschijnt. Graag de volledige naam tonen. Er komt ook een voorstel voor de tekst daar. (@Makkmetp we ontvangen graag het tekstvoorstel) (@rubenvdlinde de tekst staat in #187 )
+
+
+
+- [x] Gebruikers: Verwijderen laatste gebruiker van een leverancier gaat fout. Het scherm gaat flikkeren op het inlogscherm. (@rubenvdlinde bug)
+
+
+
+- [x] Als leverancier kan ik bij een gemeente pagina acties uitvoeren. Deze knop weglaten bij organisaties. ❌ Bij een gemeente staat de witte plus met blauwe vak nog, waarmee je acties kan uitvoeren voor je eigen organisatie.
+- [x] Toon alleen de door jouw geregistreerde producten/applicaties/koppelingen/diensten bij een gemeente. Alle andere mogen niet getoond worden (@Makkmetp hij kijkt nu nog breed omdat gebruik niet uitgerold is, valt dus buiten deze fase.)
+
+
+
+- [x] Bij een organisatie is het logo niet te verwijderen als deze al is geupload. Wel te vervangen. Graag ook kunnen verwijderen. ✅
+
+
+
+- [x] Bij Mijn Organisatie is niet naar beneden te scrollen (Edge). Dit blijkt incidenteel te zijn (@Makkmetp niet consisten repliceerbaar, we kunnen wat hulp gebruiken bij het patroon vinden). ✅ Tijdens het testen geen last meer van ondervonden.
+
+
+
+- [x] Ik ben ingelogd als peter@faq.nl en gekoppeld aan de organisatie FAQ. Daarnaast zijn er nog twee gebruikers voor deze organisatie opgevoerd: jan en roos. Wanneer ik in de back-end zoek naar de gebruikers van organisatie FAQ(faq) vind ik de gebruikers roos en jan wel, maar niet peter. (via de app softwarecatalogus). Via accounts wel omdat de accounts van peter en roos geactiveerd zijn. Maar daar zie ik niet aan welke organisatie de gebruiker gekoppeld is, maar weer niet via Openregisters > Voorzieningen > Contactpersoon. Ik zou verwachten dat in de softwarecatalogus-app bij een organisatie alle gebruikers te zien zijn. Maar ook via Open registers. Bij accounts is het wenselijk dat daar duidelijk is aan welke organisatie een account is gekoppeld (@Makkmetp het lijkt er op dat eerste contact persoon wordt ontkoppelt). ❌
+
+
+
+
+
+
+
+- [x] De gebruiker Roos heeft in de front-end de rollen "Aanbod-beheerder" en in de back-end " software-catalogus-user." Graag zorgen dat dit overeenkomt en duidelijk is welke rechten ze heeft (@rubenvdlinde bugje). ❌ Via de front-end is een gebruiker nog niet te activeren. Via de back-end krijgt de gebruiker de rechten van de software-catalog-user.
+
+
+
+- [x] Het exporteren van mijn producten gaat nog niet goed met .csv. De lange omschrijving komt onder de regel terecht en het scheiden via tekst naar kolommen op komma geeft de melding "Er bevinden zich hier al gegevens. Wilt u deze vervangen? (@Makkmetp kijken we graag even op mee) ❌ Laten we hier samen naar kijken. Er staan nu UUID's in het ene format .CSV, en in het andere XLSX staan de tekens voor een array.
+
+
+
+- [ ] De excel export lijkt goed. Alleen de volgorde van de kolommen is niet logisch. Logo mag uit de kolommen gelaten worden. Graag de volgende volgorde en naamgeven gebruiken: id, naam, beschrijvingkort, beschrijvinglang, website, contactpersoon, hostingVorm, hostingJurisdictie, hostingLocatie, aanbiederId, modules, omvat, onderdeelVan (@jderuig1968 wijzigings verzoek)
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2025-10-16)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2025-12-18)
+
+Dit issue is gelabeld igs, maar als ik het issue lees zie ik alleen een wijzigings verzoek open staan. @Makkmetp en @markbacker waar lees ik overheen?
+
+### Reactie 3 — @rubenvdlinde (2026-02-18)
+
+Afspraak: @markbacker @Makkmetp gaan issue doornemen en koppelen dan terug
+
+### Reactie 4 — @Makkmetp (2026-02-18)
+
+De export vraag gaat verder in #15. Verfijning volgt daar, indien nodig.
+Depubliceren is niet meer relevant.
+
+Issue wordt geaccepteerd.
+IGS blijft erop voor het overzicht.
diff --git a/issues/190.md b/issues/190.md
new file mode 100644
index 00000000..cf680475
--- /dev/null
+++ b/issues/190.md
@@ -0,0 +1,121 @@
+# #190 — Applicatie en diensten
+
+**Status:** CLOSED | **Labels:** Aanbod, Restpunt
+**Auteur:** @Makkmetp | **Datum:** 2025-10-16
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/190
+
+---
+
+## Beschrijving
+
+- [x] Edge: Na applicatie aanmelden lege pagina en applicatie is niet aangemaakt. (@remko48 )
+
+
+
+- [x] Referentiecomponenten in alfabetische volgorde tonen (@remko48 )
+
+
+
+- [x] Als bewijs bij een standaard dient een bestand of een URL opgevoerd kunnen worden. Als er geen bestand of URL wordt opgevoerd dan is het product niet compliant. Er is dan geen bewijs. (@remko48 )
+
+
+
+- [x] Het Menu ...Acties bij applicaties is te lang. Graag alleen de volgende opties: Bekijken, Bewerken, Depubliceren, Versie toevoegen (indien het ook on-premise is).
+
+
+
+- [x] Bij licentievorm Closed Source kan je de Open source licenties selecteren.
+- [x] Er staat een logo veld welke niet gebruikt kan worden.
+- [x] Type optie graag verwijderen
+
+
+
+- [x] Module aanpassen naar Applicatie
+- [x] Tekstvak "Wat is er nieuw ….." over hele breedte tonen
+- [x] De optie Gebruik verwijderen. Deze heeft geen toegevoegde waarde.
+
+
+
+
+**Dienst registreren**
+- [x] Is dit de wizard die gebruikt wordt voor de diensten op zelf geregistreerde producten of ook op producten geregistreerd van andere aanbieders? Dit vanwege de teksten die er nu gebruikt worden (@Makkmetp bij de). (@rubenvdlinde tekstvoorstel staat in #187 )
+- [x] Niet geactiveerde gebruikers kunnen geselecteerd worden als contactpersoon (@Makkmetp das by design). (@rubenvdlinde duidelijk. Dit blijft zo )
+- [x] Soort dienst is een verplicht veld (@Makkmetp als in dat zou het moeten zijn en is het nu niet?).
+
+
+
+
+- [x] Het zoeken naar een eigen product geeft een No options melding, terwijl wanneer je in de lijst naar beneden scrolt het product wel zichtbaar is en geselecteerd kan worden (@Makkmetp zouden we graag even op mee kijken).
+- [x] Het zou logisch zijn wanneer de eigen producten direct bovenaan staan. Is dat mogelijk? (@jderuig1968 wijzigings verzoek, de zou wel ingewikkeld worden)
+
+
+
+
+- [x] Gebruikers zullen niet begrijpen dat erbij een product nog de applicaties geselecteerd moeten worden waar de dienst van toepassing op is. Zeker bij een product die uit dezelfde applicatie bestaat (@Makkmetp het selecteren van een applicatie is niet verplicht, als er maar één applicatie is zou deze automatisch geselecteerd moeten worden. We kunnn hier evt nog een verduidenlijkende tekst bij plaatsen).
+
+
+
+- [x] Tekst in knoppen Dienst aanmelden aanpassen naar Dienst registreren (@remko48 )
+
+
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2025-10-16)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-10-17)
+
+> Het Menu ...Acties bij applicaties is te lang. Graag alleen de volgende opties: Bekijken, Bewerken, Depubliceren, Versie toevoegen (indien het ook on-premise is).
+
+Applicaties weten niet als ze on-premise zijn. Dit zit aan het product gekoppeld en niet aan de applicatie
+
+### Reactie 3 — @rubenvdlinde (2025-12-18)
+
+Kijken tot in hoevere deze punten nog relevant zijn nu producten is afgeschaald
+
+### Reactie 4 — @Makkmetp (2026-02-03)
+
+- [x] Edge: Na applicatie aanmelden lege pagina en applicatie is niet aangemaakt. (@remko48 )
+✅ Getest en opgelost.
+- [x] Referentiecomponenten in alfabetische volgorde tonen (@remko48 )
+✅ Getest en opgelost.
+- [x] Als bewijs bij een standaard dient een bestand of een URL opgevoerd kunnen worden. Als er geen bestand of URL wordt opgevoerd dan is het product niet compliant. Er is dan geen bewijs. (@remko48 )
+✅ Getest en opgelost. #382 staat nog open en wordt daar verder afgehandeld
+- [x] Het Menu ...Acties bij applicaties is te lang. Graag alleen de volgende opties: Bekijken, Bewerken, Depubliceren, Versie toevoegen (indien het ook on-premise is).
+✅ Getest en opgelost.
+- [x] Bij licentievorm Closed Source kan je de Open source licenties selecteren. ✅ Getest en opgelost.
+- [x] Er staat een logo veld welke niet gebruikt kan worden. ✅ Getest en opgelost.
+- [x] Type optie graag verwijderen ✅ Getest en opgelost.
+- [x] Module aanpassen naar Applicatie ✅ Getest en opgelost.
+- [x] Tekstvak "Wat is er nieuw ….." over hele breedte tonen ✅ Getest en opgelost.
+- [x] De optie Gebruik verwijderen. Deze heeft geen toegevoegde waarde. ✅ Getest en opgelost.
+
+
+**Dienst registreren**
+- [x] Is dit de wizard die gebruikt wordt voor de diensten op zelf geregistreerde producten of ook op producten geregistreerd van andere aanbieders? Dit vanwege de teksten die er nu gebruikt worden (@Makkmetp bij de). (@rubenvdlinde tekstvoorstel staat in #187 ) ✅ Getest en opgelost. De wizard wordt in andere issues verder getest.
+- [x] Niet geactiveerde gebruikers kunnen geselecteerd worden als contactpersoon (@Makkmetp das by design). (@rubenvdlinde duidelijk. Dit blijft zo ) ✅ Getest en opgelost. Nu zijn alleen de contactpersonen van de eigen organisatie te zien en te selecteren.
+- [x] Soort dienst is een verplicht veld (@Makkmetp als in dat zou het moeten zijn en is het nu niet?). ✅ Getest en opgelost. Dienst heeft een eigen wizard en is uit deze wizard gehaald. Issues over deze wizard staan ergens anders
+- [x] Het zoeken naar een eigen product geeft een No options melding, terwijl wanneer je in de lijst naar beneden scrolt het product wel zichtbaar is en geselecteerd kan worden (@Makkmetp zouden we graag even op mee kijken). ✅ Getest en opgelost. Zelf opgevoerde applicaties worden gevonden.
+- [x] Het zou logisch zijn wanneer de eigen producten direct bovenaan staan. Is dat mogelijk? (@jderuig1968 wijzigings verzoek, de zou wel ingewikkeld worden) ✅ Getest en opgelost. Verdere opmerkingen opnemen in #354
+
+
+- [x] Gebruikers zullen niet begrijpen dat erbij een product nog de applicaties geselecteerd moeten worden waar de dienst van toepassing op is. Zeker bij een product die uit dezelfde applicatie bestaat (@Makkmetp het selecteren van een applicatie is niet verplicht, als er maar één applicatie is zou deze automatisch geselecteerd moeten worden. We kunnn hier evt nog een verduidenlijkende tekst bij plaatsen). ✅ Getest en opgelost. Dit is onderdeel van product afschalen geweest.
+
+- [x] Tekst in knoppen Dienst aanmelden aanpassen naar Dienst registreren (@remko48 ) ✅ Getest en opgelost. Wizard heet nu Dienst publiceren.
+
+Alles getest en openstaande punten worden in andere issues verder opgelost.
+
+
diff --git a/issues/191.md b/issues/191.md
new file mode 100644
index 00000000..7681f6ce
--- /dev/null
+++ b/issues/191.md
@@ -0,0 +1,37 @@
+# #191 — Back-End
+
+**Status:** CLOSED | **Labels:** Organisatie en configuratie, Bevinding
+**Auteur:** @Makkmetp | **Datum:** 2025-10-16
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/191
+
+---
+
+## Beschrijving
+
+
+- [x] Gebruiker toevoegen via Back-end geeft een foutmelding (@rubenvdlinde).
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2025-10-16)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2025-12-18)
+
+@rubenvdlinde dubbelcheck en dan naar @Makkmetp zetten
+
+### Reactie 3 — @Makkmetp (2026-02-04)
+
+✅Het aanmaken van een contactpersoon/gebruiker via de back-end is gelukt. Het aanpassen van het wachtwoord en daarna met de nieuwe gebruiker inloggen is goed gegaan.
diff --git a/issues/225.md b/issues/225.md
new file mode 100644
index 00000000..88797ed4
--- /dev/null
+++ b/issues/225.md
@@ -0,0 +1,102 @@
+# #225 — Testresultaten 29-10-2025
+
+**Status:** CLOSED | **Labels:** Aanbod, Bevinding, Restpunt
+**Auteur:** @Makkmetp | **Datum:** 2025-10-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/225
+
+---
+
+## Beschrijving
+
+- [x] URL bij standaarden niet verplichten met https:// ervoor:
+
+
+
+Een gebruiker van Shift2 is geactiveerd en daarna ingelogd. De rechten van de gebruiker is aanbod-beheerder.
+- [x] De gebruiker kan de organisatie niet publiceren via Mijn organisatie.
+- [x] De producten en applicaties die wel vindbaar zijn via de zoekmachine worden niet getoond via Producten of Applicaties in het linkermenu.
+- [x] Als ik een eigen applicatie of product open via zoeken, dan kan ik die niet bewerken.
+
+
+
+- [x] Wanneer je bij een applicatie de on-premise variant voor hosting opvoert dan wordt die de eerste keer opgeslagen. Wanneer je daarna het product gaat bewerken, dan is die hosting variant ervan afgevallen. Graag eerst bespreken voordat eraan gewerkt wordt.
+
+- [x] Shift2 is volgens de back-end gepubliceerd en ook vindbaar via de zoekmachine, maar wanneer je ingelogd bent als gebruiker van Shift2 krijg je de melding dat deze nog niet gepubliceerd is.
+
+
+
+- [ ] De organisatie die vanmorgen is aangemaakt (Baron) is niet te vinden via de zoekmachine. Wel wanneer je de applicatie selecteerd, dan het product wat erbij hoort en dan de organisatie die eraan gekoppeld is. Het zou logisch zijn wanneer je filtert op leverancier Baron, dat de leverancier getoond wordt.
+
+
+
+- [ ]Ik ben ingelogd als een gebruiker van de organisatie Baron. Nu kan ik op de organisatie pagina van een andere organisatie via een blauwe plus een product toevoegen. Die plus zorgt voor verwarring. Je denkt namelijk dat je een product kan toevoegen aan de organisatie Centric. Wanneer je het uitvoert gaat het wel goed. Het product komt bij je eigen organisatie terecht. Mogelijk is de makkelijkste oplossing om de organisatie alleen te kunnen bewerken via Dashboard > Mijn organisatie. Het is verstandig om deze eerst te bespreken voor er ontwikkeld gaat worden.
+https://softwarecatalogus.accept.opencatalogi.nl/publicatie/8654869d-50d1-5945-967a-2406a00ac3ab
+
+
+
+---
+
+## Reacties (7)
+
+### Reactie 1 — @github-actions (2025-10-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-10-29)
+
+Ik zie dat het account dat actief is voor shift2 de alleen de rol `software-catalog-users` en niet `aanbod-beheer`.
+De andere user die onder shift2 hangt heeft die rol wel maar is geen gebruiker.
+Beide hebben ze hetzelfde email-adres. Wat dus de rede kan zijn dat het lijkt dat het niet klopt
+
+### Reactie 3 — @Makkmetp (2025-10-29)
+
+Over welk account heb jij het? @remko48 .
+De gebruiker die ik geactiveerd heb en waarmee wordt ingelogd heeft de rechten aanbod-beheerder
+
+
+
+### Reactie 4 — @remko48 (2025-10-30)
+
+Er zijn twee accounts op nextcloud voor Shift2
+
+
+
+Die zijn ook te zien als contactpersonen maar deze contactpersonen zijn gelinkt aan de juiste organisatie.
+Maar de nextcloud accounts zijn hier niet goed gelinkt aan vanwege meerdere data imports
+
+
+
+
+
+
+
+
+### Reactie 5 — @rubenvdlinde (2025-12-18)
+
+Deze punten komen als het goed is allemaal te vervallen met het nieuw RBAC ipv published verhaal
+
+### Reactie 6 — @Makkmetp (2026-03-03)
+
+✅URL bij standaarden niet verplichten met https:// ervoor:
+✅De gebruiker kan de organisatie niet publiceren via Mijn organisatie. Publiceren is verwijderd
+✅ De producten en applicaties die wel vindbaar zijn via de zoekmachine worden niet getoond via Producten of Applicaties in het linkermenu. -> Producten is verwijderd.
+✅ Als ik een eigen applicatie of product open via zoeken, dan kan ik die niet bewerken. -> zelf opgevoerde of geimporteerde applicaties zijn te bewerken.
+✅Wanneer je bij een applicatie de on-premise variant voor hosting opvoert dan wordt die de eerste keer opgeslagen. Wanneer je daarna het product gaat bewerken, dan is die hosting variant ervan afgevallen. Graag eerst bespreken voordat eraan gewerkt wordt. -> de varianten blijven bestaan en zijn te bewerken.
+
+
+
+✅Shift2 is volgens de back-end gepubliceerd en ook vindbaar via de zoekmachine, maar wanneer je ingelogd bent als gebruiker van Shift2 krijg je de melding dat deze nog niet gepubliceerd is.-> is komen te vervallen omdat Publiceren is verwijderd.
+✅De organisatie die vanmorgen is aangemaakt (Baron) is niet te vinden via de zoekmachine. Wel wanneer je de applicatie selecteert, dan het product wat erbij hoort en dan de organisatie die eraan gekoppeld is. Het zou logisch zijn wanneer je filtert op leverancier Baron, dat de leverancier getoond wordt. -> De organisatie is direct vindbaar zonder controle door een beheerder van VNG. Dit zorgt voor grote kans op spam op de softwarecatalogus. Hiervoor wordt een nieuw issue ingeschoten -> #447
+
+✅Ik ben ingelogd als een gebruiker van de organisatie Baron. Nu kan ik op de organisatie pagina van een andere organisatie via een blauwe plus een product toevoegen. Die plus zorgt voor verwarring. Je denkt namelijk dat je een product kan toevoegen aan de organisatie Centric. Wanneer je het uitvoert gaat het wel goed. Het product komt bij je eigen organisatie terecht. Mogelijk is de makkelijkste oplossing om de organisatie alleen te kunnen bewerken via Dashboard > Mijn organisatie. Het is verstandig om deze eerst te bespreken voor er ontwikkeld gaat worden. -> Deze plus is inderdaad weg. Voor de andere plussen op andere overzichtspagina's wordt een nieuw issue gemaakt. Zie #448 voor verdere uitwerkingen.
+
+### Reactie 7 — @markbacker (2026-03-04)
+
+Met de overgebleven punten in nieuwe issues kan deze gesloten.
diff --git a/issues/23.md b/issues/23.md
new file mode 100644
index 00000000..55d92188
--- /dev/null
+++ b/issues/23.md
@@ -0,0 +1,763 @@
+# #23 — Als aanbod- en gebruik-beheerder van de huidige Softwarecatalogus wil ik mijn reeds geregistreerde gegevens weer zien in de nieuwe Softwarecatalogus
+
+**Status:** OPEN | **Labels:** Organisatie en configuratie, Aanbod, Gebruik, PvE eis, Bevinding, Datamigratie
+**Auteur:** @rubenvdlinde | **Datum:** 2025-02-06
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/23
+
+---
+
+## Beschrijving
+
+zodat ik deze niet opnieuw hoef in te voeren.
+De data-migratie is beschreven in het Word-document Programma van Eisen Softwarecatalogus"
+
+
+---
+
+## Reacties (27)
+
+### Reactie 1 — @github-actions (2025-02-06)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2025-03-19)
+
+@Makkmetp zou je een voorbeeld import bestand kunnen aanleveren?
+
+### Reactie 3 — @Makkmetp (2025-04-16)
+
+@rubenvdlinde Ik heb een aantal dummy bestanden naar je gemaild. Laat maar weten of dat voldoende is.
+
+### Reactie 4 — @rubenvdlinde (2025-05-12)
+
+@matthiasoliveiro als de pr van de import functie er doorheen is kunnen we de bestanden gebruiken voor de eerste import test met de huidige gegevens.
+
+### Reactie 5 — @rubenvdlinde (2025-05-12)
+
+@matthiasoliveiro en @Makkmetp de pr is er doorheen dus wat mij betreft kunnen we deze morgen even kort doortesten en kijken of het werkt. And if so dit issue naar een later moment schuiven ivm de definitieve import die pas einde project wordt gedaan.
+
+### Reactie 6 — @Makkmetp (2025-05-14)
+
+Dit zou inderdaad fijn zijn om te testen. Is er ergens een uitleg/handleiding om dit uit te voeren?
+
+### Reactie 7 — @Makkmetp (2025-05-22)
+
+Er is zojuist een succesvolle import gedraaid op de test omgeving van organisaties met daarin samenwerkingen, gemeenten en leveranciers.
+Er zijn een aantal velden toegevoegd aan het schema Organisatie, alleen of het het juiste format is, dient nog na te worden gegaan. Er wordt vandaag nog gewerkt aan alle velden die nodig zijn voor "Organisatie" en welke we kunnen vullen. Een aantal velden zullen namelijk leeg zijn bij de import. Dat zijn onder andere:
+- beschrijving kort
+- beschrijving lang
+- oin
+- kvk
+- logo
+- links
+
+Het is nog niet getest op acceptatie. Om het daar te kunnen accepteren wil ik graag dat alle velden in het correcte format worden opgevoerd. De velden staan in de powerpoint, die nog onder handen is.
+
+### Reactie 8 — @Makkmetp (2025-05-23)
+
+@rubenvdlinde gaat het lukken om de velden voor maandag toe te voegen aan acceptatie?
+
+### Reactie 9 — @rubenvdlinde (2025-05-23)
+
+@Makkmetp ik heb op de radar staan om ze na de lunch toe te voegen dus ja
+
+### Reactie 10 — @Makkmetp (2025-05-26)
+
+@rubenvdlinde De import heeft +/- 22 seconden gedraaid om 790 objecten aan te maken. Dit zijn alle gemeenten, samenwerkingen en leveranciers uit de huidige softwarecatalogus.
+
+Wat me tot nu toe opvalt:
+Bij de samenwerkingen worden er nog geen relaties gelegd met de bijbehorende gemeenten op basis van het id. Deze id's zijn ingelezen als een array [""]. De uses en used by zijn beiden leeg. Zie bijvoorbeeld c80da306-528c-4c05-a231-e110a9b16b70
+
+Default waarde van de weergave van de organisaties in de front-end dient nog aangepast te worden.
+Graag daar de volgende velden default zetten wanneer je de pagina voor het eerst toont:
+- Naam (altijd vooraan zetten)
+- Contactpersonen (voor dit veld geldt dat er altijd maar één contactpersoon is namens een organisatie)
+- E-mailadres
+En is de volgorde van de kollommen aan te passen?
+
+Naast de gemeenten zijn de leveranciers en samenwerkingen ook geimporteerd. In de back end worden deze wel getoond. In de front-end Beheer Organisaties niet. Is daar een reden voor?
+
+Al met al een eerste goede test op acceptatie.
+
+### Reactie 11 — @rubenvdlinde (2025-05-27)
+
+@Makkmetp ik ga er even naar kijken, twee vragen
+
+- Heb je voor mij de import (zodat ik kan repliceren)
+- Is dit een goede eerste test als in we hebben de basis gezien, fase 1 technisch aantonen is daarme rond en we pakken dit ditjes en datjes op in fase 2?
+- Ik zla de ditjes en datjes morgen proberen op te pakken
+
+### Reactie 12 — @Makkmetp (2025-05-27)
+
+@rubenvdlinde ik mail je de import.
+
+### Reactie 13 — @rubenvdlinde (2025-05-28)
+
+Top ik heb hem
+
+### Reactie 14 — @rubenvdlinde (2025-07-22)
+
+@rubenvdlinde en @markbacker vandaag de schema's doorlopen en naar de import bug kijken
+
+### Reactie 15 — @rubenvdlinde (2025-09-03)
+
+Vandag nog een check doen of alles is geimporteerd, daar testbaat als admin door van roganisatie te wisselen
+
+### Reactie 16 — @Makkmetp (2025-09-10)
+
+- [ ] Het is op dit moment niet te controleren. In de back-end is er geen optie om de naam van de organisatie te vinden(of anders dit staat niet in het overzicht van de Softwarecatalogus-app). Zoeken op naam heeft geen zin. Via het toevoegen van kolommen aan de tabellen van registers komt alleen maar N/A naar voren. (Register Voorzieningen > Schema > Organisatie)
+- [ ] Er zijn geen gebruikers ingeladen, waardoor je er niet mee kan inloggen
+- [ ] Via het zoekvenster op home kan je wel zoeken op bijvoorbeeld Centric en dan zie je aantal applicaties, maar of dit ze allemaal zijn, is niet te controleren.
+- [ ] Zoeken op UUID (bv. ba234a2f-7da1-4841-93eb-e2f8562b9b83) levert niks op.
+- [ ] Hoe kan ik dit op een goede en eenvoudige manier met elkaar gaan vergelijken?
+
+
+
+### Reactie 17 — @rubenvdlinde (2025-10-29)
+
+Dit is af te testen via de frontend, de import tool is af dus het hangt nu even op @markbacker om een volledige dataset aan te leveren en dan kan deze (via de mooie nieuwe filter functionaliteit) worden afgetest.
+
+### Reactie 18 — @Makkmetp (2025-12-16)
+
+Opnieuw testen met een:
+aanbod-beheerder van een bestaande leverancier
+gebruik-beheerder van een bestaande gemeente
+
+### Reactie 19 — @markbacker (2025-12-17)
+
+
+Correcties in dataset
+- organisatie.csv
+ - dubbele organisatie id's opgelost.
+- koppeling.csv
+ - zijn nu niet geïmporteerd
+ - koppelingType bevat waarden intern en extern (in openRegister schema bevat `Type` nu de verkeerde waardelijst voor transportprotocol
+ - Toegevoegd kolom transportprotocol (met waarden webservices, bestandsuitwisseling, etc)
+- module en moduleversie.csv
+ - fout weergegeven diacrieten vervangen
+
+Benodigde wijzigingen en correcties nav onderstaande issues
+Oplossen vóór import
+- #312
+ - koppelingen importeren zonder dat UUID worden getoond
+- #282
+ - koppeling
+ - type vervangen door koppelingType
+ - geregistreerd_door toegevoegd
+ - module
+ - type vervangen door moduleType
+ - geregistreerd_door toegevoegd
+ - organisatie
+ - organisatieType
+ - Diensten worden niet aangeleverd, maar ook hier moet type naar dienstType
+ - Dit gegeven is eenvoudig af te leiden uit @self.organisation => ophalen organisatieType als geregistreerd_door
+- #315
+ - publiceren uit te schakelen door @self.published => @self.created
+
+
+### Reactie 20 — @markbacker (2025-12-17)
+
+Nieuwe dataset gegenereerd met bovenstaande wijzigingen.
+
+Zie https://github.com/VNG-Realisatie/Softwarecatalogus-datamigratie/tree/main/data
+
+### Reactie 21 — @markbacker (2025-12-18)
+
+Aangezien Ruben gisteren aangaf dat de impact van het uitschakelen van publiceren minimaal is, heb ik vanmorgen alle CSV’s opnieuw geëxporteerd zonder de kolom `@self.published`.
+
+
+### Reactie 22 — @rubenvdlinde (2026-01-06)
+
+@markbacker gaat even kijken anar de doubleurs
+
+### Reactie 23 — @markbacker (2026-01-07)
+
+[moduleversie.csv](https://github.com/VNG-Realisatie/Softwarecatalogus-datamigratie/blob/main/data/moduleversie.csv) bevat nu weer alleen unieke ID’s.
+
+Dubbelingen ontstonden doordat vanuit gebruik (door gemeenten of samenwerkingen) toegevoegde versies niet werden weggefilterd wanneer de leverancier al een versie had opgevoerd. Versies vanuit gebruik blijven alleen behouden als er geen leverancier-moduleversie bestaat.
+
+### Reactie 24 — @markbacker (2026-01-07)
+
+Nog te doen: de verwijzingen naar niet meer bestaande GEMMA referentiecomponenten en standaarden wijzigen of verwijderen
+
+### Reactie 25 — @markbacker (2026-02-04)
+
+Om deze te accepteren voor Aanbod moet ik nog zien dat de koppelingen goed geïmporteerd worden.
+
+Geïmporteerde koppelingen krijgen de default naam zoals benoemd in #312
+
+### Reactie 26 — @rubenvdlinde (2026-02-12)
+
+Dubleerd dan met #312 die is opgepakt en naar review, maar dat staat los van dit issue (#312 is immers een los issue) dit issue kan daarmee wat mij betreft ter acceptatie (gezien bevinden hierboven is dit issue volgensmij geacepteerd)
+
+### Reactie 27 — @rubenvdlinde (2026-02-23)
+
+Er zijn een aantal aanpassingen nodig op de aangeleverde import voordat deze goed getest kan worden
+
+- https://github.com/VNG-Realisatie/Softwarecatalogus/issues/401
+-
+
+Daarnaast is er nog een probleem met de gebruiks objecten in de import
+
+Elk gebruik object dient altijd een **afnemer** te hebben. In de dataset ontbreekt bij **545 objecten (2,2%)** de afnemer volledig. Al deze objecten zijn gekoppeld aan een koppeling (geen enkel geval betreft een losse module, dienst of product). Het opvallende is overigens dat een flink deel van dit gebruik de status in productie heeft.
+
+Bovenstaande heeft een verstorend effect op tabbelen, overizchtne en facets. Met andere woorden het werkt technisch (en dat is ook te zien) maar aantallen kloppen zo nu en dan niet. Dus je kan het lastig terug rekenen.
+
+
+## Volledig overzicht gebruik met missende afnemer
+
+| ID | Gekoppeld aan (koppeling UUID) | Status |
+|---|---|---|
+| `471c4e76-7c25-5106-ad87-65642332ef5a` | `f9bd3097-1105-5e8c-93c9-c02c2461daa6` | In productie |
+| `a973a608-a9a2-5a58-9ace-db1b47f7f053` | `e9a9da00-58fa-5942-b5f6-a6c6fbdc72ae` | In productie |
+| `44e2fb19-10c8-59cf-829e-7ba3f953b9ec` | `47cb7664-4ace-5460-92e7-dfc11e6cdf88` | In productie |
+| `e4f380a7-0c88-5647-9865-9e26f3a34347` | `742b28ef-340c-5b2a-b6e1-9c72a6a332f7` | In productie |
+| `115f077a-43de-5e33-926e-60dc08b604b1` | `b51ab475-52a5-5607-adc4-1a39faac04cf` | In productie |
+| `36c7b83a-59d3-5d80-a88c-3218cafde8f9` | `b8601747-616e-5add-82fd-b7b205d5d509` | In productie |
+| `e166c713-ae8e-5d17-88f1-27cefec36381` | `c5bbaa02-56f9-5cf1-9628-563879d919d4` | In productie |
+| `93156e4e-2dde-5719-b9cf-2a7f52b63d2b` | `12569c60-4ed3-5704-9a9c-01caa5b1a960` | In productie |
+| `f94724e8-ed09-5329-986a-776e2416139b` | `ec9ac583-8878-5414-af62-b61abce82088` | In productie |
+| `fb5e6ed3-952d-54f2-b567-3c7a06ad088a` | `807a1378-7f95-547b-b423-917f6cc5a8e5` | In productie |
+| `a357fa2e-00cd-5df6-80af-85b709e043b2` | `b24d29c4-6719-58dc-9fe6-a59533c0c3ca` | In productie |
+| `8cf6c79d-3ec2-5863-9134-71f875f049ba` | `b82c6459-54fe-5c8b-b469-39ef9c81438d` | In productie |
+| `a21acced-efed-549e-be99-b17320565859` | `50aa33d1-8bfe-5e23-a92a-09fc07124619` | In productie |
+| `e29efcbc-a043-52c1-9814-ba18bdccb207` | `4adaaec8-6aef-50c7-a0d3-bdab9b520c59` | In productie |
+| `d1047788-75a0-52a6-b00c-5b87757eaabd` | `ab066857-4ac6-5c09-82f9-a95e3d4a51d6` | In productie |
+| `0bf4188e-b901-5fd6-839f-e97aed2f8a4a` | `a273d8f6-3d72-5ff8-bf83-d205edf6ec2d` | In productie |
+| `e9486d39-0f3a-5f87-9835-e0065b87d909` | `baf1ce65-3a45-5b88-a6e4-ccd80435a724` | In productie |
+| `d73fb662-96a9-5b63-a3fc-e6bfd12c02f5` | `22b984f1-1b88-5547-bcdf-c1459e9c514e` | In productie |
+| `ad4a5310-c177-58cf-8653-792be916690b` | `edce33cc-f035-5fc8-9da9-2bafffc78ded` | In productie |
+| `c92d561a-125e-5f2b-a7ee-559453038c62` | `2ac7d642-6f0a-5841-aa12-7bca3183da13` | In productie |
+| `6f6bcf1e-e3cb-5c16-97a5-a7c4c4cf1908` | `266ec051-2d21-55ed-88a3-6603c3cb582f` | In productie |
+| `51003bb9-9ca6-5650-ac15-02fe3c4a2c9e` | `d8401825-a7f0-5fad-a890-f480764f6dad` | In productie |
+| `df00a094-47ac-5c30-8efa-2b0450e7c8f5` | `2c58b68b-0cef-507c-8183-56c340f9b37b` | In productie |
+| `1446f528-b168-5ee7-b404-bc2f0abd9a34` | `0822c6bd-1fd5-53f6-9dc8-45a0c6e4629b` | In productie |
+| `efd39fba-5b98-517d-a710-99952c771e13` | `2163cea7-d6b0-5c1e-90bb-8868ce62e798` | In productie |
+| `ffa10d73-239a-53a0-9f90-d548e0f0c887` | `a91b0c92-c8c3-59f2-9f0e-7720b3df6481` | In productie |
+| `035b2f8a-471b-5d87-8f2d-52ad0a2a47ca` | `1c06928e-19b4-522a-9f0a-86b81815a2e8` | In productie |
+| `29346fdf-2a2b-56df-a459-6112e70e1adc` | `581ecd15-b204-5ae1-baad-64c79b8a3daa` | In productie |
+| `ce6d2773-d7a1-598d-9630-e32d776b8f45` | `14253447-1b17-554b-9546-25ac10625c1b` | In productie |
+| `26855c90-3d75-5690-9e34-83681fa2ba69` | `60b13872-15a2-582c-a88d-b36e5766c85e` | In productie |
+| `4a6f971d-eeb6-533d-bb0f-76961ab58e57` | `6a940a0d-0610-5714-bdbc-650097062aad` | In productie |
+| `5c09b7aa-a79c-5465-ab6e-b0ad966b888f` | `6a940a0d-0610-5714-bdbc-650097062aad` | In productie |
+| `38245c74-5718-5642-a83d-e26a0994ee1b` | `414a90d0-47c0-512f-a283-6f8001f9a540` | In productie |
+| `c40ad9e3-d246-5c5a-baf1-fed2de0e57b8` | `d24bb76f-06f1-5d7d-b309-2747f0520f4f` | In productie |
+| `e88756c3-6ac9-5e13-8c4a-2fac9af64597` | `d24bb76f-06f1-5d7d-b309-2747f0520f4f` | In productie |
+| `7795da99-8baa-5104-9554-820c6ce29c17` | `c5cbfe12-945e-5af9-8c69-10d7eea36a79` | In productie |
+| `bfb8e6fb-6ced-57d6-b01b-6ad32c171a20` | `29352832-1c70-5dcd-a96f-26261bcfb8f0` | In productie |
+| `8c554ae2-a39b-51a6-be9d-e12229bde946` | `c1c529c8-ce59-5cd9-a311-db8f86e847a6` | In productie |
+| `adada3ac-372c-5d37-956e-e32c24b1cf20` | `b6dcb47d-3a31-5119-9468-a5786e0213b9` | In productie |
+| `5b4cb2da-2b76-5d8e-8e8e-e0544c46670d` | `22b984f1-1b88-5547-bcdf-c1459e9c514e` | In productie |
+| `4b546e71-91b2-5f6c-9788-86e73e2d7905` | `a0d26417-02a9-54c4-9771-a15ccae0e455` | In productie |
+| `d89ca0c5-a91e-5801-835b-7d0f5ed60773` | `38276719-f65b-523c-94ca-f19cf1ea03e3` | In productie |
+| `bb2c052b-c71f-55c6-bef2-2ce2699e5fba` | `909086d5-afd3-52ca-8769-599e4569923f` | In productie |
+| `040bb867-9868-51f2-8a65-81a6cfbace78` | `5789e11a-6a48-5487-8c2a-27db9d2e8868` | In productie |
+| `ac9a1c91-81b1-58f3-81f1-927b1e5c0611` | `76bf75a5-278b-50db-b7bc-f2b87f9c3b4c` | In productie |
+| `a1ee8585-1934-51f3-8fa6-ca792f8c9197` | `555d73be-03b5-54ce-9fa7-78a24b5e13d9` | In productie |
+| `ec27c54e-cf2d-570c-ba0a-007409a0ed29` | `94ce1a86-ba60-533d-8ce8-66917ba2edc0` | In productie |
+| `0ae73776-3d02-5077-9674-2f7b53d3357f` | `e2fe863d-311d-58a7-87ac-b6943e2ef67f` | In productie |
+| `7d6cc7ea-23dc-58cd-9ee1-010a2bad7577` | `e76b3f23-2477-59e6-9a60-afb429f3ad37` | In productie |
+| `0f760216-8ef5-5abf-8d3a-a4de348e7716` | `42a5c6e6-ea0f-5fb5-96b7-dbaf956fc0ae` | In productie |
+| `c2e7cdbd-1607-5ae4-8ff6-ec4a89486073` | `0822c6bd-1fd5-53f6-9dc8-45a0c6e4629b` | In productie |
+| `10f428e2-d55c-5054-aa6d-40a2f62e83be` | `feb09147-75d3-53a5-81b3-9a16407145e7` | In productie |
+| `eefd1794-f3f9-530c-b9bd-1f9cc6b1dfb7` | `0a05688c-6b75-5b6a-9052-8d18f54f5003` | In productie |
+| `62ae4460-99db-5e11-afaf-aaeb313d34be` | `dffd4135-ffea-550a-bf65-0cb43321b53b` | In productie |
+| `ecd96df3-388a-5fdf-b93a-ec8344a507de` | `b7bed49b-6e66-50c6-b0e1-bb61eacaa801` | In productie |
+| `7c96dbb0-5d21-5d70-83f3-ae92b84dd326` | `bc405058-1ef1-5424-b330-b3f08d64e076` | In productie |
+| `ab7748ca-ab63-53b6-a0ca-d2faa13f3e4b` | `7b8636de-070a-58e0-8468-5d69d092fd5a` | In productie |
+| `c3ee0ea9-6d77-52c8-b1d3-25366d2fbb70` | `20335b9b-7111-5fc8-8d74-d501325d3f44` | In productie |
+| `90d24ae9-3fb9-5b5b-b283-ab6842583733` | `13dacd7a-f981-55aa-bc4b-27fc86a5dbca` | In productie |
+| `dff32141-b89a-5da0-944c-e195a9c9406a` | `587c6f5d-9a6a-5375-9a62-7158b570d76c` | In productie |
+| `a2576b35-2cde-5106-9b0c-76998d010fbf` | `0a05688c-6b75-5b6a-9052-8d18f54f5003` | In productie |
+| `7337f0c4-00ed-5faa-aa40-6eee0e56a66d` | `4af551b5-902d-587c-a38c-a6aa86fba849` | In productie |
+| `410ff184-1d19-5734-a3aa-ef7bdd1fdfb8` | `5cbd8222-2eb6-5cce-989a-859551a32837` | In productie |
+| `3f93851b-1a1d-5755-a533-d0609f4d02d2` | `264c9c6a-362e-520f-81f4-f60a1586a03d` | In productie |
+| `13f5889e-89db-5d4d-9b34-7210f5da36c7` | `9fa900ee-c266-58fd-b1a5-b5b73c223c4f` | In productie |
+| `f8e275e0-5224-56e3-916f-0793e6dc396c` | `4e28d291-f80b-5e67-ada9-3c4c3c80f5ac` | In productie |
+| `85963399-2cda-5dae-a355-b6b77f23b466` | `2c7be7c5-65eb-5c56-aac9-86e779ff112c` | In productie |
+| `051baf8f-380b-5756-a80b-f84f655cd519` | `8320a538-cd45-5fda-a239-ba25d49f145d` | In productie |
+| `68565364-a38e-5057-8326-f719bc5c87bc` | `9ae938cb-4d88-511a-98b9-acbc431697a6` | In productie |
+| `f86a0f79-8e6b-5eb0-9a32-ee63f88de145` | `caaeab76-4fb4-57b9-8ca9-31dec55eca69` | In productie |
+| `303a7862-dc1f-5e37-aaba-a100cab668bf` | `0822c6bd-1fd5-53f6-9dc8-45a0c6e4629b` | In productie |
+| `94276a26-5fbd-5e08-b80e-7e918fd35234` | `feb09147-75d3-53a5-81b3-9a16407145e7` | In productie |
+| `9d959e25-524e-5bea-8e5d-64c2a1e94c2a` | `4adaaec8-6aef-50c7-a0d3-bdab9b520c59` | In productie |
+| `761433e0-4aef-5f70-b792-cd71585f8e9f` | `0a05688c-6b75-5b6a-9052-8d18f54f5003` | In productie |
+| `0047ffe7-5dbb-5915-8994-04f59ad6a809` | `dc476f70-53d7-5a06-9548-17a58be1de22` | In productie |
+| `e80297c5-d757-50e3-ae8a-adf6c5ef0b90` | `624ddbb6-438b-5495-9eb5-19b9d186a8b2` | In productie |
+| `05cb35d7-75b4-51c2-8586-165780effa3c` | `75d938cf-e4f3-5a55-8877-43586cff905f` | In productie |
+| `61bd5449-fda5-5516-9917-da12db6d91f6` | `9281e11d-8a2a-5376-870e-faa131cb162d` | In productie |
+| `55f4c90d-6591-51c3-bf43-5c9137b98f13` | `24bba2dd-5544-508e-99b4-c13377800746` | In productie |
+| `47699688-dda1-581a-985f-7b4721c6ea0e` | `03d127e4-f68f-54f3-afa0-95dd48ee0a5f` | In productie |
+| `cc31089a-5db7-5d19-b011-e9190d19e381` | `60b13872-15a2-582c-a88d-b36e5766c85e` | In productie |
+| `2a751346-c3d9-52e3-bfcb-0a73a9502335` | `ed584494-9d79-547b-9412-68fa743b8f77` | In productie |
+| `4bb55ec2-854d-57ed-bdba-867a0609dca3` | `b6547611-f2c7-5cd8-92cb-f56c114837a4` | In productie |
+| `688d91bc-7177-5e21-8693-a7e3f60e91e9` | `392b6033-7237-52ce-8a40-3640fa188396` | In productie |
+| `158faa82-f78c-5cf2-b8f7-f2db421e795d` | `6c34f0dc-db35-5dce-96c8-af9ba81ca73e` | In productie |
+| `be57a2f4-c776-52f9-a61c-5fb79a71b96a` | `216236af-4389-586b-a0a1-c9abeae0f386` | In productie |
+| `6107dce4-7a04-5a3d-b53e-8b4dba9db712` | `ab0ce0a5-4bc8-583e-a50a-faca6a3a436d` | In productie |
+| `07bc6730-c66b-5d55-b8d6-b0328952ed30` | `50033165-da88-5747-89a3-7cb2ec289d36` | In productie |
+| `3f3805a1-0156-51dd-b98d-0ff988c5995b` | `e555d684-9e95-5fc3-9a11-db3bf79930c4` | In productie |
+| `745f2e92-a1c6-58e6-aea0-36015cb7840c` | `7b54569a-f0f6-5f8a-ae0d-ed07261cdbf3` | In productie |
+| `a0aa1a0f-baf4-5a8f-bd8e-94a3e4419d55` | `3ee435e7-5759-5ebc-8397-e82207d9a504` | In productie |
+| `5be98098-9d2a-5969-a138-e99558198b4e` | `6914c492-57c5-512b-b008-be5dfb7f51f3` | In productie |
+| `fce12413-7ceb-5dfa-8405-b174b699e84a` | `5cb136b3-a461-5338-93e7-79e6d71ade86` | In productie |
+| `95e86285-bdb4-5838-9f9a-a6330335d3c4` | `ab8af2ec-4599-5561-a830-e630e255ce4a` | In productie |
+| `03f19b5e-7559-5b9a-bbc6-947cf9f3c30d` | `3e9c996b-afd6-53bd-b862-52b0f585f813` | In productie |
+| `54243097-6fe8-5b86-baf7-607d65d9a42a` | `baeb73d4-32e0-5190-a0bf-927315f8df93` | In productie |
+| `de216e7c-f8e3-5f7e-85fa-a2e9219c7c0c` | `366f8b87-3143-5f0b-ba48-be9f4fa68028` | In productie |
+| `cf4d74cf-c389-55a3-b403-d26f0d0f5570` | `94f72e1c-7e16-5c9e-976d-7b3d443549df` | In productie |
+| `5a6aba4c-3470-507f-8f9a-f6c905c4844c` | `7f36a874-946b-55f7-a554-1a552e693dd2` | In productie |
+| `bfe81cba-fbb8-51bb-9e61-569d24225757` | `d3fd1d77-8254-57db-8b3a-f95de31317d7` | In productie |
+| `7e273f24-a97b-5912-8f35-2160659330bf` | `7270f5ad-30bf-5023-ae84-ca7fc67834a9` | In productie |
+| `b6b621a4-8c36-5a97-9065-c39fec1b13b1` | `81385ac0-f3f1-5e41-b54a-ec6b915aa85c` | In productie |
+| `7c5e8a6b-8d47-5bbc-a9c1-fa4c52118631` | `803ff0b5-9dad-53c3-bfbf-15cf053a1af4` | In productie |
+| `0b300dff-fed5-5ce1-a091-0d10497e55c8` | `f9bd3097-1105-5e8c-93c9-c02c2461daa6` | In productie |
+| `1be52c37-f52f-5133-82d3-97c44165149d` | `e9a9da00-58fa-5942-b5f6-a6c6fbdc72ae` | In productie |
+| `cff5a8d4-dcb8-53a2-b047-c0cc34e46216` | `47cb7664-4ace-5460-92e7-dfc11e6cdf88` | In productie |
+| `3a0c7c78-756b-54e7-a647-fa656cc87353` | `e5e5b5b5-39ca-57ae-9688-130fd373683b` | In productie |
+| `7557bc1e-f91f-5773-8e6a-bbb2cd336199` | `62bf8c4e-9aff-57ab-bf8f-95bd689e67fd` | In productie |
+| `9b645af0-fdc7-5b31-aeef-559f7a7f940d` | `816d81db-530c-577c-9f82-a7b46c268f13` | In productie |
+| `c5974ac3-2611-5323-bae5-3f24ec084091` | `266c4f97-5d00-59f6-b6a3-2481efdd4e6c` | In productie |
+| `ac3e3674-c074-5656-82da-8a418b4d331f` | `b0487fc5-f3df-547f-a6d0-3c46ccf4e7d5` | In productie |
+| `cb3bf9e8-381a-5b1d-aa63-c20fe1960a6b` | `d344b6e7-3c6b-55c0-984e-6ea4150aac4c` | In productie |
+| `8f7ee5e0-a214-500f-a0d2-0dd1a3a64860` | `be2a6ab2-166a-5c48-9a9f-a09316faf3a1` | In productie |
+| `e120d54d-3110-5f59-b828-928cdce0afde` | `f86bc58e-1014-5c7e-b5bd-f06f85e7d10d` | In productie |
+| `b4e48934-1662-5bc9-96c3-f92b43566133` | `95279e1b-cadc-5a6d-9b59-fa456d350ceb` | In productie |
+| `bcf724f5-9018-5d7b-8ba5-dca42cf7b76c` | `fcfc35f4-9c43-50ed-9e4b-191f61de6c96` | In productie |
+| `64f983b1-930d-543b-8436-b655fe6b8aad` | `304a2d74-769f-5ef0-a4df-4252d17d18e9` | In productie |
+| `545bda5e-33ef-58a3-80d5-07ae84f4f4aa` | `828c2181-6b03-5d91-954d-8ed3a1abe417` | In productie |
+| `b25b7a21-b4f6-54a5-abe9-ba174478d397` | `48d2bdcc-d039-51da-a33b-38b354916506` | In productie |
+| `8434947a-d0f4-5eec-ab18-b64cff4e0513` | `ca79c581-5dca-5672-92ed-f066a9e30d06` | In productie |
+| `d89516da-7d6d-5fd3-ba54-4d6d8dbbbcb3` | `6a58d2d2-b44f-5b18-9f80-9ddf18d4598a` | In productie |
+| `e1dba386-fa9d-553c-a777-4a1b8861f211` | `6a58d2d2-b44f-5b18-9f80-9ddf18d4598a` | In productie |
+| `2dcfc990-1669-5770-a74a-34df0abadd2b` | `ca79c581-5dca-5672-92ed-f066a9e30d06` | In productie |
+| `e065226c-25f5-58e4-b0da-7b347d7c19a5` | `6a58d2d2-b44f-5b18-9f80-9ddf18d4598a` | In productie |
+| `9abc418e-748a-5ce7-bdb2-d7f9febead92` | `6a58d2d2-b44f-5b18-9f80-9ddf18d4598a` | In productie |
+| `10ddf965-c23e-5cf8-a09f-e67330348034` | `933f4182-3a0f-5350-89d8-ded5ea105cf4` | In productie |
+| `9b369a32-a684-594b-afe4-db33ca7cf16a` | `933f4182-3a0f-5350-89d8-ded5ea105cf4` | In productie |
+| `74aecf2d-3978-50f7-b74d-dfc652bef6af` | `f4037e78-3b5a-5df8-8a59-0aeff76dfa4e` | In productie |
+| `c93ec712-e1b1-5aba-b9ae-1f42d69d9960` | `0cf4b2b9-42bc-52fc-a899-750a07bd364a` | In productie |
+| `e4b70f9c-39f9-573c-be08-dc7d49eafbe3` | `6e888f5f-ce73-5afe-a17c-1b800ca78342` | In productie |
+| `2e47895b-589a-5438-9066-0e52bc04690b` | `5fa1fd21-dd77-50b2-84e6-3f19dedd6d22` | In productie |
+| `7de386b0-2c9e-5d3f-995d-348b946b68a7` | `b20e3323-0228-5299-a707-6de751a5af5f` | In productie |
+| `3acacb96-8739-5529-8acf-72dbc1704943` | `192e70ce-cf96-5203-ad98-914a10b13691` | In productie |
+| `8b4a730f-63a6-53af-a83b-dc4f14bf3823` | `1ff0bd14-d27b-52a5-b02e-2486ce1f7de5` | In productie |
+| `70dd9161-29af-5e06-9676-e689a69033f5` | `6914c492-57c5-512b-b008-be5dfb7f51f3` | In productie |
+| `159d1e85-91ed-5c03-b1c4-cdf5787574da` | `95b0a3a8-db1b-515c-810d-77b734f1cb02` | In productie |
+| `7041ab09-de7e-5ae5-b6ba-1fe4dbfe890d` | `88fa25f3-6048-5a01-8fc2-36e538bf4efb` | In productie |
+| `44d0fd93-24f0-5caf-988c-177a770792e6` | `74bec1b7-ffb0-579f-a6db-78f4505672b3` | In productie |
+| `0d8efb4f-1579-5f16-ab8a-f6aa65104c5b` | `52bbdc0f-424b-5de6-b7b4-1e3551574f44` | In productie |
+| `3f62f9f1-59d5-5b66-be89-72df0e96a7cf` | `244025e2-7975-5a5f-9ef0-9fd0c1bb3c03` | In productie |
+| `92fabcb8-6fb6-5faf-a9cd-1999b10962eb` | `e91f0ff6-ff4a-5988-9076-9f2768deec8c` | In productie |
+| `6d798658-3c0b-51b4-a527-053f0450b0e9` | `8c6cde82-aa8f-5860-8e30-90e115f453f2` | In productie |
+| `f765fd82-e24c-5560-b63a-37295390b0dc` | `062695f2-ea61-51a2-94f7-ed5ded74d150` | In productie |
+| `8f17db59-cd56-5b56-9d7f-d034f0ecb386` | `fb075d7e-7fee-5614-94a9-9f02b8f2a690` | In productie |
+| `2326ed2d-cd79-54c5-a646-390f0c24e599` | `0f12636c-7348-5182-8971-2b0fbc5099a7` | In productie |
+| `0e5ef32a-3b67-5c85-81df-a7c9e687dd2f` | `e7e8584c-b1bf-571d-8c4a-c4af7fa25512` | In productie |
+| `191b3481-d285-5eb8-9cd0-91945eefa461` | `1543c332-7c3c-5d9d-bbdb-cb07a70222bb` | In productie |
+| `aac8d5a9-90a3-5664-83ad-6a8073c8588b` | `1307f037-a3f1-5be7-b4e6-b23eaa71080c` | In productie |
+| `e6a41c3d-7228-5dcb-9779-7ccf654346a6` | `5e38d537-693b-5a39-af6d-d492b0a7bfbe` | In productie |
+| `3865e981-e76c-51f6-9d05-a4c109e4232f` | `1ff0bd14-d27b-52a5-b02e-2486ce1f7de5` | In productie |
+| `3d162e17-287d-562a-827d-2c84c8378e77` | `6914c492-57c5-512b-b008-be5dfb7f51f3` | In productie |
+| `130ab35e-909d-5aab-84ab-3d663e509567` | `95b0a3a8-db1b-515c-810d-77b734f1cb02` | In productie |
+| `bd747eb4-ef45-5d91-b685-d52a00ed74f5` | `88fa25f3-6048-5a01-8fc2-36e538bf4efb` | In productie |
+| `1caad2b6-8228-5e50-bb55-73d2fac639ef` | `74bec1b7-ffb0-579f-a6db-78f4505672b3` | In productie |
+| `b0403be7-4e0c-51b8-b93b-8201b3299ae0` | `52bbdc0f-424b-5de6-b7b4-1e3551574f44` | In productie |
+| `2dee492f-d24b-50a5-bc4e-cb9c92d30567` | `244025e2-7975-5a5f-9ef0-9fd0c1bb3c03` | In productie |
+| `a1b2bce0-f6e4-504d-8f45-e17f74113d94` | `e91f0ff6-ff4a-5988-9076-9f2768deec8c` | In productie |
+| `eb41a1a9-f9ef-5d63-b406-e19ef4f64606` | `8c6cde82-aa8f-5860-8e30-90e115f453f2` | In productie |
+| `f18c7838-0b3d-5c04-8363-299235dcf0b5` | `062695f2-ea61-51a2-94f7-ed5ded74d150` | In productie |
+| `4a33719e-ed55-5bda-ba08-87e6bd7ff981` | `fb075d7e-7fee-5614-94a9-9f02b8f2a690` | In productie |
+| `341c2f53-8330-5ea5-90bf-494797f54a64` | `1ff0bd14-d27b-52a5-b02e-2486ce1f7de5` | In productie |
+| `ebca0020-01f0-5d56-8934-8dfef27b7554` | `6914c492-57c5-512b-b008-be5dfb7f51f3` | In productie |
+| `df8ad8d2-a37e-5779-92cc-1716380e8ac7` | `95b0a3a8-db1b-515c-810d-77b734f1cb02` | In productie |
+| `6c34cc3b-9c13-5a5b-893b-9d8d5949e80a` | `88fa25f3-6048-5a01-8fc2-36e538bf4efb` | In productie |
+| `843cf28d-42c8-5b88-9499-361a3f5f5917` | `74bec1b7-ffb0-579f-a6db-78f4505672b3` | In productie |
+| `a7b013ba-3b7f-5cd4-ac16-927ae50d9b91` | `52bbdc0f-424b-5de6-b7b4-1e3551574f44` | In productie |
+| `cf0e14df-8df4-5493-9c32-3abbc037abdd` | `244025e2-7975-5a5f-9ef0-9fd0c1bb3c03` | In productie |
+| `5f0d4f39-c3d1-50a1-b66d-73665e7b35dc` | `e91f0ff6-ff4a-5988-9076-9f2768deec8c` | In productie |
+| `8dce61ac-5b5f-527c-b1e0-d5394116b7e0` | `8c6cde82-aa8f-5860-8e30-90e115f453f2` | In productie |
+| `4a5d029a-7907-5130-8877-8b4c455073a7` | `062695f2-ea61-51a2-94f7-ed5ded74d150` | In productie |
+| `eac13c4e-be10-50f4-8ee0-312ee0fa755f` | `fb075d7e-7fee-5614-94a9-9f02b8f2a690` | In productie |
+| `e47e49ba-050c-59a7-bb26-0e1db00cae23` | `217291b0-cb9c-538f-ab06-59662125e225` | In productie |
+| `77392506-1264-5615-928f-c662df9317d4` | `afd617ff-82ac-5fde-8f02-7b2520dab4b0` | In productie |
+| `201ea2eb-1c2a-5679-a2f7-7a79faf722d1` | `a514b766-a0b1-572e-ac5f-377d54a66f9b` | In productie |
+| `9324f199-f1ad-54e2-ad90-b48e00cb7c0f` | `66211221-38ff-5ebd-8036-bfd86547a03d` | In productie |
+| `28c92800-0cfe-5284-9374-ecabcd92b9ee` | `3175435b-2a44-52d9-9ff3-253e54470e77` | In productie |
+| `3869307c-b3ad-55f7-8aa8-773786ef837d` | `adda1d71-6e83-5709-a55e-da65f8479a9f` | In productie |
+| `5250e6ab-90df-5e4b-bb2f-1e8ccec14d63` | `08c4ea0a-bd86-5032-911f-8f472dca497b` | In productie |
+| `faa2c12b-54d0-5238-8edd-0b5503dc6cac` | `3d3726f2-e339-51e5-b344-35c2063b3376` | In productie |
+| `27221c17-61e5-5724-9cbb-b775098c4a6e` | `c1f1e1e6-ff24-525c-b5c0-5fb424e4fb5e` | In productie |
+| `1b6ee15d-917a-5ee2-b375-e9c9b291d193` | `3ee435e7-5759-5ebc-8397-e82207d9a504` | In productie |
+| `f30426be-de87-58ed-a0f1-955ad7590408` | `6914c492-57c5-512b-b008-be5dfb7f51f3` | In productie |
+| `bce5d946-dfd2-5221-a9c4-c9d64b137845` | `3e9c996b-afd6-53bd-b862-52b0f585f813` | In productie |
+| `fb8ab46b-581c-53e4-800b-9a498837d939` | `baeb73d4-32e0-5190-a0bf-927315f8df93` | In productie |
+| `87640690-8a43-58d8-9c03-cb588d16c0c6` | `da7fa446-89bb-5978-97ca-434270a70035` | In productie |
+| `fa304cee-7dbc-503a-b087-3df12292f9d9` | `7b54569a-f0f6-5f8a-ae0d-ed07261cdbf3` | In productie |
+| `4dd2e686-b465-505f-87d0-c5c193b1c730` | `94f72e1c-7e16-5c9e-976d-7b3d443549df` | In productie |
+| `0f612bab-f3c9-5bf3-9775-82eeb00e9b38` | `ab8af2ec-4599-5561-a830-e630e255ce4a` | In productie |
+| `041003f5-aac0-5a59-a4aa-eac456d7ccae` | `366f8b87-3143-5f0b-ba48-be9f4fa68028` | In productie |
+| `ec699212-498b-5ac8-8ce8-d48fab54328e` | `e555d684-9e95-5fc3-9a11-db3bf79930c4` | In productie |
+| `46a795b7-efe2-5e71-b4e6-a939bc6ee744` | `5cb136b3-a461-5338-93e7-79e6d71ade86` | In productie |
+| `dc0c5db3-a29e-5e72-a54b-69addb2ff20f` | `3adc7ec0-249d-59a1-bfa5-15ae5ae69030` | In productie |
+| `ee49add6-fce2-5800-a75b-e963dcc2b4e4` | `8da0fd34-13e9-5e21-b81e-3ec96956a365` | In productie |
+| `150b5995-e055-5e6f-a3da-6acf811da87c` | `03d127e4-f68f-54f3-afa0-95dd48ee0a5f` | In productie |
+| `b1311d98-8a26-5f92-86aa-0434705bd770` | `834210ff-077f-51c1-b152-6f82075ec9b1` | In productie |
+| `69421e4b-8cad-51d9-bc51-ccc7f23f100c` | `2ce615aa-78c9-52f7-99bf-273d59223f05` | In productie |
+| `a72c3f25-a613-5216-8888-f22072dee024` | `ba1b496b-227e-5fe7-8a6e-15a809a42161` | In productie |
+| `b718675f-6755-579a-9e94-c6ab5950d558` | `ce830b18-62a2-5ec3-817d-107491180bb4` | In productie |
+| `6ec9eeaf-bae5-512f-9618-5bfe915fca19` | `f0da48a9-5846-5ab6-8901-80728bcf9d97` | In productie |
+| `5aadeb0a-27d4-5700-8fce-14bd195c5979` | `2439a09e-bcd7-579d-b597-08b509b20606` | In productie |
+| `1d7d6b67-e328-5a5c-be65-fbf07907c55d` | `414a90d0-47c0-512f-a283-6f8001f9a540` | In productie |
+| `2d0e67a4-79b4-5686-816c-e8ee3026b668` | `d24bb76f-06f1-5d7d-b309-2747f0520f4f` | In productie |
+| `c15125e5-b2a3-587f-bd45-111cca6c14da` | `f123596b-19b7-50cf-a6b7-5818efb2d016` | In productie |
+| `cc8fc771-eb51-58d3-bbac-94d59f697300` | `212fc901-e39b-55b8-aaf0-a3af54cd57b3` | In productie |
+| `c071f81e-5fb5-5804-bcbc-b2748bb19471` | `e7c4e874-de45-59db-92a4-d551f396cdc5` | In productie |
+| `fddcb4e9-9195-5d18-9b5e-a6223dc80e47` | `74e4b65e-05c9-54d6-8af9-cd17e55331c1` | In productie |
+| `c22fc525-c415-58b2-9240-c5f5eaa4bdb8` | `74e4b65e-05c9-54d6-8af9-cd17e55331c1` | In productie |
+| `f1ef4dca-8ecc-5207-803d-3a9b82221f6e` | `74e4b65e-05c9-54d6-8af9-cd17e55331c1` | In productie |
+| `c46c7c82-b7d7-516c-bc6f-4d4da00f0ac2` | `88930f4b-7097-5854-8974-7f7e8df9ea46` | In productie |
+| `2e3a39ea-992a-5f54-aff9-ec34a3ac286e` | `807a1378-7f95-547b-b423-917f6cc5a8e5` | In productie |
+| `cd6dd53b-4659-5f64-bf4c-901c63f5f888` | `a314cdee-ad34-5d39-87b3-203edcdb90d5` | In productie |
+| `5c65e8b9-2cc2-5ba2-b92e-3885262612be` | `91c402e9-550b-5a1b-8320-789d398a3148` | In productie |
+| `a7e22d3d-60bf-5863-9dda-c5e4cf3b6546` | `f3a1c8d5-fb60-5422-899c-24092b96cafc` | In productie |
+| `6a709653-6f31-521c-96ec-f8185c0f71b5` | `ddcd3b23-f308-5e4e-923e-83de15a1798b` | In productie |
+| `8a4032a0-aafa-578a-887d-536b2d35cf95` | `8949c7cb-20ca-5bb0-85e1-d67c209957a3` | In productie |
+| `0c414777-dce6-5861-bbe8-809586e2ab47` | `5ce77cc0-c6db-5d2b-aece-be6e0043352a` | In productie |
+| `afb32416-a65f-50e5-ae53-174058cab27a` | `15228835-f5f7-5e43-a0b1-472a4b8383b5` | In productie |
+| `8d028a80-ac2e-524a-84c9-f4740ef8975a` | `d8b92f3c-4163-56bd-bc3e-7954e6a3fc91` | In productie |
+| `21825d82-6d1a-571a-83e2-a9a487fab299` | `ae6600f5-1872-596c-9e15-12d8d3998ab8` | In productie |
+| `7707c87a-8a1c-5efe-a013-4adb8e6c9f38` | `bc27f476-29fb-5df5-b486-b1046a606321` | In productie |
+| `c82f4d09-7e5c-57ed-8cb3-767592adf1f5` | `15607db2-4a40-552d-883f-3ecc75da9ec7` | In productie |
+| `8ddb8c62-7ce5-5715-acd4-1fd21ff1ed71` | `d9b91dd0-1536-56b7-b585-3c720d43ac61` | In productie |
+| `673956ae-e58e-5342-a45c-daf7473bd8d1` | `2b1242ee-054b-5ebf-9f22-772fa2935c89` | In productie |
+| `d2282bbe-6ee5-5c62-bb50-adf011f9a2b8` | `12b7bee5-35b9-5467-a335-c5ed1a47719b` | In productie |
+| `7ec68eb2-3e7f-56c2-8444-2e70677ef38b` | `ff8709bc-a462-5332-b684-1abf7fb50266` | In productie |
+| `9c13614a-92bf-5c68-bfea-74c8e7486498` | `80bc5e34-89c5-541f-a96b-ef016e41813c` | In productie |
+| `6d8dead7-3aea-58f9-9e2b-f5b951dd8e5e` | `d1a96f4b-e09d-5029-8774-389c5dc347e7` | In productie |
+| `3b101823-b380-5705-92c5-602bef40d771` | `794dafaa-097e-53d8-bbf9-52731fcfad77` | In productie |
+| `1de41f31-cea9-517f-a96b-7f9ad0705b0f` | `6ef1d65a-f4f5-5561-87b8-62561016ec2f` | In productie |
+| `97268423-057d-57da-9cf3-e2789240b797` | `d344b6e7-3c6b-55c0-984e-6ea4150aac4c` | In productie |
+| `fe5ebcd6-1ac5-58a8-b5a9-b65d025a45e3` | `be2a6ab2-166a-5c48-9a9f-a09316faf3a1` | In productie |
+| `348e7082-6693-54c8-a669-7615508272ae` | `f86bc58e-1014-5c7e-b5bd-f06f85e7d10d` | In productie |
+| `97ab47d7-b4a3-5472-aafa-d18c2c247bbe` | `95279e1b-cadc-5a6d-9b59-fa456d350ceb` | In productie |
+| `baa978d5-4f1b-5c78-8efe-18579d87ddaa` | `fcfc35f4-9c43-50ed-9e4b-191f61de6c96` | In productie |
+| `33833e05-00aa-509e-a8a2-c6cb3364e119` | `f6143937-91f8-5bbb-ba88-a40addcba974` | In productie |
+| `3efe4ed5-e518-5cb9-9c91-10d58663430e` | `3a1392b5-2e7f-5f73-9a8a-bf35e8cf3c09` | In productie |
+| `9cf8f600-f232-57de-862b-c051df8b4f7c` | `7b6a7f2e-2c1f-5750-8c39-9be76feabc5c` | In productie |
+| `daf51874-28f2-53f6-b8db-2fe08733eb1b` | `d9bd674a-1285-5772-8c9d-b95e9d3e829b` | In productie |
+| `890c0131-0785-5faa-93f6-6a53ffcb223d` | `102668ae-b321-5670-a252-dd8f90b6334f` | In productie |
+| `7ecb1085-f08d-5643-99bc-022ecbdf8cce` | `4c6919b9-8b09-50e5-9358-b0136dad0949` | In productie |
+| `d71ef8fa-e6cc-5877-a966-2645d371ff95` | `7d60e4de-8770-5178-a7cf-d6a7c7ed95a1` | In productie |
+| `0deb12ae-3cba-5146-90a0-8cab19862a17` | `160bf91f-01aa-5ead-8688-c62138244178` | In productie |
+| `040e8cc2-91da-54d3-815c-fe1e371b22c2` | `a3dc8104-cf1d-5367-9dd5-1014366f9ca4` | In productie |
+| `0ae95549-f71e-521c-9829-42b04b90c891` | `9d3f0736-0f63-5425-8211-c23bb4119ce2` | In productie |
+| `de3e91fe-4e24-5c6b-b959-f5e3017bb1f5` | `1efb347a-1410-532b-809b-fe21452a6227` | In productie |
+| `3e7d0cb8-1a79-5ebe-b527-6266ffe3fd99` | `4cfa4b46-43a5-5195-a666-4160119990b3` | In productie |
+| `8dd3ec80-7df5-583d-a48c-72d0a4c09d2c` | `41e96ddb-05c4-5651-9ae6-16fa269747e9` | In productie |
+| `3376fca3-e893-5eb8-bf39-647aeea2296e` | `f9782f92-1609-5528-9104-d2977b6a40ae` | In productie |
+| `223755c6-c37f-54ad-84fd-41066e3ca662` | `34c09a06-aca0-501d-8131-718def643431` | In productie |
+| `ada4a3bf-82dc-501d-8e38-b74fb253d59f` | `223b0976-9e8b-51fb-bbb6-6fdfc07bfc1a` | In productie |
+| `31caf691-b300-5524-81f1-b2848bffaedb` | `b29040ce-129f-5b5a-9c86-12246bb9e350` | In productie |
+| `4541bdff-58b4-5b2f-b20a-cc123c11b745` | `1ff0bd14-d27b-52a5-b02e-2486ce1f7de5` | In productie |
+| `be8421c6-af30-5191-aec2-3047a5f0529c` | `6914c492-57c5-512b-b008-be5dfb7f51f3` | In productie |
+| `719572e3-0eeb-5f51-9ec3-4e588742b66d` | `93c88201-2091-5ed7-87c5-5585ae323a80` | In productie |
+| `52ec24e0-c39b-5e1c-afc0-d2a5991e0da6` | `bdd2c409-88f5-5535-a6eb-e21c2621235d` | In productie |
+| `03ce5098-6063-55b6-ada8-f31148c6a244` | `231d3edc-02a1-5551-abc5-b3574bdf292a` | In productie |
+| `4a6c9d37-8409-5a13-bcf0-9e74565fb539` | `0d064caf-ee1a-5fe0-8b52-ea2016e244fe` | In productie |
+| `450f558d-e58d-56d5-895c-7e984bfcc934` | `cccbc1e3-d95a-51d9-b6da-9c69b277a98b` | In productie |
+| `00db371b-a270-5d0e-a54a-5ac99d473a0f` | `1b4b4204-fc37-5875-a23e-af179b1e73e0` | In productie |
+| `dbe56c8e-2aa1-5c5a-83e7-b82f8bcf9672` | `07298fbe-1bea-5ff8-a240-662fd78c3375` | In productie |
+| `74c75cc7-7b6b-5868-861c-5117387b2ecd` | `72c176f7-4262-5486-bef9-0f91d108ffe3` | In productie |
+| `60ab3349-a166-5c27-ab61-62b7dbfd4b9e` | `83eb1063-6429-5b91-b7ad-5a8d40fd8bef` | In productie |
+| `c3c76fb4-7570-598d-be48-9d0c9c433af6` | `8c0f09a4-f67a-526c-a0c7-ae071f378b32` | In productie |
+| `5d0a292b-22d2-5ab7-b4e2-a7651a4d285f` | `4bf17959-ef33-5b85-aecd-21b08123b4a5` | In productie |
+| `d2896561-5017-530b-a2d4-c8bc0f304aa0` | `a1a17c41-f53b-57bf-8ad8-2fbc81657c29` | In productie |
+| `1eb2feee-4ac6-56dd-aa92-7ef62d5d1d7e` | `fa4b73e5-852d-5472-8ce0-2e59f9ddbff5` | In productie |
+| `f7d3beb4-1f5b-5e53-a95e-b9031e2b25c5` | `99ee9f32-6a46-5cb4-b7f5-1c65352fdc68` | In productie |
+| `ebaced07-1db4-565b-81d7-e6f066b18bb7` | `28b56b21-a311-57ea-a89b-a68a74b8a9f2` | In productie |
+| `c6849efd-749a-50f8-8238-497346bc27ff` | `d8537d54-df43-5e92-a77e-dacb31d8c96e` | In productie |
+| `a11fc37c-c2b8-541c-965c-b548a428c9e2` | `66c5f9f3-71f3-5cd3-86fb-f27f77a8985b` | In productie |
+| `af2ccfa8-b67a-5baa-94cb-363ff132bee8` | `94ebae6d-b34f-519f-bd26-0c586e0ba77f` | In productie |
+| `d6039a0a-3e9c-59e9-9f36-213836b0668c` | `d729997c-f898-54ff-b7e1-a388d551fbc3` | In productie |
+| `fd8176df-1081-578e-896d-3c34d451e1d1` | `9ccb0560-6f01-59fd-8d91-ff13f1750581` | In productie |
+| `456a8809-5919-570b-b36d-b597a2b7f7cd` | `d769bd0e-5cf2-5e57-9c00-2ce3f8d2f357` | In productie |
+| `989b3921-dc99-5fb9-bac3-7f1ddd3f07b1` | `69a3a36e-4b35-5b23-b99e-ead4dc569c0f` | In productie |
+| `e2367ab5-b20f-5b9b-b590-2df22a09d861` | `c8188a98-52d0-519b-9442-23369c0bfa31` | In productie |
+| `374a17b3-94ac-5bf9-8ee2-9f3d11f401ce` | `8ec263f9-e6fb-5f0a-8c27-42c6d89c60cd` | In productie |
+| `70f02189-e37e-56c2-b5b4-41df0195e543` | `a779df3b-8d2a-543a-818b-c54fa86a3792` | In productie |
+| `5a6d2d09-b35e-5be5-8800-87539de69ed6` | `44e92e26-7e8d-5129-901d-6238a530e174` | In productie |
+| `e09eb12f-7760-550b-8bc9-93b368eaab26` | `5a4e474b-99e1-5d20-ba94-25d3853c7ff2` | In productie |
+| `6df9f931-19a8-5a96-9ca2-dca114e84458` | `803ff0b5-9dad-53c3-bfbf-15cf053a1af4` | In productie |
+| `a55ba201-0ca4-571f-86ec-5234f6d478f7` | `39908daf-4a7c-5c94-b76d-30e33125c126` | In productie |
+| `7a3d02b0-4e41-5881-b1df-b23aee06dd6e` | `c0e388dc-15b5-5cb8-9058-0f526a9ace88` | In productie |
+| `cc387c93-f71a-53e6-8e73-878ba782e264` | `562da2a1-76e7-5e39-a314-d9cc5f949587` | In productie |
+| `2fd0e630-8609-5592-bb23-becb44e383d9` | `9d14beb2-c78f-5b10-9a32-5743439ec5a6` | In productie |
+| `ed754c35-0c8a-5b68-9cfb-d65fae46ab67` | `b24d29c4-6719-58dc-9fe6-a59533c0c3ca` | In productie |
+| `64ccd25a-b533-5d09-a053-aeca7ca4660f` | `b0487fc5-f3df-547f-a6d0-3c46ccf4e7d5` | In productie |
+| `4c68d15c-4ec4-5fde-b8e0-68b44e40e17f` | `5c6f7990-ff87-5ea4-98b2-3f4cffc8f9e4` | In productie |
+| `9ed09fb8-af07-57e7-bb4d-b78213693ea8` | `c8c1849b-6010-5c75-982f-6684c9f4805c` | In productie |
+| `998e1b2f-cbd9-525d-b9c0-690b4d6248e6` | `ae5bbaf6-8154-5ea6-bc12-98790b1f818e` | In productie |
+| `5e26bf23-e553-552c-bb43-1340905f34bc` | `0aaaaa66-167b-5892-b74a-e18d242a4a88` | In productie |
+| `84c5ce4e-ceb7-55ae-b6d3-547ea0171a9f` | `34df04b3-2b5f-51c8-aad0-5d0aa535c1d9` | In productie |
+| `7b95d93f-bc6e-5680-8a56-4cfac8cc7231` | `8a556974-4788-5dfc-a12d-c735f35d6429` | In productie |
+| `457f69cd-7310-54fa-b994-70b199e9e80c` | `d0450748-11b7-53cb-9033-70f2862c65ab` | In productie |
+| `74566260-d2f1-5095-90e6-f2299583fcad` | `13ff9cd3-323f-535d-bb15-a421e339e53f` | In productie |
+| `b703a861-516f-5dc5-badb-267648334b88` | `5e7367e1-c25b-511d-ba55-ef4c86083691` | In productie |
+| `7cfb6046-d2e4-538a-9404-2321b312d9de` | `49e0b025-8a00-58b8-ac60-20b7a61d5b9f` | In productie |
+| `fc165bb8-7d9b-5c2f-b02d-3ddc647e7fa2` | `ccdcd087-c537-57d6-830f-aa72d199f367` | In productie |
+| `000c19e2-ca78-5b21-a32b-899629a185fd` | `b587e914-dc3f-58bb-8851-0b0a45f137db` | In productie |
+| `c541ce1c-6829-5fe9-95f6-193bc0422f19` | `4a891bdb-b51b-5c7f-bc1c-ffb4560be57b` | In productie |
+| `b443fb80-5700-57e5-b28f-d72ee8d0ce84` | `566c2909-d286-556a-9f72-0c0f0a9b4360` | In productie |
+| `f7f90556-f208-5aad-adc1-8615a15a8ff6` | `03d127e4-f68f-54f3-afa0-95dd48ee0a5f` | In productie |
+| `2d094a98-e953-5879-b4aa-b786a3eacf69` | `2dbe3728-23fc-55c3-9141-5ead8b406481` | In productie |
+| `5f20d6ed-230c-5a1b-ac68-21395d3e1ee3` | `03d127e4-f68f-54f3-afa0-95dd48ee0a5f` | In productie |
+| `8cb37fce-cc80-50ce-8d3d-5a93fff4bb0e` | `566c2909-d286-556a-9f72-0c0f0a9b4360` | In productie |
+| `3e744e92-73b3-5a19-b3f8-e54d19c3f166` | `566c2909-d286-556a-9f72-0c0f0a9b4360` | In productie |
+| `70dd49d0-da95-5247-a0e7-a0ed5001fc40` | `03d127e4-f68f-54f3-afa0-95dd48ee0a5f` | In productie |
+| `181e3a87-94e4-5330-b0ca-c524d7e8082b` | `80fbece3-0816-5e1d-bc9d-354060d537a6` | In productie |
+| `c66ba940-10a8-51b1-b951-a14a21c86d90` | `c30a71e0-b811-562d-824b-5ee926df5965` | In productie |
+| `75489082-f5ee-5222-9412-b19e3846e11d` | `189ef7ed-1878-58eb-83a8-403a1e80dd69` | In productie |
+| `3d656197-72e8-54b4-88b4-516e4aa2512a` | `189ef7ed-1878-58eb-83a8-403a1e80dd69` | In productie |
+| `e883ea9d-3382-53cb-be1a-385672d23bc6` | `062695f2-ea61-51a2-94f7-ed5ded74d150` | In productie |
+| `50143d06-84ae-50b9-b2b0-88e5c6a3d685` | `11a29108-2ad5-5602-8b2c-4b9cdb914f6b` | In productie |
+| `c376f1f3-c49a-5c68-abfd-bd8eb57c649e` | `fe6508e2-8398-5056-873d-d6635ff3da46` | In productie |
+| `6219c2e5-4db0-5222-8e77-0cbd293f7c6c` | `0bc6d413-719b-5c90-8c42-a3327e12ac23` | In productie |
+| `53e1a0bf-2613-57e0-8d54-058715a5bbb2` | `ae0e7ebc-d135-5211-a6f0-4fdd774e7064` | In productie |
+| `40df8e95-65e6-52ac-bc9b-f999b7efcb36` | `f123596b-19b7-50cf-a6b7-5818efb2d016` | In productie |
+| `70b03ad3-1a61-53cf-980e-159db9023501` | `212fc901-e39b-55b8-aaf0-a3af54cd57b3` | In productie |
+| `f9fed205-11c4-558e-b554-c94905ff822b` | `f743f6d9-b3ab-53b0-888b-e4b91cda7c75` | In productie |
+| `7fafb971-1b92-5817-8dc4-b3f0e87a0cc3` | `3f83ea80-a317-5e12-8256-66a9a6973a78` | In productie |
+| `cd7fae15-85d9-5ee0-8fcb-ce7b08554d25` | `e7c4e874-de45-59db-92a4-d551f396cdc5` | In productie |
+| `7b823159-cad8-5a52-83eb-5f9d6cf98185` | `587c6f5d-9a6a-5375-9a62-7158b570d76c` | In productie |
+| `e8be48f0-39e0-585e-a84d-083216a8df20` | `587c6f5d-9a6a-5375-9a62-7158b570d76c` | In productie |
+| `e1fcadb3-1136-5e77-990b-2347b534c882` | `74e4b65e-05c9-54d6-8af9-cd17e55331c1` | In productie |
+| `284d4e37-2051-5d4f-ae0c-946944fd502a` | `74e4b65e-05c9-54d6-8af9-cd17e55331c1` | In productie |
+| `6b70498a-e498-5404-b0fa-1a562364cafb` | `74e4b65e-05c9-54d6-8af9-cd17e55331c1` | In productie |
+| `303a2250-dec0-5f49-b834-92a4a9b9af87` | `a8e31e56-2c63-5222-ad23-f42b4f2b0901` | In productie |
+| `b187e386-2f27-5057-93e5-6dce0e9d5fdf` | `07298fbe-1bea-5ff8-a240-662fd78c3375` | In productie |
+| `15a0c791-8f1d-5f4f-a4f5-ec63de032cd4` | `72c176f7-4262-5486-bef9-0f91d108ffe3` | In productie |
+| `abcf1b74-ac63-5ee3-9634-7d14e66c31a5` | `83eb1063-6429-5b91-b7ad-5a8d40fd8bef` | In productie |
+| `d73b1d87-11c7-5119-805e-01382586d38c` | `8c0f09a4-f67a-526c-a0c7-ae071f378b32` | In productie |
+| `bf2f08c6-72be-5d37-86a9-229e7373187a` | `0719a840-79e7-5062-b4be-9022b501a635` | In productie |
+| `d1c08d37-a08b-5b78-84b6-7a84892fe6e8` | `b24d29c4-6719-58dc-9fe6-a59533c0c3ca` | In productie |
+| `c9357218-542d-500c-a7e4-77624f9d801a` | `0719a840-79e7-5062-b4be-9022b501a635` | In productie |
+| `2aad987a-f017-55e6-8df3-d9430a630f0e` | `c1067469-f900-5b78-8780-b2c33f55441c` | In productie |
+| `fd132528-59e0-5e85-8b7b-297300d56d91` | `29c48227-25d4-5e7c-936e-634a26a8558f` | In productie |
+| `f1fd3ebe-180f-5c33-99e7-af4e5f9e4df7` | `ba997855-9793-55a9-bb46-3e564be6c920` | In productie |
+| `43bef2f7-7cf5-5a58-89c1-f31bf8cb779d` | `e6b0f24b-0243-5466-aebe-5a663f9b8828` | In productie |
+| `66ccc13c-d921-5350-9dcf-b46ea35f06dc` | `ff647584-8edc-55c5-b4cc-95f9432e5638` | In productie |
+| `fa48af69-d625-5d73-b5df-f44b88331ec5` | `566c2909-d286-556a-9f72-0c0f0a9b4360` | In productie |
+| `6e81f313-ccdb-5cef-b91b-153abc38107a` | `03d127e4-f68f-54f3-afa0-95dd48ee0a5f` | In productie |
+| `6f8f0d62-e2c9-5950-a21c-fb460889f799` | `2dbe3728-23fc-55c3-9141-5ead8b406481` | In productie |
+| `3c2bd9be-d96f-5ad9-8dca-cdc6b79d3402` | `03d127e4-f68f-54f3-afa0-95dd48ee0a5f` | In productie |
+| `0e6ba778-4057-5349-a6e8-76efe170c483` | `566c2909-d286-556a-9f72-0c0f0a9b4360` | In productie |
+| `0d0d2540-e4d4-5fdf-882d-10c263c7eb23` | `566c2909-d286-556a-9f72-0c0f0a9b4360` | In productie |
+| `070d5148-8808-5504-882b-5a65a1a84afc` | `03d127e4-f68f-54f3-afa0-95dd48ee0a5f` | In productie |
+| `c43eff3f-b5c8-5bd9-b702-c59f6f9adb89` | `f17acc8f-8435-58e4-81fa-9a4740da2c8d` | In productie |
+| `72d6d75a-e3f7-50cb-92c2-062b0bb68ec4` | `03d127e4-f68f-54f3-afa0-95dd48ee0a5f` | In productie |
+| `d61629e6-d5be-5a5f-8d65-f85e3e0a74e9` | `03d127e4-f68f-54f3-afa0-95dd48ee0a5f` | In productie |
+| `17f690ca-b7f2-5fe9-81bf-389abdc79788` | `03d127e4-f68f-54f3-afa0-95dd48ee0a5f` | In productie |
+| `55151271-3f20-51b5-b5aa-f204e25e9295` | `97aa75c0-f766-5c51-9289-d46c5ef9c17f` | In productie |
+| `08ab67a5-bbb4-5504-b587-ea7a4a35d9e1` | `f9782f92-1609-5528-9104-d2977b6a40ae` | In productie |
+| `79d76b15-555e-5809-9c20-f7798d4f4b65` | `9d79e8f7-c653-59a5-abdb-88a735329057` | In productie |
+| `aad47347-2814-5d5a-8403-0da6d50240fc` | `d2575263-1a23-5b36-a7f0-66117695859a` | In productie |
+| `776a1897-5ef7-5911-8820-1de5b1c4b65b` | `3cafed90-f78f-5ae5-b2fb-49a8b6feacbb` | In productie |
+| `048f40c5-6e0c-55c9-9374-b588745ed441` | `b6ec02de-d937-5fee-a102-b11d9ab5e186` | In productie |
+| `5f43cffc-bcdc-52db-ab34-34d26a094f5a` | `b4c645f1-107f-5fe2-b0e5-6b51cd65e869` | In productie |
+| `c93dfa56-2320-50a0-9f8a-638eb2ffa9c9` | `36c9cf1b-890f-58be-a2a2-a2b22e1e3d79` | In productie |
+| `c6cb6dc1-f398-546c-b2b2-8c0f35a4e084` | `98d82790-35b1-53a9-be0d-4c5480930bc1` | In productie |
+| `1f570f4f-0c27-5139-b031-75831e367c94` | `da869f57-d2f5-503d-9880-049a80abdce3` | In productie |
+| `1c7f4063-45c1-5f65-af9f-941fe2a5f874` | `2c080d1d-5ac2-5f57-9855-8db8b75b5d6e` | In productie |
+| `2fc1dd91-383a-5ddd-8004-615eeecb07cd` | `47d7b3e0-5a8d-56cc-b2f6-a9e2693afc84` | In productie |
+| `f36c89b9-8bc7-592d-95e3-ba6caf9e67c0` | `2b1d6a02-cc53-5c5a-8966-1d204e84a05e` | In productie |
+| `7bc46d4d-2684-5dba-af47-3556ecd624db` | `b0bdd9ed-2b95-509c-baf6-589e107aee6d` | In productie |
+| `4a67bbea-7f11-540a-ad7b-88c53ce177e7` | `ee4bebd2-da4b-5ba3-9894-96c6c45807f0` | In productie |
+| `61d0fba0-9e0a-5c74-96eb-9076246a480e` | `130c4330-dbb7-590f-a141-0435fdd2fb0a` | In productie |
+| `bf646a8d-1566-5108-8448-48c26fb21a6e` | `926592e3-a0b2-52c8-abd8-877cac901eea` | In productie |
+| `629a612c-fe4a-5fdf-b3be-da264d94bce1` | `67a473c5-26ef-5a14-baf5-3bcf09437718` | In productie |
+| `fd253573-7262-5d62-bd26-06483f4e2587` | `a2e7d8e6-c6f4-557d-9f97-a6bd23782490` | In productie |
+| `d33b3775-9d97-5617-a1ac-7638e1ad48f8` | `52bbdc0f-424b-5de6-b7b4-1e3551574f44` | In productie |
+| `66c80080-2349-53b3-bee6-1ed99a9d5b42` | `81a11297-a9fc-5a92-b3f3-77964a76d63b` | In productie |
+| `b83a12c8-48d9-5ae0-9ccb-9f48ad2af10f` | `3ee435e7-5759-5ebc-8397-e82207d9a504` | In productie |
+| `974aa330-e990-51e0-89ba-cb9915689031` | `c66dc514-afa4-5d4f-87cb-54cd3ef958fd` | In productie |
+| `7d40664f-bb7d-5492-98da-f003133e3743` | `c66dc514-afa4-5d4f-87cb-54cd3ef958fd` | In productie |
+| `83cb1cb9-b6d9-53e4-b829-5c362148e980` | `c66dc514-afa4-5d4f-87cb-54cd3ef958fd` | In productie |
+| `4e31bb7d-6ce7-5749-a4fe-e0f8d849db15` | `c66dc514-afa4-5d4f-87cb-54cd3ef958fd` | In productie |
+| `fe664e46-a9a1-5250-a1b4-c95d7d3842c4` | `adba44af-862e-5b03-bc5e-4b368379cdd3` | In productie |
+| `8ae5c51a-e298-5ccf-8546-db732b9127b5` | `adba44af-862e-5b03-bc5e-4b368379cdd3` | In productie |
+| `cd38fab7-6a09-518a-903f-52681cf4eb24` | `adba44af-862e-5b03-bc5e-4b368379cdd3` | In productie |
+| `e6b0443a-baf6-5bf6-b812-d0164e6a5dfc` | `adba44af-862e-5b03-bc5e-4b368379cdd3` | In productie |
+| `81424ab4-6e7b-50e6-b286-43154885131c` | `adba44af-862e-5b03-bc5e-4b368379cdd3` | In productie |
+| `97df4b03-7865-51f7-bc1f-b2577207799c` | `adba44af-862e-5b03-bc5e-4b368379cdd3` | In productie |
+| `cc0c4ed7-efa6-5aae-9e73-c9e9a74ab9fb` | `6c00ece9-f70e-5e10-a4cb-a2d1f2f949e7` | In productie |
+| `37718b94-7098-59ab-a90a-284eb78f3783` | `6c00ece9-f70e-5e10-a4cb-a2d1f2f949e7` | In productie |
+| `bebe1f31-af01-59d4-b23b-6f6c16b5b486` | `2b1d6a02-cc53-5c5a-8966-1d204e84a05e` | In productie |
+| `6ef3d981-8625-5fa5-8688-d7546af9a969` | `6c00ece9-f70e-5e10-a4cb-a2d1f2f949e7` | In productie |
+| `44b6b52d-456a-58f4-8437-46efd0176a79` | `6c00ece9-f70e-5e10-a4cb-a2d1f2f949e7` | In productie |
+| `998172db-f9aa-57f3-8e72-a87aae058db6` | `f123596b-19b7-50cf-a6b7-5818efb2d016` | In productie |
+| `c60204f9-b111-54f2-ae8f-c3e015597da5` | `d3a6ecb8-6d65-5d52-be57-6401ca446c86` | In productie |
+| `e85dc7e6-7a99-512d-9b42-451bb92457fa` | `7bcf1491-5882-5cc1-aad7-bdeb229bcfdd` | In productie |
+| `429c8cd8-e9b2-59a8-8b7a-5348e78015af` | `0b63bfcb-9a81-5d18-9ea8-3d40b9a1bdd4` | In productie |
+| `f82878a8-cf82-5600-8630-b947a7e463d3` | `865c7cff-ab5a-5146-8feb-ea52dfd438e2` | In productie |
+| `ae503fb5-35d1-5f7f-a38a-52dc0240e126` | `6afa005c-6c31-5222-87ae-acb5b45318a9` | In productie |
+| `1906e2b4-e21f-560a-a88a-8ba0019ec4ee` | `d5ab42c0-3fcb-5c67-a9f4-0482f834b452` | In productie |
+| `b47b35b1-5bc4-550e-9945-51a436712742` | `4accf4c9-5a9e-5e25-8b2c-42e10ebad78a` | In productie |
+| `5adc110d-26a3-591a-a171-a8a6f31d78ee` | `99b8707c-ccca-51fb-8e4f-5351950e6e3d` | In productie |
+| `ec562695-0c47-5474-8cf3-69e449126156` | `d7d971b5-c870-5f34-93f9-622cedb923d5` | In productie |
+| `5c26db1a-b428-53c3-b61f-f98edb3a8caa` | `56e9844e-eab4-5c75-b12a-c34d4a77a3d3` | In productie |
+| `d81342f8-6b25-53e0-945c-6b2ba5d66115` | `94f72e1c-7e16-5c9e-976d-7b3d443549df` | In productie |
+| `2a342094-e462-5d5c-8d63-c93d41407f33` | `aa6d2d7a-99bc-5052-b3e5-acf6fb6e2877` | In productie |
+| `57fc7bd1-6f5e-5c45-ac6b-7c376e437fdf` | `d1a96f4b-e09d-5029-8774-389c5dc347e7` | In productie |
+| `6c19e1e7-95e6-5a33-a28d-a635af134629` | `a4cb45cb-2dd4-57bf-80ba-32439ed7c7c9` | In productie |
+| `11ad684e-0dca-5082-bf3c-fe7f3d5543e3` | `03bd04ff-0d9f-5b99-9f6c-ceadac5a01fe` | In productie |
+| `89ec89a7-0076-5a39-8b4d-0ac1f8e411a4` | `3f5952d5-da95-5321-859f-6105566eec5a` | In productie |
+| `e4bc2a59-10fa-549d-ac3f-14550468837e` | `f0ab6c7a-fcb9-5fa7-9f5e-d665eaa48bc2` | In productie |
+| `30d9fcf9-29dd-5796-ae8e-fe7031b0eca7` | `f0da48a9-5846-5ab6-8901-80728bcf9d97` | In productie |
+| `bb5e0f9e-2bf9-534e-9b14-310247418226` | `60cd2221-9fe4-550d-a4e2-734f5ef50f2e` | In productie |
+| `bcfe8727-d5f9-5005-92d1-0de06a649138` | `d9bd674a-1285-5772-8c9d-b95e9d3e829b` | In productie |
+| `59985985-c0da-5cca-bd68-9e9b137e8969` | `c5fbdc7f-456d-542d-97f8-e1ea79e28039` | In productie |
+| `ec301b4f-44d2-503c-bbf8-bceec703b0d6` | `721d4118-dbeb-593e-980a-d4e5a917a2b8` | In productie |
+| `cf5443ab-b74a-5a47-8cd3-095e5dcc50b1` | `2d2083a7-be19-503a-8267-6b727a915af5` | In productie |
+| `e7451e6e-f52d-5ae1-b0f2-0f1a2e3ab4f6` | `2d2083a7-be19-503a-8267-6b727a915af5` | In productie |
+| `db4cb12b-c94e-5348-9776-46af7d62ed61` | `2d65860b-bedb-576a-991d-6a7e06651b5e` | In productie |
+| `490cc97d-5be0-5ec3-a3d2-ab87aa0e5df9` | `484b36ab-85cf-5929-82f3-7101729efe00` | In productie |
+| `44b63ca9-c1a2-5ddf-a9a7-f287ded5f33c` | `5ced0890-f379-5443-8dd2-cebb74800a3e` | In productie |
+| `0266cacb-973e-5ec1-b3d6-67b96fa30967` | `21c01daf-75ee-5ec7-a6c4-b575d62b4afe` | In productie |
+| `ee2693a1-7104-5fe9-943a-d75ccc0b29fa` | `3ee435e7-5759-5ebc-8397-e82207d9a504` | In productie |
+| `76303dab-1dd0-50bb-9598-4aa5dbbc9a55` | `7131c342-a55f-5189-a197-f60d719af63a` | In productie |
+| `0e2b2fb1-16ac-511c-8c02-a52a802ebf2c` | `ec54d714-0fce-5776-b8da-19798d6ada4a` | In productie |
+| `31e425c5-69d5-5671-ba14-3787bdde8426` | `b6547611-f2c7-5cd8-92cb-f56c114837a4` | In productie |
+| `d68eebfe-8dba-59d9-a735-9ff0d72cd0ec` | `b6547611-f2c7-5cd8-92cb-f56c114837a4` | In productie |
+| `51304fa3-fb20-592c-810b-ed7d0adf319b` | `b29040ce-129f-5b5a-9c86-12246bb9e350` | In productie |
+| `780435a4-ce87-513a-97bb-d810896c379d` | `07298fbe-1bea-5ff8-a240-662fd78c3375` | In productie |
+| `3d691520-f36c-5d13-81c2-ba7000477357` | `07298fbe-1bea-5ff8-a240-662fd78c3375` | In productie |
+| `47ce0b1f-8d41-5923-befb-9dc8c29fdad0` | `b6547611-f2c7-5cd8-92cb-f56c114837a4` | In productie |
+| `308d6eb0-5a1b-5c3d-ab65-677c80c27c3d` | `0719a840-79e7-5062-b4be-9022b501a635` | In productie |
+| `a4018a79-16f4-5371-a4b4-a564728821fa` | `b24d29c4-6719-58dc-9fe6-a59533c0c3ca` | In productie |
+| `483a36f5-7e03-5dff-9db3-10966ac79090` | `0719a840-79e7-5062-b4be-9022b501a635` | In productie |
+| `d0fd0cb0-d7e5-5d42-bd02-d15c7cefd86d` | `6e49300d-0b7d-5f93-8416-c3350127cafa` | In productie |
+| `1e4911ea-4f7a-5bbb-a0ae-6cd8cd3c7593` | `2439a09e-bcd7-579d-b597-08b509b20606` | In productie |
+| `43b94aca-a175-544c-9c21-fdfe6b44db2a` | `e22042c3-fbf9-5d32-87c5-3f0d985345cc` | In productie |
+| `c3f2e170-c1d4-5233-a56e-2dfec9fdf6f9` | `8f93564a-8d41-5035-811c-944ba0ef6490` | In productie |
+| `381e6369-f602-5ee0-a859-c5a94ebcff08` | `e5263ff1-5c2d-5fe8-b248-941068af1529` | In productie |
+| `167df4d4-ed72-582b-b5c8-f5429740ce1b` | `f663d1d5-1bb1-5ffe-bdec-5595f4a3c4ea` | In productie |
+| `d169a836-6497-5926-8408-b3eb142a8c49` | `840e6040-0439-5432-8c89-2dd62079d8c6` | In productie |
+| `940d112b-f545-567e-8be8-ddaf83daafdc` | `226e8fb6-58ec-59e3-ae0e-eaa642b43de7` | In productie |
+| `7d44d88d-f132-5dbe-bbae-d35d6cbd633b` | `226e8fb6-58ec-59e3-ae0e-eaa642b43de7` | In productie |
+| `ae15adbe-75da-57fd-b29a-0e3668a23204` | `564bbbb1-2753-5e01-ac7b-3c96ccc287ca` | In productie |
+| `aa056488-026b-557f-9524-f9b5f4bf92b0` | `b6dd443b-1ed7-5e65-b834-428a996ed168` | In productie |
+| `81106390-52d9-5cca-9009-2491726571ba` | `1cde0fb5-9720-5fd3-9914-3b2f409d1bdb` | In productie |
+| `7bcb19cd-2356-57a0-989b-2b88db39d688` | `ab4a56be-d562-57e1-85ab-05f953cc159d` | In productie |
+| `230b924d-a39b-58b2-a9c9-27f76488e4aa` | `9d5faa63-069f-5b5f-b2ed-b4913bd14a91` | In productie |
+| `e555d0ba-b155-5cb7-9529-0ca3df176941` | `13b2adb6-d741-55dc-ab5a-bf563f5497b1` | In productie |
+| `67450150-9790-5bb6-b77d-9db057f87730` | `0b676fb0-a91f-5553-bb0a-f024ebe37170` | In productie |
+| `4a1cae12-b472-54ef-abfc-2eb9f5624dab` | `c66dc514-afa4-5d4f-87cb-54cd3ef958fd` | In productie |
+| `f218524a-aa76-55ed-ac79-8ae40d17a5ad` | `c66dc514-afa4-5d4f-87cb-54cd3ef958fd` | In productie |
+| `1a9ac65e-0a4a-5997-b7f5-ce1022c7d3f3` | `c66dc514-afa4-5d4f-87cb-54cd3ef958fd` | In productie |
+| `96e14b0f-749c-59ca-8f1c-9da02fd49535` | `c66dc514-afa4-5d4f-87cb-54cd3ef958fd` | In productie |
+| `2cbae82f-2c31-597c-8c39-807ad65655ce` | `adba44af-862e-5b03-bc5e-4b368379cdd3` | In productie |
+| `93b93a7b-fcd6-54ee-b395-85ac94cc3fb7` | `adba44af-862e-5b03-bc5e-4b368379cdd3` | In productie |
+| `1ea827d2-706d-5f22-b8ca-02a086d1306e` | `adba44af-862e-5b03-bc5e-4b368379cdd3` | In productie |
+| `02306398-fa25-51af-8773-b6c1be06e1fe` | `adba44af-862e-5b03-bc5e-4b368379cdd3` | In productie |
+| `4262564d-c91c-5359-be45-4167ac5aa6ca` | `adba44af-862e-5b03-bc5e-4b368379cdd3` | In productie |
+| `7b9ea932-3ccb-5d0a-99b6-6563413aacb0` | `adba44af-862e-5b03-bc5e-4b368379cdd3` | In productie |
+| `658776ad-db9d-5e21-9094-eb8b6d0acea7` | `6c00ece9-f70e-5e10-a4cb-a2d1f2f949e7` | In productie |
+| `80629920-6c31-57c6-8391-d1dcce20d800` | `6c00ece9-f70e-5e10-a4cb-a2d1f2f949e7` | In productie |
+| `39b6f197-351e-592b-a230-35c2cc4263f3` | `6c00ece9-f70e-5e10-a4cb-a2d1f2f949e7` | In productie |
+| `bac4f439-43fa-5b87-b58b-b990199bbd0a` | `6c00ece9-f70e-5e10-a4cb-a2d1f2f949e7` | In productie |
+| `fb5b5969-84ce-53fe-8be9-3443dd37a3b8` | `226e8fb6-58ec-59e3-ae0e-eaa642b43de7` | In productie |
+| `b43d34fd-0e0d-5756-a706-37f17247e490` | `226e8fb6-58ec-59e3-ae0e-eaa642b43de7` | In productie |
+| `a2b0c123-e787-5ba1-baaf-9d0c2956fa95` | `0f5ea3ee-edf4-5b94-82a8-6296cb282d7a` | In productie |
+| `ab6f4dd4-80b2-5855-b61f-e92a86e4900a` | `2bd454ce-1339-54e7-b428-d182a30c48de` | In productie |
+| `3c368cd4-2729-5ae9-abc6-32cb78b95009` | `5b30e84f-e299-5284-ab70-9a934602b08d` | In productie |
+| `e053883c-aa13-5a37-b4cd-f329d4e286c7` | `ac68d69f-8065-5dee-945a-33175589ed24` | In productie |
+| `53391dd9-d58b-5fbf-a6a4-36d496f16134` | `23a3315b-7fb5-569b-bdd4-b02198897d3e` | In productie |
+| `57985e83-2c3c-5aa5-b627-d06f882a97f2` | `26d0faff-5b8b-5b47-a118-2d7f1999f677` | In productie |
+| `eefde240-7d75-58b4-9e2f-5ca36717e2cd` | `23a3315b-7fb5-569b-bdd4-b02198897d3e` | In productie |
+| `230d4ce9-ef24-5ca6-b89d-5a92bdc62bda` | `26d0faff-5b8b-5b47-a118-2d7f1999f677` | In productie |
+| `4bebd7a6-0ddb-5953-b60c-53dd5e67c2e5` | `3c7c6c08-447a-521b-93eb-dd9ed7bbbc8b` | In productie |
+| `bdf47914-9281-532e-8926-10364e125402` | `b6dcb47d-3a31-5119-9468-a5786e0213b9` | In productie |
+| `a87c7eca-ea0d-5138-9a47-5d50ad5e0647` | `062695f2-ea61-51a2-94f7-ed5ded74d150` | In productie |
+| `9995283d-53be-5d1f-8b9c-184c6d900a22` | `11a29108-2ad5-5602-8b2c-4b9cdb914f6b` | In productie |
+| `c338a862-374c-5835-9728-409a859df6c9` | `11a29108-2ad5-5602-8b2c-4b9cdb914f6b` | In productie |
+| `4d29f6a6-13f9-597e-b68c-ba09626824fe` | `11a29108-2ad5-5602-8b2c-4b9cdb914f6b` | In productie |
+| `68f3e871-8561-55ce-a148-c16872e2fb21` | `11a29108-2ad5-5602-8b2c-4b9cdb914f6b` | In productie |
+| `c6ab3487-5422-5add-a5fb-ebc86e2e19e0` | `5f3ba828-1325-5c6b-9734-b50d629bbb91` | Gepland |
+| `d2fb7736-9729-5f4f-8279-af1c2175d236` | `c5bbaa02-56f9-5cf1-9628-563879d919d4` | Gepland |
+| `86434b2d-9750-5f38-9714-80fd903dfa4e` | `9049b24d-79f1-576e-acd0-d9590dabc1d0` | Gepland |
+| `690491cb-38b5-5d80-bd9a-aa6b612d2990` | `25b72858-de33-52ca-8d55-7de41a164b20` | Gepland |
+| `19006a3f-973d-5e85-bcc6-3377c5093541` | `e2165720-c78a-5bce-9bb2-48c62a5ab0a0` | Gepland |
+| `3e334326-22c3-5d0e-9f49-53dbc102a97c` | `d3a6ecb8-6d65-5d52-be57-6401ca446c86` | Gepland |
+| `cb238224-826f-5548-bebf-22f7c0a63ee0` | `86328d80-c96c-5355-82af-7dc0a51fad52` | Gepland |
+| `ea05956a-08b5-5732-9732-cd4a7d45b268` | `558c6b7c-ebb4-5efd-bb8c-96c49c619c78` | Gepland |
+| `74f37a03-ce7a-5094-b6f0-064dc58b0034` | `b8f318bd-c429-5853-8c3f-901123c15834` | Gepland |
+| `17be8fec-61fb-5a22-98aa-138aa4777717` | `255dbdbd-0dd5-5038-b60c-7aad44292514` | Gepland |
+| `063bd20e-9cc2-5099-884b-5ca6fd758c7c` | `4716222a-5440-59fe-883c-07d24b0a2fd9` | Gepland |
+| `c2a9c32e-5a6b-5f7b-ba4e-23392b349694` | `566c2909-d286-556a-9f72-0c0f0a9b4360` | Gepland |
+| `3f1d6c82-178c-5e0d-a76d-e1cdc9656d36` | `5697b02a-9a2e-52d4-81ea-7fbf4b6c6f53` | Gepland |
+| `b7e25278-cd83-52cc-a2c4-c69f97b84110` | `4fa01260-58b3-589d-be51-430d7e96c747` | Gepland |
+| `79b8cc1a-0e22-50a1-853f-f8bf025a8c17` | `2dbe3728-23fc-55c3-9141-5ead8b406481` | Gepland |
+| `905ee4cd-258f-588d-ae96-c5acfdccad66` | `566c2909-d286-556a-9f72-0c0f0a9b4360` | Gepland |
+| `6726aef5-1a12-5813-91b2-4268b233eddb` | `566c2909-d286-556a-9f72-0c0f0a9b4360` | Gepland |
+| `bb2827f6-47f4-558f-86a6-105338e2792b` | `5f3ba828-1325-5c6b-9734-b50d629bbb91` | Gepland |
+| `d0bd3c5d-7d5a-5464-900d-98e9ded9781b` | `919734b0-40ae-5efa-96c4-ce5931b88ef1` | Gepland |
+| `4dcef342-a78b-51ca-80c5-5835426a89b1` | `834210ff-077f-51c1-b152-6f82075ec9b1` | Gepland |
+| `00b4e644-7d01-55cd-a7a2-3102dc0b4213` | `1b043479-aa43-5b76-a70f-585403443813` | Gepland |
+| `df11d363-3fe9-519c-9a39-4f356241a947` | `60b13872-15a2-582c-a88d-b36e5766c85e` | Gepland |
+| `79cc0ba5-0c94-57ab-9a80-fce9aab61b33` | `61c158cc-47c0-5df0-91c1-3532ab911986` | Gepland |
+| `ee0b8a02-a2f2-5972-b916-6195ff3dadc1` | `bca3d60e-dcc2-5529-96a1-ff72900053d0` | Gepland |
+| `21806b3d-5642-5ba3-9cd4-034583d463ae` | `266c4f97-5d00-59f6-b6a3-2481efdd4e6c` | Gepland |
+| `ea0a0421-a736-5952-9695-1b5c80d7daf0` | `13a6bc35-0132-5ec8-99e1-24a82b9d2c98` | Gepland |
+| `4b436ac3-a30e-5709-8760-21e586ff97f8` | `dfe96d5a-2ac4-553a-8d0b-5a46b5243516` | Gepland |
+| `aee534af-2785-5eff-8f68-4533b1b581a4` | `587c6f5d-9a6a-5375-9a62-7158b570d76c` | Gepland |
+| `b3c78b30-db90-5f6e-b81e-3c46437a79a7` | `587c6f5d-9a6a-5375-9a62-7158b570d76c` | Gepland |
+| `8cad5f56-6659-5d2e-8ad1-0df99ac27d44` | `bbe7fe14-e6ea-517a-9ff9-2d3de37e47ff` | Gepland |
+| `51eb69da-a8c2-5f81-aa27-ffa7d80e371d` | `15a76909-5ae2-596f-9a0d-7652a9c883ce` | Gepland |
+| `69d6f382-2c45-533b-a091-1453ac94b464` | `aea1eaf1-2432-5220-b221-f5a9f0f9cef5` | Gepland |
+| `1fca5f10-e9ee-53b1-928f-3bd9d4af800f` | `11a29108-2ad5-5602-8b2c-4b9cdb914f6b` | Gepland |
+| `b9dd3854-2aa7-5e06-8708-83ce79980fb3` | `11a29108-2ad5-5602-8b2c-4b9cdb914f6b` | Gepland |
+| `b2563d2c-13eb-568f-8a8b-1a779cf2f3aa` | `11a29108-2ad5-5602-8b2c-4b9cdb914f6b` | Gepland |
+| `e7bad74d-d28e-54f1-9af8-41c9e5908626` | `11a29108-2ad5-5602-8b2c-4b9cdb914f6b` | Gepland |
+| `9c011519-be11-5625-a512-0d260967a27d` | `11a29108-2ad5-5602-8b2c-4b9cdb914f6b` | Gepland |
+| `cd469ebc-f3a8-583b-a7e3-3854f6a2cf86` | `11a29108-2ad5-5602-8b2c-4b9cdb914f6b` | Gepland |
+| `b437dfa6-a806-591f-9920-8d9281f49436` | `3ee435e7-5759-5ebc-8397-e82207d9a504` | — |
+| `77cef2ff-dfb3-5a86-ab29-01003698e607` | `95225b45-56b4-58b4-9757-92e89b8fe889` | — |
+| `8f0595ec-2a1d-5572-af4a-4c532b341d44` | `062878e7-8d8a-5b21-a135-6e992eb3223b` | — |
+| `fc5b3346-9c01-51d1-bf35-30623a6af354` | `e1326efe-f333-504d-8317-bb4ce4d25fb7` | — |
+| `0ee02782-55ec-5b61-bef9-bedc182d5ac3` | `3c5f896e-b92f-543b-8e65-39dc357d3150` | — |
+| `b09c2bc4-5458-5563-a98e-97e12525d3e9` | `61cae1b5-61fe-5bb0-a74a-726dbe3419d7` | — |
+| `93071188-e4ad-5114-8e74-84976a46a67a` | `29227d60-373f-5c6e-b916-5a2ef0b37ec1` | — |
+| `5cfa083c-e320-5e61-b06b-350f14f11b07` | `b9ab7d99-5a23-5839-9921-6e4734dc37b8` | — |
+| `ca730c98-a1a5-5ed1-8518-2848402f2579` | `ab8af2ec-4599-5561-a830-e630e255ce4a` | — |
+| `ac4c0316-fc5b-5810-a529-89232cadda0d` | `e471e8e4-8c00-5759-86c7-6f813a6fa080` | — |
+| `77ddc5c7-d22a-58f6-b19a-76645cd0b4b1` | `b9ab7d99-5a23-5839-9921-6e4734dc37b8` | — |
+| `febccbe7-4598-542a-83cb-e57b1c1d018c` | `ab8af2ec-4599-5561-a830-e630e255ce4a` | — |
+| `3d33106c-179f-5a4e-b18c-439ac5834d8e` | `b9ab7d99-5a23-5839-9921-6e4734dc37b8` | — |
+| `12fb50e1-5787-5398-b256-e93dfd29571b` | `ab8af2ec-4599-5561-a830-e630e255ce4a` | — |
+| `4c439b41-aa28-5f7f-9196-8282431937fa` | `1b043479-aa43-5b76-a70f-585403443813` | — |
+| `14bd9166-b391-57ef-9496-dfacf657070c` | `99280d28-cf43-5a4c-8b78-56fc418a209c` | — |
+| `a64b30c8-924c-5b37-b4ba-2f815a57d9dd` | `14ba3262-a010-58d5-b8de-905bad7a00ac` | — |
+| `53b644d9-bee6-5a2f-9aab-9941214738ae` | `f68bf399-5c4d-5ac3-9584-4a3345a1f3f8` | — |
+| `d5640951-6109-5e89-aad0-48035abbf065` | `24bba2dd-5544-508e-99b4-c13377800746` | — |
+| `80858419-75f2-593f-bfbc-7551496adc3c` | `e6835add-3e9b-5ce1-bda4-69914ff11533` | — |
+| `0356b2ac-93cf-50c1-b877-f7ff7dc74254` | `e956d3a4-0338-58b7-920c-65efc701105d` | Uit te faseren |
+| `0c99fb23-0951-5174-b1bd-dffb419f71b2` | `fdb1c97a-36c3-5406-abef-2bf7bec53ba1` | Uit te faseren |
+| `460b1678-f589-5af8-a5d3-a1663598191f` | `ae0e7ebc-d135-5211-a6f0-4fdd774e7064` | Uit te faseren |
+| `020b4520-97b8-5784-9f99-a248772472ab` | `0822c6bd-1fd5-53f6-9dc8-45a0c6e4629b` | Uitgefaseerd |
+| `73a5f0f7-3983-54d1-9c76-96cb8ae1d0a5` | `ec54d714-0fce-5776-b8da-19798d6ada4a` | Uitgefaseerd |
+| `4bba14ec-c90d-5cf2-a772-a454f0428191` | `03d127e4-f68f-54f3-afa0-95dd48ee0a5f` | Uitgefaseerd |
+| `5c90ec60-1776-5575-9a02-bb9410b81100` | `e7c4e874-de45-59db-92a4-d551f396cdc5` | Uitgefaseerd |
+| `8f0eea5e-693a-535e-b004-1ff272dc3097` | `74e4b65e-05c9-54d6-8af9-cd17e55331c1` | Uitgefaseerd |
+| `6d58dada-0d6e-5113-8a66-8e79dc881b5a` | `f743f6d9-b3ab-53b0-888b-e4b91cda7c75` | Uitgefaseerd |
+| `354b28d6-2556-5630-8a6a-ce371a68fe75` | `678aa789-64ae-5577-8c7b-5b72158d0f09` | Uitgefaseerd |
+| `2d449fce-58f7-584d-aaf1-de513f99bd3d` | `062695f2-ea61-51a2-94f7-ed5ded74d150` | Uitgefaseerd |
+| `8762931a-0c4b-5c12-8ab6-ab737d15258b` | `11a29108-2ad5-5602-8b2c-4b9cdb914f6b` | Uitgefaseerd |
+
diff --git a/issues/248.md b/issues/248.md
new file mode 100644
index 00000000..fdac238c
--- /dev/null
+++ b/issues/248.md
@@ -0,0 +1,67 @@
+# #248 — Titels van de tabs in orde maken
+
+**Status:** OPEN | **Labels:** Aanbod, Afschalen Producten
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/248
+
+---
+
+## Beschrijving
+
+Niet alle tabbladen hebben een titel .
+Bijvoorbeeld de tab "Overige" met het icoon van een grafiek als label maar zonder tekst.
+
+Heeft een relatie met #232
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten
+
+### Reactie 3 — @WilcoLouwerse (2026-01-12)
+
+Hertest (test omgeving) = OK
+
+### Reactie 4 — @WilcoLouwerse (2026-02-02)
+
+Hertest (accept omgeving) = OK
+
+### Reactie 5 — @Makkmetp (2026-02-26)
+
+✅De pagina waarmee je je eigen applicaties beheert hebben alle tabbladen een titel
+
+
+
+✅Via zoeken hebben alle tabbladen van een applicatie een titel
+
+
+
+✅Alle tabbladen van een organisatie hebben titel
+
+
+
+✅Alle tabbladen van diensten hebben een titel
+
+
+
+✅Alle tabbladen van koppelingen hebben een titel
+
+
+
+✅Alle tabbladen van een contactpersoon hebben een titel
+
+
diff --git a/issues/254.md b/issues/254.md
new file mode 100644
index 00000000..de8f2980
--- /dev/null
+++ b/issues/254.md
@@ -0,0 +1,53 @@
+# #254 — Beheer pagina's dashboard: wizard benamingen
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten, Bevinding, Beheer
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/254
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (6)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @WilcoLouwerse (2025-12-08)
+
+De naam voor de Wizard die voorheen Gebruik toevoegen heette is aangepast, dit is dus te hertesten
+
+### Reactie 3 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten en is meegekomen bij oplevering Gebruik op 09-12-2025.
+
+### Reactie 4 — @Makkmetp (2025-12-18)
+
+Hierbij de namen van de Wizards:
+
+
+
+Voor gebruik beheerders:
+- Nieuwe applicatie toevoegen(externe pakketten) is komen te vervallen en wordt geïntegreerd in Applicatie toevoegen
+
+
+
+### Reactie 5 — @WilcoLouwerse (2026-01-12)
+
+Hertest (test omgeving) = OK
+
+### Reactie 6 — @Makkmetp (2026-02-04)
+
+✅De namen van de wizards zijn zoals aangegeven
diff --git a/issues/263.md b/issues/263.md
new file mode 100644
index 00000000..6ef8579d
--- /dev/null
+++ b/issues/263.md
@@ -0,0 +1,44 @@
+# #263 — Niet ingelogd: onder een applicatie staat in het tabje gebruik de gemeenten
+
+**Status:** OPEN | **Labels:** Organisatie en configuratie, Afschalen Producten
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/263
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten
+
+### Reactie 3 — @WilcoLouwerse (2026-01-12)
+
+Hertest (test omgeving) = OK
+
+### Reactie 4 — @WilcoLouwerse (2026-02-02)
+
+Getest (accept omgeving), het gebruik tabje op applicatie detail pagina's is niet zichtbaar als je niet bent ingelogd
+
+### Reactie 5 — @Makkmetp (2026-02-26)
+
+✅Als niet ingelogd gebruiker staat er bij een applicatie geen tab Gebruik meer met de Gemeenten
+
+
diff --git a/issues/264.md b/issues/264.md
new file mode 100644
index 00000000..dae90ecf
--- /dev/null
+++ b/issues/264.md
@@ -0,0 +1,48 @@
+# #264 — Tekst aanleveren: 404-melding bij Niet ingelogd: onder een applicatie staat in het tabje gebruik de gemeenten
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/264
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-09)
+
+@Makkmetp Hiervoor moeten de teksten worden aangeleverd voordat dit issue kan worden opgepakt.
+
+In relatie met https://github.com/VNG-Realisatie/Softwarecatalogus/issues/187
+
+### Reactie 3 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten
+
+### Reactie 4 — @Makkmetp (2025-12-18)
+
+De use-case:
+een bezoeker zoekt een applicatie en gaat naar de detailpagina. De tab Gerbuik is zichtbaar. Een bezoeker klikt daarop en de 404-melding wordt getoond.
+
+Gewenste resultaat:
+- de tab gebruik is niet zichtbaar voor een bezoeker die niet is ingelogd.
+
+### Reactie 5 — @Makkmetp (2026-02-04)
+
+✅Het tabblad gebruik is niet meer zichtbaar voor een bezoeker van de softwarecatalogus die niet is ingelogd.
diff --git a/issues/265.md b/issues/265.md
new file mode 100644
index 00000000..3e626672
--- /dev/null
+++ b/issues/265.md
@@ -0,0 +1,48 @@
+# #265 — Nieuwe gebruiker heeft software-catalog-users
+
+**Status:** CLOSED | **Labels:** Organisatie en configuratie, Bevinding, Beheer
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/265
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @WilcoLouwerse (2025-12-08)
+
+
+
+### Reactie 3 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten
+
+### Reactie 4 — @rubenvdlinde (2025-12-11)
+
+Kleine logica aanpassing in SWC app, kan ik oppakken
+
+### Reactie 5 — @Makkmetp (2026-02-03)
+
+Het aanmaken van een nieuwe gebruiker via Contactpersonen > Toevoegen krijgt de juiste rol: aanbod-beheerder. Hierbij is de rol niet geselecteerd welke resulteerde in een error, zie daarvoor #365
+
+Het aanmaken van een nieuwe leverancier en daarna deze activeren levert een gebruiker op met de juiste rol: aanbod-beheerder
+
+
+
+Bij deze getest en goedgekeurd
diff --git a/issues/266.md b/issues/266.md
new file mode 100644
index 00000000..5b250e75
--- /dev/null
+++ b/issues/266.md
@@ -0,0 +1,60 @@
+# #266 — Na inloggen: Mijn account & persoonlijke gegevens leeg?
+
+**Status:** CLOSED | **Labels:** Organisatie en configuratie, Afschalen Producten, Bevinding
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/266
+
+---
+
+## Beschrijving
+
+Mogelijk door de langzamere indexering van SOLR
+
+
+
+---
+
+## Reacties (8)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @WilcoLouwerse (2025-12-08)
+
+@remko48
+Ik zie dit ook terug, ook op de test omgeving, ik vermoed dat dit iets met "het me endpoint" te maken heeft?
+
+### Reactie 3 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten
+
+### Reactie 4 — @rubenvdlinde (2025-12-11)
+
+Is er een beeld van wat er gewenst is van het "me" endpoint? dan kan ik deze oppaken
+
+### Reactie 5 — @rubenvdlinde (2025-12-18)
+
+Bespreken wat het endpoint nu og mist dan
+
+### Reactie 6 — @rubenvdlinde (2026-01-02)
+
+Als een contact persoon word omgezet naar een nexcloud acount dan wordt de data niet goed meegegeven, meer info in -> https://conductionworkspace.slack.com/archives/C09A4UKGZS9/p1766131589157959
+
+### Reactie 7 — @Makkmetp (2026-02-25)
+
+✅De informatie is direct na registratie en inloggen beschikbaar.
+Er is wel een nieuw issue ontstaan #431
+
+
+
+### Reactie 8 — @markbacker (2026-02-25)
+
+Akkoord
diff --git a/issues/267.md b/issues/267.md
new file mode 100644
index 00000000..cafb5578
--- /dev/null
+++ b/issues/267.md
@@ -0,0 +1,41 @@
+# #267 — Naam is softwarecatalogus i.p.v. VNG softwarecatalogus
+
+**Status:** CLOSED | **Labels:** Organisatie en configuratie, Afschalen Producten, Tekstuele wijzigingen
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/267
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten
+
+### Reactie 3 — @Makkmetp (2026-02-04)
+
+❌De naam is nog niet goed opgevoerd.
+
+### Reactie 4 — @Makkmetp (2026-02-26)
+
+✅De omgeving heeft de goede naam gekregen.
+
+
+
diff --git a/issues/273.md b/issues/273.md
new file mode 100644
index 00000000..f824f76e
--- /dev/null
+++ b/issues/273.md
@@ -0,0 +1,38 @@
+# #273 — Wizard Dienst: Een zojuist opgevoerde applicatie wordt niet direct gevonden
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/273
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Is meegekomen bij oplevering Gebruik op 09-12-2025.
+
+### Reactie 3 — @Makkmetp (2025-12-18)
+
+Nog te testen.
+
+### Reactie 4 — @Makkmetp (2026-02-04)
+
+✅De applicatie is direct vindbaar.
diff --git a/issues/274.md b/issues/274.md
new file mode 100644
index 00000000..8b40f690
--- /dev/null
+++ b/issues/274.md
@@ -0,0 +1,35 @@
+# #274 — Wizard dienst: tekst dient nog aangepast te worden naar nieuwe benamingen
+
+**Status:** OPEN | **Labels:** Aanbod, Afschalen Producten, Tekstuele wijzigingen
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/274
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @Makkmetp (2025-12-16)
+
+Verplaatst van Afschalen producten naar Gebruik.
+Teksten tzt nog nagaan en aanpassen indien nodig.
+
+### Reactie 3 — @rubenvdlinde (2026-02-11)
+
+@remko48 dubbelchecken en doorzetten naar @WilcoLouwerse
diff --git a/issues/277.md b/issues/277.md
new file mode 100644
index 00000000..ae8b0b9f
--- /dev/null
+++ b/issues/277.md
@@ -0,0 +1,36 @@
+# #277 — Beheer: Applicaties overzicht teksten aanpassen
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten, Beheer
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/277
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @Makkmetp (2025-12-18)
+
+Het is onduidelijk welke acties er zijn gedaan en of het opgelost is. Nog te testen.
+
+### Reactie 3 — @Makkmetp (2026-02-04)
+
+✅Tekst klopt.
+
+
diff --git a/issues/278.md b/issues/278.md
new file mode 100644
index 00000000..66375863
--- /dev/null
+++ b/issues/278.md
@@ -0,0 +1,68 @@
+# #278 — Filterteksten aanpassen
+
+**Status:** OPEN | **Labels:** Aanbod, Afschalen Producten, Tekstuele wijzigingen, Zoeken
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/278
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (8)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten
+
+### Reactie 3 — @markbacker (2025-12-18)
+
+Conduction schrijft toelichting hoe dit door VNG kan worden beheerd
+
+### Reactie 4 — @WilcoLouwerse (2026-02-19)
+
+@rubenvdlinde moet hier nog even een linkje aan dit issue toevoegen die doorverwijst naar de toelichting tekst
+
+### Reactie 5 — @rubenvdlinde (2026-02-20)
+
+Oeh, hier zit nog een cashing lijkt het.
+
+### Reactie 6 — @rubenvdlinde (2026-02-23)
+
+Mondeling afgesproken Type ipv Objecttype, dus dat is wat er voor nu even staat. @Makkmetp en @markbacker zouden nog even nadeken over een betere naam (wij wisten het ook niet) dus die pasen we dan nog graag aan :)
+
+### Reactie 7 — @rubenvdlinde (2026-02-23)
+
+Handleidingen staan op https://vng-realisatie.github.io/Softwarecatalogus/ :)
+
+### Reactie 8 — @Makkmetp (2026-02-26)
+
+✅Referentiecomponenten is goed geschreven
+❌De handleiding klopt niet. Deze geeft een pad aan, wat niet te volgen is.
+
+
+Volgens de handleiding kom ik dan terecht op de volgende pagina, waar niks aanklikbaar is:
+
+
+
+Via Schemas in het linkermenu > het openen van een schema via ...Actions > Edit is te navigeren naar de juiste propertie. Door op de drie puntjes erachter te klikken openen alle configureerbare opties. Zo ook de Facet Title. Door iets te wijzigen en daarna op Save te klikken worden de wijzigingen doorgevoerd.
+
+✅Facets zijn aan te passen.
+
+
+
+
diff --git a/issues/280.md b/issues/280.md
new file mode 100644
index 00000000..0b635cba
--- /dev/null
+++ b/issues/280.md
@@ -0,0 +1,52 @@
+# #280 — Zoeken: sorteren gaat niet goed.
+
+**Status:** OPEN | **Labels:** Aanbod, Afschalen Producten, Zoeken
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/280
+
+---
+
+## Beschrijving
+
+Nog te bespreken met Mark
+
+Vraag is ook: sorteer je alleen op de tabel die je ziet of ook op de volledige tabel.
+Waarom is alleen de zichtbare pagina sorteren de standaard?
+- performance - het kost veel resources
+- standaard UX-patroon
+- consistentie over alle pagina's heen - wat gebeurt er als je op zoekresultatenpagina 3 zit en sorteert op A tm Z?
+
+Wanneer alle pagina's sorteren?
+- wanneer de tabel niet te lang is
+- de hele dataset is ingeladen aan de clientside
+- geen performance problemen hebt
+- Sorting volledig en direct moet zijn, bijv. in Excel-achtige tabellen
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten
+
+### Reactie 3 — @Makkmetp (2026-02-03)
+
+Bevindingen op tussenoplevering Zoeken issue #340
+Nog niet opgelost:
+- Sorteren bevindingen niet opgelost
+- Ontbreken filter Type
+
diff --git a/issues/283.md b/issues/283.md
new file mode 100644
index 00000000..0439a2f9
--- /dev/null
+++ b/issues/283.md
@@ -0,0 +1,40 @@
+# #283 — Zoeken > Applicatie: Tab gebruik zichtbaar & versies
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten, Zoeken
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/283
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten
+
+### Reactie 3 — @Makkmetp (2025-12-18)
+
+Het is onduidelijk of dit is opgelost. Nog te testen.
+
+### Reactie 4 — @Makkmetp (2026-02-04)
+
+✅De tab gebruik is niet zichtbaar
+✅Centric burgerzaken en begraven hebben geen onbekend meer staan bij de versie
+✅De versies worden gepresenteerd als cards onder het tab Versies
diff --git a/issues/284.md b/issues/284.md
new file mode 100644
index 00000000..5453e6ed
--- /dev/null
+++ b/issues/284.md
@@ -0,0 +1,88 @@
+# #284 — Applicatie: toont standaarden ipv standaardversies
+
+**Status:** CLOSED | **Labels:** Aanbod, PvE eis, Afschalen Producten, Bevinding
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/284
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (12)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-09)
+
+Wat zijn de standaard versies?
+
+### Reactie 3 — @remko48 (2025-12-09)
+
+We kennen alleen standaarden in de AMEF elementen.
+
+### Reactie 4 — @Makkmetp (2025-12-16)
+
+
+
+
+### Reactie 5 — @markbacker (2025-12-16)
+
+Nog een inhoudelijke aanvulling op standaarden en standaarversies in de GEMMA en hoe wij standaardversies tonen onder een applicatie
+- een standaard heeft één of meerdere standaardversies, gerelateerd aan elkaar met een specialisatie-relatie
+- een standaardversie
+ - herken je aan GEMMA type=Standaardversie
+ - heeft property Versieaanduiding, met daarin alleen de versie.
+ - de naam van standaardversie is samengesteld uit de naam van de standaard en de verieaanduiding
+ - heeft property status.
+ - Toon onder verplicht en aanbevolen alleen de standaardversie met status=in ontwikkeling en status=in gebruik
+ - Toon andere statussen onder toegevoegd door leverancier
+
+Voorbeeld van standaard en standaardversies
+
+
+### Reactie 6 — @rubenvdlinde (2025-12-16)
+
+Betekend dit dat we compliancy zouden moeten vastleggen op standaarversies ipv standaarden? dat heeft dan wel gevolgen voor de filters op de zoek pagina.
+
+### Reactie 7 — @markbacker (2025-12-16)
+
+Ja, applicaties zijn compliant op een standaardversie.
+
+Op de zoekpagina is er een filter Standaarden, maar de te selecteren waarden zijn zo te zien juist de standaardversies
+
+De uit de bestaande SWC aangeleverde compliancy-objecten bevatten een relatie tussen applicatie (module) en een standaardversie.
+
+### Reactie 8 — @rubenvdlinde (2025-12-16)
+
+Actiepunt, vandaag even doornemen wat we hier willen dan zodat we deze kunnen mee pakken.
+
+### Reactie 9 — @Makkmetp (2025-12-16)
+
+Dit is een blocking issue voor livegang.
+
+### Reactie 10 — @rubenvdlinde (2025-12-17)
+
+De pakken we op onder #6
+
+### Reactie 11 — @WilcoLouwerse (2026-01-12)
+
+Hertest (test omgeving) = OK
+
+### Reactie 12 — @Makkmetp (2026-02-04)
+
+✅Standaardversies worden getoond in het tabblad Standaarden onder een applicatie.
+
+
diff --git a/issues/285.md b/issues/285.md
new file mode 100644
index 00000000..8dceb839
--- /dev/null
+++ b/issues/285.md
@@ -0,0 +1,38 @@
+# #285 — Zoeken: zojuist aangemaakte organisatie wordt niet gevonden
+
+**Status:** CLOSED | **Labels:** Organisatie en configuratie, Afschalen Producten
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/285
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten en is meegekomen bij oplevering Gebruik op 09-12-2025.
+
+### Reactie 3 — @Makkmetp (2025-12-18)
+
+Dit dient nog getest te worden.
+
+### Reactie 4 — @Makkmetp (2026-02-04)
+
+✅De organisatie wordt nu gevonden nadat deze is aangemaakt.
diff --git a/issues/286.md b/issues/286.md
new file mode 100644
index 00000000..62f40ef0
--- /dev/null
+++ b/issues/286.md
@@ -0,0 +1,56 @@
+# #286 — Aanmelden organisatie: 500-error bij wachtwoord wijzigen
+
+**Status:** CLOSED | **Labels:** Organisatie en configuratie, Afschalen Producten
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/286
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (6)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten
+
+### Reactie 3 — @WilcoLouwerse (2026-01-13)
+
+Hertest (test omgeving) = OK
+
+### Reactie 4 — @WilcoLouwerse (2026-02-02)
+
+Hertest (accept omgeving) = OK
+
+### Reactie 5 — @Makkmetp (2026-02-23)
+
+✅ Wachtwoord wijzigen via de back-end > accounts is gelukt zonder foutmelding.
+✅Wachtwoord wijzigen via back-end > softwarecatalogus > organisaties > gebruiker is gelukt zonder foutmelding.
+
+Het wijzigen van een wachtwoord door een gebruiker zelf kan straks via de optie "Wachtwoord vergeten?" op het inlogscherm.
+
+
+### Reactie 6 — @Makkmetp (2026-02-23)
+
+✅Het aanmaken van een gebruiker bij een geïmporteerde organisatie is gelukt en ook om mee in te loggen.
+
+Het wachtwoord wijzigen van een gebruiker van een geïmporteerde organisatie lukt niet vanwege de karakters die gebruikt zijn in de mailadressen:
+
+
+
+Optie is om bij een volgende import één of meerdere nepaccounts aan te maken bij verschillende te importeren organisaties.
diff --git a/issues/287.md b/issues/287.md
new file mode 100644
index 00000000..c92abe13
--- /dev/null
+++ b/issues/287.md
@@ -0,0 +1,42 @@
+# #287 — Leverancier: tab met grafiek toont overige applicaties die onder Applicatie horen
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/287
+
+---
+
+## Beschrijving
+
+Dit had volgens Conduction met een limiet van 30 resultaten te maken.
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten
+
+### Reactie 3 — @Makkmetp (2025-12-18)
+
+Dit dient nog getest te worden.
+
+### Reactie 4 — @Makkmetp (2026-02-04)
+
+✅Er worden meer dan 30 applicaties weergegeven en de grafiek-tab is niet meer zichtbaar.
+
+
diff --git a/issues/288.md b/issues/288.md
new file mode 100644
index 00000000..fef00529
--- /dev/null
+++ b/issues/288.md
@@ -0,0 +1,47 @@
+# #288 — Beheer: Wizards voor een leverancier
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten, Bevinding, Beheer
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/288
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten en is meegekomen bij oplevering Gebruik op 09-12-2025.
+
+### Reactie 3 — @Makkmetp (2025-12-18)
+
+Dit dient nog getest te worden.
+Use case:
+- met zojuist aangemelde gebruikers van leveranciers en van gemeenten
+- met geimporteerde gebruikers van leveranciers en gemeenten.
+
+### Reactie 4 — @Makkmetp (2026-02-04)
+
+✅De wizards Applicatie publiceren, koppeling publiceren, Applicatiegebruik melden en dienst publiceren worden goed getoond.
+
+Applicatiegebruik melden dient inhoudelijk nog getest te worden in combinatie met een gemeentelijk account en een bestaande leverancier.
+
+### Reactie 5 — @markbacker (2026-02-04)
+
+Alle knoppen zijn er. De wizard Applicatiegebruik melden wordt als onderdeel van gebruik getest
diff --git a/issues/289.md b/issues/289.md
new file mode 100644
index 00000000..30d7976c
--- /dev/null
+++ b/issues/289.md
@@ -0,0 +1,43 @@
+# #289 — Beheer: tabelvoorkeuren worden niet bewaard
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten, Bevinding, Beheer
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/289
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten
+
+### Reactie 3 — @markbacker (2025-12-18)
+
+Eerste stap voorkeuren bewaren in de sessie
+In de volgende levering voorkeuren bewaren bij de ingelogde gebruiker
+
+### Reactie 4 — @WilcoLouwerse (2026-01-12)
+
+Hertest (test omgeving) = OK
+
+### Reactie 5 — @Makkmetp (2026-02-03)
+
+✅De voorkeuren worden bewaard. Ook wanneer je uitlogt en inlogt
diff --git a/issues/290.md b/issues/290.md
new file mode 100644
index 00000000..4674e938
--- /dev/null
+++ b/issues/290.md
@@ -0,0 +1,42 @@
+# #290 — Beheer: Contactpersonen zoeken werkt niet
+
+**Status:** CLOSED | **Labels:** Organisatie en configuratie, Afschalen Producten, Bevinding, Beheer
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/290
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten en is meegekomen bij oplevering Gebruik op 09-12-2025.
+
+### Reactie 3 — @Makkmetp (2025-12-18)
+
+@remko48 het is onduidelijk of dit opgelost is. Is dit te testen wanneer de omgeving weer beschikbaar is?
+
+### Reactie 4 — @WilcoLouwerse (2026-01-12)
+
+Hertest (test omgeving) = OK
+
+### Reactie 5 — @Makkmetp (2026-02-04)
+
+✅Het zoeken werkt
diff --git a/issues/291.md b/issues/291.md
new file mode 100644
index 00000000..322252eb
--- /dev/null
+++ b/issues/291.md
@@ -0,0 +1,41 @@
+# #291 — Beheer: Organisatie bewerken via contactpersoon
+
+**Status:** CLOSED | **Labels:** Organisatie en configuratie, Afschalen Producten, Bevinding, Beheer
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/291
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten en is meegekomen bij oplevering Gebruik op 09-12-2025.
+
+### Reactie 3 — @Makkmetp (2025-12-18)
+
+Het KvK Nummer is nog te zien in schema's Organisatie.
+
+Nog te testen via de front-end
+
+### Reactie 4 — @Makkmetp (2026-02-04)
+
+✅KvK is (eindelijk) niet meer vindbaar/zichtbaar
+✅Het bewerken van de organisatie gaat nu goed.
diff --git a/issues/292.md b/issues/292.md
new file mode 100644
index 00000000..7dd2e718
--- /dev/null
+++ b/issues/292.md
@@ -0,0 +1,36 @@
+# #292 — Applicatie publiceren: lijst met onbekende contactpersonen
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten, Bevinding, Wizard
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/292
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten en is meegekomen bij oplevering Gebruik op 09-12-2025.
+
+### Reactie 3 — @Makkmetp (2026-02-04)
+
+✅De lijst met contactpersonen zijn nu alleen van de zojuist opgevoerde organisatie.
+
+Dit dient alleen nog getest te worden met de geïmporteerde organisaties
diff --git a/issues/294.md b/issues/294.md
new file mode 100644
index 00000000..e2166f86
--- /dev/null
+++ b/issues/294.md
@@ -0,0 +1,61 @@
+# #294 — Applicatie publiceren: uitlijning rechthoek om op te voeren.
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten, Bevinding, Wizard
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/294
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (8)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten
+
+### Reactie 3 — @WilcoLouwerse (2026-01-12)
+
+Hertest (test omgeving) = OK
+
+### Reactie 4 — @Makkmetp (2026-02-04)
+
+✅Uitlijning is goed wanneer je geen referentiecomponent hebt geselecteerd.
+❌De uitlijning gaat nog niet goed wanneer je een referentiecomponent hebt geselecteerd.
+
+
+
+Qua gebruiksvriendelijkheid / intuitief gedrag is nog onduidelijk of dit gaat werken.
+
+### Reactie 5 — @rubenvdlinde (2026-02-04)
+
+@remko48 zou je hier nog een keer naar kunnen kijken? Hij zit in de hertest van @WilcoLouwerse op ok maar is volgens @Makkmetp nok
+
+### Reactie 6 — @rubenvdlinde (2026-02-10)
+
+Aangezien dit issue terug is gezet op todo door @Makkmetp heb ik hem ook weer geopend.
+
+### Reactie 7 — @rubenvdlinde (2026-02-11)
+
+Eerste vraag is even werkt dit nu goed op de test omgeving @remko48, als die het niet doet @SudoThijn inschakelen als die het wel doet naar @WilcoLouwerse voor testen.
+
+### Reactie 8 — @Makkmetp (2026-02-23)
+
+✅Ziet er netjes uit.
+
+
diff --git a/issues/295.md b/issues/295.md
new file mode 100644
index 00000000..86d313a8
--- /dev/null
+++ b/issues/295.md
@@ -0,0 +1,42 @@
+# #295 — Applicatie publiceren: Koppeling veld is smal
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten, Bevinding, Wizard
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/295
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten
+
+### Reactie 3 — @WilcoLouwerse (2026-01-12)
+
+Hertest (test omgeving) = OK
+
+### Reactie 4 — @WilcoLouwerse (2026-02-02)
+
+Hertest (accept omgeving) = OK
+
+### Reactie 5 — @Makkmetp (2026-02-04)
+
+✅Dit ziet er nu goed uit.
diff --git a/issues/297.md b/issues/297.md
new file mode 100644
index 00000000..2a1633dd
--- /dev/null
+++ b/issues/297.md
@@ -0,0 +1,39 @@
+# #297 — Applicatie publiceren: koppeling applicatie B niet te selecteren
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten, Bevinding, Wizard
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/297
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten en is meegekomen bij oplevering Gebruik op 09-12-2025.
+
+### Reactie 3 — @markbacker (2025-12-17)
+
+Publiceren applicaties moet correct werken na opschalen product
+
+### Reactie 4 — @Makkmetp (2026-02-04)
+
+✅Er worden meer applicaties getoond dan voorheen. Of het ze allemaal zijn is lastig te achterhalen
+✅De verschillende weergave voor een applicatie of BGV zorgt voor meer onderscheid en duidelijkheid.
diff --git a/issues/298.md b/issues/298.md
new file mode 100644
index 00000000..23db46a7
--- /dev/null
+++ b/issues/298.md
@@ -0,0 +1,38 @@
+# #298 — Applicatie publiceren: Buitengemeentelijke voorzieningen herkenbaarder maken tussen applicaties
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten, Bevinding, Wizard
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/298
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten en is meegekomen bij oplevering Gebruik op 09-12-2025.
+
+### Reactie 3 — @markbacker (2025-12-17)
+
+Publiceren applicaties moet correct werken na opschalen product
+
+### Reactie 4 — @Makkmetp (2026-02-04)
+
+✅Buitengemeentelijke voorzieningen hebben een ander kleur icoon gekregen.
diff --git a/issues/299.md b/issues/299.md
new file mode 100644
index 00000000..024ae398
--- /dev/null
+++ b/issues/299.md
@@ -0,0 +1,34 @@
+# #299 — Beheer: Applicatiedetail Diensten tab kaartje zonder tekst
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten, Bevinding, Beheer
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/299
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten en is meegekomen bij oplevering Gebruik op 09-12-2025.
+
+### Reactie 3 — @Makkmetp (2026-02-04)
+
+✅Het tabblad is niet meer gezien. Hiermee opgelost.
diff --git a/issues/3.md b/issues/3.md
new file mode 100644
index 00000000..cd00e387
--- /dev/null
+++ b/issues/3.md
@@ -0,0 +1,170 @@
+# #3 — Als aanbod-raadpleger wil ik pakketten kunnen zoeken en filteren op ondersteuning van verplichte en aanbevolen standaarden
+
+**Status:** CLOSED | **Labels:** Aanbod, PvE eis, Bevinding, Zoeken
+**Auteur:** @rubenvdlinde | **Datum:** 2025-02-06
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/3
+
+---
+
+## Beschrijving
+
+Zodat ik pakketten kan selecteren die goed samenwerken met de door mij gebruikte pakketten en voorzieningen.
+https://conduction.atlassian.net/browse/VSC-286
+
+---
+
+## Reacties (24)
+
+### Reactie 1 — @github-actions (2025-02-06)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2025-03-19)
+
+Komen deze verplichte standaarden etc uit een referentielijst van GEMMA?
+
+### Reactie 3 — @Makkmetp (2025-04-08)
+
+@rubenvdlinde De verplichte standaarden zitten in de GEMMA gekoppeld aan referentiecomponenten. Bij het selecteren van een referentiecomponent, worden nu meteen de gekoppelde verplichte en/of aanbevolen standaarden getoond.
+
+### Reactie 4 — @rubenvdlinde (2025-04-09)
+
+Check, @remko48 deze komt dan in eerste instantie terug bij het selecteren van referentie componenten bij het aanmaken van een voorziening.
+
+### Reactie 5 — @rubenvdlinde (2025-05-12)
+
+@matthiasoliveiro Oke daar zit die nu in, dus deze hangt op het bijwerken van Open Catalogi op de twee omgevingen zodat het zoeken "aan gaat" en we daar ook visueel kunnen kijken naar resutlaten en wat vindbaar is. In princiepe is het daarmee een kweste van deployen en testen.
+
+@Makkmetp er is een seperaat issue voor het inregelen van de zoekbalk, die blokeerd dan in princiepe het aftesten hiervan (er is immers geen zoek formulier waarmee he kan).
+
+### Reactie 6 — @rubenvdlinde (2025-05-12)
+
+@remko48 de impedement voor dit issue is eigenlijk https://github.com/VNG-Realisatie/Softwarecatalogus/issues/6 waar deze informatie wordt gecreerd, aangezien we de zoekbalk dynamisch opbouwen zou het daarna eigenlijk goed moeten gaan (zoekbalk configuratie daargelaten).
+
+### Reactie 7 — @rubenvdlinde (2025-07-22)
+
+De count bug is gefixed, mar ik zou deze testen na de import
+
+### Reactie 8 — @Makkmetp (2025-07-23)
+
+De filtering lijkt nog niet te werken. Wanneer je zoekt op "test" of "omgevingsloket" dan komen de aantallen nog niet overeen.
+
+
+
+
+
+### Reactie 9 — @remko48 (2025-07-23)
+
+@Makkmetp Dit is opgelost en staat op [vng.opencatalogi.nl/zoeken](https://vng.opencatalogi.nl/zoeken)
+
+
+### Reactie 10 — @Makkmetp (2025-07-23)
+
+Het lijkt nog niet helemaal stabiel te werken.
+Wanneer je via de homepage gaat, zie je minder filters dan dat je direct de URL [vng.opencatalogi.nl/zoeken](https://vng.opencatalogi.nl/zoeken) gebruikt.
+Daarnaast als je nog wat aan en uit klikt, dan stopt het filteren op een gegeven moment. Meerdere filters aanklikken zorgt niet voor minder resultaten.
+
+
+
+
+### Reactie 11 — @Makkmetp (2025-07-23)
+
+Ik zie nu inmiddels de filtering.
+Ik verwacht alleen dat wanneer je een volgend filter gebruikt, dat de resultaten worden verfijnd en het aantal minder wordt. In dit geval verwacht ik maar één zoekresultaat. Bij een aantal filters werkt het wel goed.
+
+
+
+### Reactie 12 — @remko48 (2025-07-23)
+
+Dit zou opnieuw opgelost moeten zijn op vng.opencatalogi.nl
+
+### Reactie 13 — @Makkmetp (2025-07-23)
+
+Bij de filtering van Organisation gaat het nog fout. En ik mis daar de namen van de organisatie. Net als bij de referentiecomponenten.
+
+
+
+### Reactie 14 — @rubenvdlinde (2025-09-03)
+
+Vandaag moeten we even goed door facets heenlopen en kijken wat daar nog op staat, we ik in iedergeval al snel spot op dit mome
+
+- [x] na zoekopdracht worden de facets niet opnieuw opgehaald (@SudoThijn )
+- [x] Na klikken facet word die niet aan de query toegevoegd (@SudoThijn )
+- [x] Lege facets (dus zonder resultaten aka 0) zouden wel getoond moeten worden (@SudoThijn )
+- [x] Null facets (geen opties) zouden niet getoond moeten worden (@SudoThijn )
+
+
+
+### Reactie 15 — @SudoThijn (2025-09-05)
+
+> Lege facets (dus zonder resultaten aka 0) zouden wel getoond moeten worden
+
+Dit lijkt mij een backend probleem sinds facets zonder resultaten niet terug gegeven worden
+
+### Reactie 16 — @rubenvdlinde (2025-09-16)
+
+Yes, dit is het solr propbleem. Solr brengt een volledig nieuwe snellere, betere etc etc facating met zich mee. Maar de omgeving wil maar niet op Solr geraken.
+
+
+### Reactie 17 — @WilcoLouwerse (2025-12-08)
+
+In overleg met @remko48 & @rubenvdlinde besloten dat het punt:
+_"Lege facets (dus zonder resultaten aka 0) zouden wel getoond moeten worden (@SudoThijn )"_
+
+Niet meer relevant is.
+Hoe het nu werkt (dat lege facets, filters die 0 resultaten hebben, niet getoond worden,) is het gewenste gedrag hier.
+
+### Reactie 18 — @Makkmetp (2025-12-16)
+
+Deze heeft een bug aangezien standaarden en versies er nog niet goed in zitten.
+Op de zoekpagina heet het filter Standaarden, maar de resultaten de standaardversies zijn.
+
+### Reactie 19 — @rubenvdlinde (2025-12-17)
+
+Code en index lijkt te klopeen, dus dit gaat nu puur over het goed opvoeren via de wizards. Voor het opvoeren in de wizards is een ander issue #6. Onder standaarden zelf staan de jusite gegevens.
+
+
+
+### Reactie 20 — @Makkmetp (2025-12-18)
+
+Het filter is aanwezig, alleen staan er nog UUID's in. Graag dit oplossen.
+
+
+
+### Reactie 21 — @remko48 (2025-12-18)
+
+Dit is een datafout.
+In gemma zijn er geen standaarden en ook geen standaardversies met de uuid's: `3b4f4757-59d4-4f93-bb36-849ffc17077a` en `94839637-0a98-486f-a85a-694fd438f8e6`
+https://raw.githubusercontent.com/VNG-Realisatie/Softwarecatalogus/refs/heads/documentation/docs/examples/GEMMA_release.xml
+
+Deze niet bestaande standaarden zijn toegevoegd aan de volgende applicaties:
+Onegov
+Djuma OpenInfo
+Centric Leefomgeving
+Rx.Mission
+BIM
+Demo omgevingswet VTH
+LEEF
+Neuron BGT
+Greenpoint
+
+### Reactie 22 — @Makkmetp (2026-01-07)
+
+@markbacker dit is issue gaat o.a.over UUID in de facets. Pak jij deze op?
+
+### Reactie 23 — @markbacker (2026-01-08)
+
+Zie #333. Daar vind je mijn analyse en het is inderdaad een datafout.
+
+Ik ga dit grotendeels oplossen bij het maken van datamigratie .csv bestanden.
+
+### Reactie 24 — @markbacker (2026-02-04)
+
+Datafout voor standaarden is opgelost. Het filteren op standaarden gaat goed.
diff --git a/issues/300.md b/issues/300.md
new file mode 100644
index 00000000..1c45545d
--- /dev/null
+++ b/issues/300.md
@@ -0,0 +1,95 @@
+# #300 — Beheer: overzicht applicaties teveel applicaties
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten, Bevinding, Beheer
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/300
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (10)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten en is meegekomen bij oplevering Gebruik op 09-12-2025.
+
+### Reactie 3 — @Makkmetp (2025-12-18)
+
+@rubenvdlinde ik vermoed dat deze te maken heeft met de RBAC. Is deze inmiddels te testen?
+
+### Reactie 4 — @WilcoLouwerse (2026-01-12)
+
+Hertest (test omgeving) = OK
+
+### Reactie 5 — @Makkmetp (2026-02-04)
+
+✅Het aantal applicaties van een "nieuwe" leveranciers klopt.
+Dit is nog niet getest voor de geimporteerde leveranciers.
+
+### Reactie 6 — @rubenvdlinde (2026-02-16)
+
+@WilcoLouwerse het aantal applicaties voor ene bestaande leverancier is terug te vinden in de import bestanden, zou zou zeggen kies er 5 uit om na te rekenenen waarvan in ieder geval centric.
+
+### Reactie 7 — @Makkmetp (2026-02-24)
+
+❌Het aantal applicaties bij de voorgestelde leverancier Centric komen niet overeen. In de import staan er voor Centric ook 39 applicaties om in te voeren.
+
+In de huidige softwarecatalogus heeft Centric - 39 pakketten opgevoerd,Shift2 - 26 pakketten en Horlings & Eerbeek Automatisering B.V. - 11 pakketten.
+
+In de vernieuwde softwarecatalogus:
+Zonder inloggen heeft:
+❌Centric - 32 applicaties
+✅Shift2 - 26 applicaties
+✅Horlings & Eerbeek Automatisering B.V - 11 applicaties
+
+Ingelogd als gebruiker van de leverancier:
+❌Centric - 32 applicaties inclusief een zelf aangemaakt pakket
+✅Shift2 - 26 applicaties
+✅Horlings & Eerbeek Automatisering B.V - 11 applicaties
+
+Huidige softwarecatalogus:
+
+
+
+
+
+
+Vernieuwde softwarecatalogus:
+
+
+
+
+Ingelogd als gebruiker van de leverancier:
+
+
+
+
+
+
+### Reactie 8 — @markbacker (2026-02-25)
+
+Nagekeken in modules.csv. Er worden 39 door Centric geregistreerde applicaties aangeleverd.
+
+### Reactie 9 — @markbacker (2026-02-25)
+
+Bevinding op ontbreken applicaties verplaatst naar nieuw issue #435
+
+### Reactie 10 — @markbacker (2026-02-25)
+
+Het tonen van teveel applicaties is opgelost
diff --git a/issues/301.md b/issues/301.md
new file mode 100644
index 00000000..074db962
--- /dev/null
+++ b/issues/301.md
@@ -0,0 +1,40 @@
+# #301 — Beheer: overzicht applicaties sorteren werkt niet
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten, Bevinding, Beheer
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/301
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten en is meegekomen bij oplevering Gebruik op 09-12-2025.
+
+### Reactie 3 — @Makkmetp (2025-12-18)
+
+Dient nog getest te worden.
+
+### Reactie 4 — @Makkmetp (2026-02-04)
+
+✅Het sorteren werkt door op de icoontjes naast de kolom titels te klikken.
+
+
diff --git a/issues/302.md b/issues/302.md
new file mode 100644
index 00000000..c653e3ef
--- /dev/null
+++ b/issues/302.md
@@ -0,0 +1,52 @@
+# #302 — Beheer: applicatie bewerken (ophalen van gegevens is traag)
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten, Bevinding, Beheer
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/302
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (6)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten
+
+### Reactie 3 — @rubenvdlinde (2025-12-18)
+
+@Rem-Dam dit is een generieke ebvinding die inderdaad moet worden opgepakt maar niks met de zijstap afschalen producten te maken heeft
+
+### Reactie 4 — @rubenvdlinde (2026-02-16)
+
+@WilcoLouwerse graag op b omgeving perfomance nalopen
+
+### Reactie 5 — @Makkmetp (2026-02-24)
+
+✅Het ophalen van de gegevens is getimed met de hand:
+dit was tussen de 2,5 seconden en de 3,8 seconden voor applicaties van een nieuw opgevoerde leverancier en applicaties en voor een geïmporteerde leverancier met applicaties.
+
+Aan de andere kant kwam er ook een error naar voren bij het ophalen van het applicatie overzicht:
+
+
+
+### Reactie 6 — @markbacker (2026-02-25)
+
+Voor error nieuw issue aangemaakt #436
+Dan kan deze gesloten
diff --git a/issues/303.md b/issues/303.md
new file mode 100644
index 00000000..71627f56
--- /dev/null
+++ b/issues/303.md
@@ -0,0 +1,38 @@
+# #303 — Beheer: applicatie bewerken dienst toevoegen ontbreekt
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten, Bevinding, Beheer
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/303
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten
+
+### Reactie 3 — @Makkmetp (2025-12-18)
+
+Indien dit is opgelost, dient dit nog getest te worden.
+
+### Reactie 4 — @Makkmetp (2026-02-04)
+
+✅Dienst publiceren is toegevoegd.
diff --git a/issues/304.md b/issues/304.md
new file mode 100644
index 00000000..8332d3be
--- /dev/null
+++ b/issues/304.md
@@ -0,0 +1,42 @@
+# #304 — Dienst bewerken: formulier teveel velden
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/304
+
+---
+
+## Beschrijving
+
+Pad ernaar is onbekend. URL is wel bekend.
+https://softwarecatalogus.accept.opencatalogi.nl/beheer/dienst/dcb2c083-94f8-4d8b-bcfe-07c62aabc870
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten en is meegekomen bij oplevering Gebruik op 09-12-2025.
+
+### Reactie 3 — @Makkmetp (2025-12-18)
+
+Indien dit opgelost is, dient dit nog getest te worden.
+
+### Reactie 4 — @Makkmetp (2026-02-04)
+
+✅Deze stap in het formulier komt nu overeen met de ppt.
+Mogelijk staat er wel een andere melding open over de (i)nformatie ballonnen
diff --git a/issues/305.md b/issues/305.md
new file mode 100644
index 00000000..f2f74eca
--- /dev/null
+++ b/issues/305.md
@@ -0,0 +1,38 @@
+# #305 — Dienst: multiselect diensttypen + label aanpassen
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten, Bevinding, Wizard
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/305
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten
+
+### Reactie 3 — @WilcoLouwerse (2026-01-12)
+
+Hertest (test omgeving) = OK
+
+### Reactie 4 — @Makkmetp (2026-02-04)
+
+✅Multiselect is beschikbaar.
diff --git a/issues/306.md b/issues/306.md
new file mode 100644
index 00000000..07ef27c3
--- /dev/null
+++ b/issues/306.md
@@ -0,0 +1,81 @@
+# #306 — Dienst: Overzicht controleren verbeteren
+
+**Status:** OPEN | **Labels:** help wanted, Aanbod, Afschalen Producten, Bevinding, Wizard, Wijziging
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/306
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (11)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten
+
+### Reactie 3 — @rubenvdlinde (2025-12-18)
+
+Ik ga hem nog iets scherper zetten, eigenlijk is dit een observatie over aanbod @Rem-Dam
+
+### Reactie 4 — @markbacker (2025-12-18)
+
+Meenemen in gebruikerstest
+
+### Reactie 5 — @markbacker (2026-02-03)
+
+- [ ] De labels Type en Diensttype zijn gevolg van dubbeling. Houd alleen label diensttype over maar dan met de inhoud van type
+- [ ] De relaties met daaronder aanbieder Pino is overbodig. Je komt alleen op dit scherm als beheerder van Pino
+
+
+
+### Reactie 6 — @rubenvdlinde (2026-02-04)
+
+De dubbele typen zijn een gevolgen van een bsproken wijziging op het datamodel, althans de toevoeging van diensttype die is nu in de configuratie waar aangepast.
+
+Voor Relaties geld dat het onderdeel van het datamodel is, dat hier wordt weergegeven is daar een gevolg van. Dat kunnen we uiteraard aanpakken maar is dan een wijzigings verzoek (en moet ook even met de groep worden besproken) :)
+
+### Reactie 7 — @rubenvdlinde (2026-02-11)
+
+ @remko48 dubbelchecen dat properties die op niet weegeven staan ook niet getoond worden in de basis infromatie.
+
+
+### Reactie 8 — @rubenvdlinde (2026-02-18)
+
+Afpraak: @Makkmetp en @markbacker pakken deze zo even op.
+
+### Reactie 9 — @Makkmetp (2026-02-18)
+
+@rubenvdlinde Graag de overbodige relaties verwijderen. Dat zijn o.a.:
+- de dubbelingen van type en diensttype
+- relaties zijn overbodig aangezien je al op de pagina van de organisatie bent
+
+De gebruikerstest is voor nu buiten scope en voor later met de gemeenten en leveranciers.
+
+### Reactie 10 — @rubenvdlinde (2026-02-20)
+
+De dubbelingen zijn verwijderd, dienstType is afgeschaald
+
+### Reactie 11 — @Makkmetp (2026-02-24)
+
+✅de dubbelingen van type en diensttype - type is niet zichtbaar. Diensttype wel.
+❔ Om welke relaties gaat dit? @markbacker Graag testen
+
+
+
+
diff --git a/issues/307.md b/issues/307.md
new file mode 100644
index 00000000..a7d6de90
--- /dev/null
+++ b/issues/307.md
@@ -0,0 +1,59 @@
+# #307 — Diensten overzicht: meer dienst bij organisatie dan er horen
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten, Bevinding, Beheer, Wijziging
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/307
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (8)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten en is meegekomen bij oplevering Gebruik op 09-12-2025.
+
+### Reactie 3 — @markbacker (2025-12-17)
+
+Na afschalen producten moeten diensten correct te werken. Ook label beheer toegekend voor bijeenhouden van bevindingen op beheertabellen
+
+### Reactie 4 — @WilcoLouwerse (2026-01-12)
+
+Hertest (test omgeving) = OK
+
+_Kolom koppelingen kan nog geselecteerd worden_
+
+### Reactie 5 — @Makkmetp (2026-02-04)
+
+❌De kolom koppelingen kan nog steeds geselecteerd worden.
+
+
+
+### Reactie 6 — @rubenvdlinde (2026-02-04)
+
+Dan gaat deze terug naar refinement, diensten kunnen vanuit het datamodel koppelingen omvaten (is ooit bedacht voor wearefrank geloof ik) dus dan zou het een aanpassing datamodel zijn. Overigens zijn we dan een flink stuk weggelopen bij de oorspronkenlijke titel van dit issue.
+
+### Reactie 7 — @rubenvdlinde (2026-02-16)
+
+@SudoThijn functioneel gezien moet de property koppelingen dan juist geconfigureerd zijn in het datamodel en op omgeving testen. Het zou kunnen zijn dat de config klopt maar ui nog niet goed reageerd.
+
+### Reactie 8 — @Makkmetp (2026-02-25)
+
+✅De kolom koppelingen is niet meer zichtbaar.
+✅Onder het overzicht diensten zie je nu alleen de zelf opgevoerde diensten aantal diensten.
diff --git a/issues/308.md b/issues/308.md
new file mode 100644
index 00000000..a57880bf
--- /dev/null
+++ b/issues/308.md
@@ -0,0 +1,77 @@
+# #308 — Diensten overzicht: default kolommen + kolom verwijderen
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten, Bevinding, Beheer, Wijziging
+**Auteur:** @Makkmetp | **Datum:** 2025-12-08
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/308
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (8)
+
+### Reactie 1 — @github-actions (2025-12-08)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2025-12-10)
+
+Dit is onderdeel van Gebruik en niet van Afschalen Producten
+
+
+
+### Reactie 3 — @remko48 (2025-12-10)
+
+@Makkmetp koppeling kan geselecteerd worden omdat het Datamodel voor dienst opgezet is met koppelingen
+
+### Reactie 4 — @WilcoLouwerse (2026-01-12)
+
+Hertest (test omgeving) = OK
+
+_Kolom koppelingen kan nog geselecteerd worden_
+
+### Reactie 5 — @Makkmetp (2026-02-04)
+
+Ik zie dat het inderdaad in het datamodel staat. @markbacker kan jij bevestigen of dit klopt? Voor mijn gevoel klopt het niet dat we een dienst op een koppeling aanbieden. Deze zijn immers niet op te halen via de zoeklijst van applicaties bij het registreren van een koppeling.
+
+
+
+
+
+### Reactie 6 — @markbacker (2026-02-04)
+
+Het idee hierachter is dat een aanbieder van een applicatie ook expliciet kan aangeven welke koppelingen vanuit de applicatie worden ondersteund.
+Een andere use case is dat een aanbieder zoals EnableU kan vastleggen voor welke koppelingen zij met het product OpenTunnel ondersteuning bieden.
+
+Dit gezegd hebbende, kiezen we er voor nu voor dit niet te doen. Dit houd de wizard Diensten en de beheertabellen overzichtelijk.
+
+Dus graag de kolom koppelingen verbergen, aangezien deze (nog) niet gevuld kan worden.
+
+### Reactie 7 — @rubenvdlinde (2026-02-11)
+
+@remko48 property defintities zijn als het goed is aangepast in config, graag dubbelcheken en dan naar @WilcoLouwerse
+
+### Reactie 8 — @Makkmetp (2026-02-25)
+
+✅Kolom koppelingen is niet meer zichtbaar en kan niet meer geselecteerd worden.
+✅Doordat de indeling van de koppelingen na een eerste keer selecteren blijven staan, is een default begin minder relevant.
+✅Wanneer je bent ingelogd als leverancier, zie je onder diensten alleen je zelf opgevoerde diensten.
+
+---
+
+## Kruisverwijzingen
+
+_Toegevoegd op 2026-03-02 op basis van API-testresultaten._
+
+- **Opgelost via #306:** API-tests voor #308 draaien onder de acceptatiecriteria van #306 (Diensten overzicht verbeteren). Beide issues betreffen het verwijderen van de koppelingenkolom en het instellen van default kolommen.
diff --git a/issues/312.md b/issues/312.md
new file mode 100644
index 00000000..c5affff9
--- /dev/null
+++ b/issues/312.md
@@ -0,0 +1,76 @@
+# #312 — Koppeling heeft verplicht een naam
+
+**Status:** OPEN | **Labels:** Aanbod, Afschalen Producten, Restpunt, Datamigratie, IGS review
+**Auteur:** @markbacker | **Datum:** 2025-12-09
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/312
+
+---
+
+## Beschrijving
+
+Met zoeken vind je koppelingen zonder naam. Enkele worden getoond als UUID, nieuw aangemaakte koppelingen worden getoond met 'Geen titel'
+
+Koppelingen hebben verplicht een naam
+- formulieren/wizard vereisen opgave van
+- naam wordt vooringevuld met een default naam
+- default naam wordt samengesteld uit `naam applicatie A` `pijltje die richting weergeeft` `naam applicatie B`
+
+Importeren van koppelingen
+- als koppeling geen naam heeft, dan krijgt de koppeling bovenstaande default naam
+- anders naam behouden
+
+---
+
+## Reacties (7)
+
+### Reactie 1 — @github-actions (2025-12-09)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @markbacker (2025-12-17)
+
+Nodig voor het correct importeren en publiceren van koppelingen
+
+### Reactie 3 — @rubenvdlinde (2026-02-16)
+
+@WilcoLouwerse er zijn dus twee casusaen waarin dit getes moet worden kopppelingen aanmaken en koppeling importeren
+
+### Reactie 4 — @rubenvdlinde (2026-02-23)
+
+Na veel grafisch wikken en wegen is dit ons voorstel geworden ->
+
+Waarom zo?
+- De A applicatie wordt expliciet uitgelicht
+- De totale koppeling staat in de beschrijving
+- De titel van de koppeling is vanuit de wizard aanpasbaar dus je kan er niet op rekenen dat daar altijd A naar B staat
+
+We realiseren ons dat het issue hiermee TECHNISCH voldaan is, maar dat er nog een gebruikers itteratie overheen mag/moet. Daarvoor is afgelopen twee weken natuurlijk niet de tijd geweest dus we vragen vriendenlijk om van de feedback een los issue aan te maken zodat we itteratief kunnen oppakken
+
+
+
+### Reactie 5 — @markbacker (2026-02-25)
+
+De meest simpele oplossing lijkt mij dat een koppeling altijd de default naam krijgt en dat deze niet aanpasbaar is. De huidige oplossing ziet er nogal dubbelop uit.
+
+❌ In de wizard krijgt een koppeling nu geen default naam. Met de bovenstaande oplossing is dit verholpen.
+
+
+### Reactie 6 — @Makkmetp (2026-02-26)
+
+Zie #432 & #433 voor aanvullende issues.
+Deze gaan over op verschillende plekken verschillende namen &
+de velden van de import lijken niet goed gekoppeld te zijn.
+
+### Reactie 7 — @Makkmetp (2026-03-04)
+
+Meerdere koppelingen missen de applicaties in de kaartjes van de zoekresultaten.
+Graag ervoor zorgen dat deze wel getoond worden in de titel en de omschrijving van de applicatie eronder kan komen te staan.
+
+
diff --git a/issues/314.md b/issues/314.md
new file mode 100644
index 00000000..7ec848f9
--- /dev/null
+++ b/issues/314.md
@@ -0,0 +1,75 @@
+# #314 — Wizard Koppeling publiceren vind zelf aangemaakte applicaties niet
+
+**Status:** OPEN | **Labels:** Aanbod, Bevinding, Koppeling
+**Auteur:** @markbacker | **Datum:** 2025-12-10
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/314
+
+---
+
+## Beschrijving
+
+https://softwarecatalogus.accept.opencatalogi.nl/forms/koppeling?type=eigen-organisatie
+
+In de wizard koppeling publiceren kom ik stap 1 niet voorbij
+- ik zoek naar een door mij aangemaakte applicatie Chaplin3 GemeenteSuite
+- deze staat niet in de lijst, zoeken helpt ook niet
+
+Wel in de lijst staan diverse applicaties, die niet van leverancier Chaplin3 zijn.
+
+Oplossing
+- [x] Aanbieders (leveranciers) kunnen alleen koppelingen aanmaken waarbij ApplicatieA door de leverancier wordt aangeboden.
+- [x] Gebruikers (gemeenten/samenwerkingen) kunnen koppelingen aanmaken voor applicaties die zij gebruiken (die in hun applicatielandschap staan)
+
+
+
+
+---
+
+## Reacties (7)
+
+### Reactie 1 — @github-actions (2025-12-10)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2025-12-11)
+
+Hier spelen twee dingen door elkaar
+
+1. Het zoeken naar applicatie naam werkt kennenlijk niet goed, dat is een bug
+2. De flow blijkt iets anders in elkaar te zitten dan gewenst, dat is een observatie over de wizards
+
+### Reactie 3 — @markbacker (2025-12-17)
+
+Het kunnen publiceren van koppelingen eerst afronden voordat we doorgaan naar gebruik
+
+### Reactie 4 — @rubenvdlinde (2025-12-18)
+
+Het aanmaken van koppelingen voor de eigen organisatie is echt een gebruiks fase issue, geen doorgeschoven aanbod issue
+
+### Reactie 5 — @markbacker (2025-12-18)
+
+Uitgaand van het informatiemodel valt wizard Publiceren koppeling onder Aanbod, de wizard Toevoegen koppeling onder gebruik. Het is verwarrend het onderdeel gebruik voor de fasering te vullen met andere onderdelen.
+
+Waar we het wel over moeten hebben is onder welke oplevering koppelingen vallen.
+
+Om de issues op koppelingen vindbaar te houden voeg ik een label koppeling toe
+
+### Reactie 6 — @remko48 (2026-02-13)
+
+Werkt nu op de B omgeving
+- [x] Aanbieders (leveranciers) kunnen alleen koppelingen aanmaken waarbij ApplicatieA door de leverancier wordt aangeboden.
+
+Werkt nog niet op de B omgeving
+- [x] Gebruikers (gemeenten/samenwerkingen) kunnen koppelingen aanmaken voor applicaties die zij gebruiken (die in hun applicatielandschap staan)
+
+### Reactie 7 — @rubenvdlinde (2026-02-16)
+
+@SudoThijn ik pak deze even over dan :)
+
diff --git a/issues/315.md b/issues/315.md
new file mode 100644
index 00000000..87944af0
--- /dev/null
+++ b/issues/315.md
@@ -0,0 +1,113 @@
+# #315 — Hoge prioriteit: Zoekpagina toont deel van gemeentelijk applicatielandschap. Dit is géén publieke informatie
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten, Datamigratie, Zoeken
+**Auteur:** @markbacker | **Datum:** 2025-12-10
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/315
+
+---
+
+## Beschrijving
+
+De zoekpagina toont in het filter leverancier onterecht ook gemeenten.
+- Door te filteren op een gemeente zie je welke (externe) pakketten deze gemeente in de oude Softwarecatalogus heeft opgevoerd.
+- Zie bijvoorbeeld https://softwarecatalogus.accept.opencatalogi.nl/zoeken?_page=1&%40self%5Borganisation%5D=8c3363d7-2269-49fd-ac71-5b4e9b809ed3
+
+En in de kaartjes van de zoekresultaten worden gemeenten getoond als leverancier van deze applicaties (aangeboden door 'gemeente'). Dit klopt niet en mag niet. De applicaties worden aangeboden door de leverancier.
+
+Oplossing
+- [ ] Filter leverancier moet alleen leveranciers bevatten
+- [ ] Kaartjes met zoekresultaten moeten bij applicaties de 'aanbieder' tonen. Dit is bijna altijd een leverancier.
+
+---
+
+## Reacties (12)
+
+### Reactie 1 — @github-actions (2025-12-10)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2025-12-11)
+
+Deze zijn zichtbaar omdat ze in het import bestand module.csv expliciet op published/publiek vindbaar zijn gezet. Zie ook:
+
+f6d2ce19-cdbb-5bdc-a6db-861892b89fc0,"9a2dd276-e60f-5f06-acd6-d3dea0852836",Platform 073,Platform 073,,,"","","","",ade41f53-7c9b-48c7-8f35-561a770e575f,Closed source,,Applicatie,Applicatie,Gemeente,"8c3363d7-2269-49fd-ac71-5b4e9b809ed3","2014-07-01T16:30:52+00:00"
+
+Als het spoed heeft zou ik ze handmatig kunnen depubliceren.
+
+Het filter leveranciers zal ik even naar kijken, die is gekopeld aan organisatie type dus als daar gemeenten tussen zitten gebeurd er iets vreemds.
+
+
+### Reactie 3 — @markbacker (2025-12-11)
+
+De applicaties mag je ook zien. Je mag alleen niet zien welke gemeente de applicaties gebruiken.
+
+Hoe ik het begrijp is publiceren objecten zichtbaar maken voor anderen dan jezelf. RBAC bepaalt vervolgens of het publiek of besloten is en wie het dan mag zien.
+
+De oplossing om te depubliceren begrijp ik niet
+
+
+### Reactie 4 — @rubenvdlinde (2025-12-15)
+
+Publiceren maakt het inderdaad zichtbaar voor alle andere dan jezelf, het "overruled" dus als het ware de reguliere RBAC.
+
+Maar als ik de tekst hierboven goedlees gaat het om iets anders namenlijk dat de gemeente niet de aanbieder moet zijn van de applicatie (dat is namenlijk het veld dat daar gebruikt wordt) maar dat deze of op een fictieve aanbieder moeten (de beoogde leverancier als dan niet al aanwezig in de software catalogus)
+
+### Reactie 5 — @rubenvdlinde (2025-12-16)
+
+Conduction gaat uitzoeken hoeveel werk het is om het concept publiceren neit langer in te zetten bij de software catalogus
+
+### Reactie 6 — @markbacker (2025-12-16)
+
+Nagekeken of in de data mogelijk aanbieder en @self.organization verkeerd staan.
+Ik zie, zoals verwacht, dat door gemeenten en samenwerking geregistreerde applicaties een andere aanbieder UUID hebben dan in @self.organization
+
+Nagekeken met een instantie uit de Zoekpagina
+- filter Leverancier, selecteer gemeente Aa en Hunze
+- toont applicatie MyExports (aangeboden door Aa en Hunze)
+
+Uit het bestand module.csv
+- gefilterd op id=4f9bf8ce-f14a-5884-bbbb-0b67cf8a0f70 van applicatie MyExports
+- aanbieder wijst naar leverancier
+- @self.organisation wijst naar gemeente f7db0bc8-ec7a-4aa9-902e-51a23f7bce51
+
+Je kunt eigenlijk ook zien op de detailpagina van applicatie. Daar staat wel correct dat de applicatie wordt aangeboden door 'MyExports bv'
+
+Conclusie. De fout zit niet in de data, maar zit ergens anders
+
+### Reactie 7 — @markbacker (2025-12-17)
+
+Nieuwe data met @self.created staat klaar. Voor nu de kolom @self.published laten staan.
+
+Voor testen
+- [ ] handmatig alles depubliceren
+- [ ] hertesten na import met data zonder kolom @self.published
+
+### Reactie 8 — @rubenvdlinde (2025-12-17)
+
+Woot! Ik heb vandaag getest of RBAC gepubliceerd kan afvangen en dat kan dus dan kan deze mee in de volgende deploy moment
+
+### Reactie 9 — @markbacker (2025-12-18)
+
+Voor eerstvolgende import bevat de data nu geen @self.published meer
+
+### Reactie 10 — @WilcoLouwerse (2026-01-13)
+
+Hertest (test omgeving) = OK
+
+_Alleen werkt depubliceren niet, maar ik heb van Remko vernomen dat dit gaat vervallen_
+
+### Reactie 11 — @Makkmetp (2026-03-03)
+
+Via zoeken naar een gemeente en daarna filters te gebruiken is het me niet gelukt om zelf geregistreerde applicaties te vinden van de gemeente, anders dan dat de naam van de gemeente in een applicatie is opgenomen.
+Het is zonder ingelogd te zijn of als leverancier in te loggen wel mogelijk om applicatie te vinden die door gemeenten zijn geregistreerd. Zie daarvoor #394. Deze heeft een relatie met dit issue en daar staat een usecases verder uitgewerkt.
+
+### Reactie 12 — @markbacker (2026-03-04)
+
+Issue kan zo gesloten.
diff --git a/issues/330.md b/issues/330.md
new file mode 100644
index 00000000..68540b52
--- /dev/null
+++ b/issues/330.md
@@ -0,0 +1,40 @@
+# #330 — /Beheer pagina's rerouten
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten, Bevinding, Beheer
+**Auteur:** @Makkmetp | **Datum:** 2025-12-16
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/330
+
+---
+
+## Beschrijving
+
+https://softwarecatalogus.accept.opencatalogi.nl/beheer/ (vb.applicaties of producten) zijn automatisch gegenereerde pagina's waarin een tabel wordt gemaakt en toont waar je rechten tot hebt.
+
+Graag deze optie blokkeren zodat deze niet meer worden gevonden.
+
+Bron voor dit issue is:
+#257
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2025-12-16)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @WilcoLouwerse (2026-01-12)
+
+Hertest (test omgeving) = OK
+
+### Reactie 3 — @Makkmetp (2026-02-03)
+
+✅Getest en werkt
+Bewerken wordt via de wizards uitgevoerd.
diff --git a/issues/332.md b/issues/332.md
new file mode 100644
index 00000000..b812c8ff
--- /dev/null
+++ b/issues/332.md
@@ -0,0 +1,144 @@
+# #332 — Voorpagina inrichten
+
+**Status:** OPEN | **Labels:** help wanted, Organisatie en configuratie, Cms, IGS review
+**Auteur:** @Makkmetp | **Datum:** 2025-12-17
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/332
+
+---
+
+## Beschrijving
+
+Hierbij een voorbeeld van de voorpagina. Deze is zoveel mogelijk gebaseerd op https://open.tilburg.nl
+
+
+
+Het logo heeft een link naar deze voorpagina, zodat je altijd terug kan.
+Rechtsboven:
+- Een link naar het dashboard, uitloggen
+
+Menu-balk:
+- Home verwijst naar de voorpagina
+- De overige menu-items zijn zelf in te richten: naam + URL
+- In de menu balk helemaal rechts je gebruikersnaam en organisatienaam, wanneer je bent ingelogd.
+
+Zoekvenster:
+- Zoekt zonder filtering in de softwarecatalogus conform de rechten die iemand heeft
+- De banner achter het zoekvenster is door de FB aan te passen.
+
+Quote:
+- Dik gedrukte tekst met subtitel. Dit is aan te passen door de FB
+
+3 blokken:
+Elk blok bestaat uit:
+- 1 icoon
+- Titel
+- Tekst (2 a 3 regels)
+- Link naar een andere pagina
+- De afmetingen (hoogte x breedte) van de blokken is gelijk aan elkaar
+De inhoud is door FB in te richten.
+
+Tekst:
+- Titel
+- Tekst
+- Link naar pagina
+- Afbeelding
+De inhoud is door FB in te richten.
+
+Footer:
+- Zoals deze nu is
+De inhoud is door FB in te richten.
+
+---
+
+## Reacties (8)
+
+### Reactie 1 — @github-actions (2025-12-17)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2025-12-18)
+
+@Makkmetp ik neem even aan de VNG hier de teksten voor aanlevert?
+
+### Reactie 3 — @Makkmetp (2025-12-18)
+
+@rubenvdlinde Ip.v de teksten aanleveren willen we de teksten zelf kunnen opvoeren.
+
+### Reactie 4 — @rubenvdlinde (2026-02-12)
+
+Dubleerd met #397
+
+### Reactie 5 — @rubenvdlinde (2026-02-18)
+
+@Makkmetp finale dubbelcheck, de verwachting is dus niet dat de voorkant er zo meteen uitziet als hier boven? (ga ik uiteraard wel een gooi naar doen) maar da tje zelf de teksten kan anpassen?
+
+### Reactie 6 — @rubenvdlinde (2026-02-18)
+
+Afspraak: We leverem zo op dat @Makkmetp kan aanpassen, hoeft inderdaad niet exact te lijken of gelijk te zijn aan voorbeeld hierboven
+
+### Reactie 7 — @rubenvdlinde (2026-02-23)
+
+Handleidingen staan op https://vng-realisatie.github.io/Softwarecatalogus/docs/Handleidingen :)
+
+### Reactie 8 — @Makkmetp (2026-02-25)
+
+✅Een bestaande pagina (Algemene voorwaarden & Privacyverklaring) zijn aan te passen en op te slaan.
+
+Inrichten homepage volgens https://vng-realisatie.github.io/Softwarecatalogus/docs/Handleidingen/pagina-beheer:
+✅Hero-titel en zoekbalk-tekst is aangemaakt in RichText, aan te passen en op te slaan.
+❌Hero-titel en zoekbalk-tekst vertoont na het opslaan HTML-code
+❌Citaat/highlight-sectie onder de hero de content type quote is niet beschikbaar en niet te testen.
+❌Drie kaarten met snelkoppelingen maakt gebruik van het Content type ContentBlocks. Deze is pas beschikbaar vanaf OpenCatalogi versie 0.8.0. De huidige softwarecatalogus draait op OpenCatalogi v0.7.9-beta.6 en nog niet te testen.
+✅ Titel "Over de softwarecatalogus"-sectie - een titel is te plaatsen "Een vernieuwde softwarecatalogus"
+✅ Beschrijving "Over"-sectie - is op te maken en toe te voegen
+✅ Link "Over"-sectie - Een link is toe te voegen
+✅Afbeelding "Over"-sectie - de afbeelding is toe te voegen
+
+❌Wanneer je een Content item van Home hebt bewerkt en daarna opslaat. En daarna het Content type weer wilt bewerken krijg je de eerste keer de melding Content succesfully added. Het bericht wat je ook krijgt bij het opslaan. Klik je weer op edit dan kan je wel bewerken.
+
+❔Elk content item heeft nu dezelfde naam, namelijk Content . Is het mogelijk, zeker bij een homepage om deze namen aan te passen? Dat maakt het navigeren en bewerken een stuk makkelijker.
+
+❌Het naderhand een volgorde met de optie Order aanpassen werkt niet. Op de pagina https://softwarecatalogus.accept.opencatalogi.nl/een-lange-titel-voor-een-testpagina is eerst de FAQ aangemaakt en daarna tekst voor de intro. Hierna is het getal van de order omgedraaid en wordt de volgorde aan de front-end niet aangepast.
+
+❔Voor de netheid bij erg lange titels van een pagina verschuift de ...Actions knop buiten het blok. Het zou netter zijn als deze daarin blijft.
+
+❌Via ...Actions een Copy gemaakt. Wat er gebeurt is dat de geselecteerde pagina wordt overschreven en met de naam "Kopie van..." de originele pagina overschrijft. De slug blijft ook gelijk. Logischer zou zijn dat er gevraagd wordt of de slug aangepast moet worden en onder welke naam de pagina opgeslagen moet worden.
+
+❔Bij het plaatsen van een image zijn nog wat opties mogelijk. Welke zijn dat? In de handleiding kon ik deze niet vinden. En is het ook mogelijk om een afbeelding van de computer te uploaden?
+
+❔Na een pagina te hebben verwijderd is de pagina nog benaderbaar en wordt er geen error terug gegeven, zoals een 404-melding. https://softwarecatalogus.accept.opencatalogi.nl/verwijderen
+
+❔De opties onder security lijken nog niet te werken. Dit is getest door voor de home een content item alleen beschikbaar te maken voor aanbod-beheerders en deze te verbergen voor de login. Deze aanpassing is te zien ook voor bezoekers die niet ingelogd zijn.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/issues/334.md b/issues/334.md
new file mode 100644
index 00000000..ded45b64
--- /dev/null
+++ b/issues/334.md
@@ -0,0 +1,82 @@
+# #334 — Zoeken
+
+**Status:** CLOSED | **Labels:** Aanbod, Afschalen Producten, Bevinding
+**Auteur:** @rubenvdlinde | **Datum:** 2026-01-22
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/334
+
+---
+
+## Beschrijving
+
+Bij het testen van zoeken zijn de volgende bevindingen gevonden
+
+- [X] Zoeken met een filter aan verbergt het filter als je niets vindt (Dit gaat niet over de gehele filters maar over het geselecteerde filter blokje en is opgelost, zie figuur 7)
+- [x] **Bespreekpunt** Je ziet nu gebruik in de publieke zoeklijst (Nee je ziet door gemeente geregistreerde applicaties)
+- [x] **Bespreekpunt** Alleen ingelogd als afnemer (gemeente of samenwerking) zie je de door afnemers toegevoegde applicaties en leveranciers (volgen het RBAC model is alleen gebruik beveiliegd)
+- [x] **Bespreekpunt** Leveranciers zien de voor hen bedoelde suggesties (de door gemeente geregistreerde applicaties zijn eigenlijk suggesties)
+- [X] UUID's in leveranciers
+- [X] Maar 50 referentiecomponenten
+- [X] Standaarden niet compleet en UUIF
+- [x] Tekstueel (Organisatietype, Referentiecomponenten, Standaardversies)
+- [x] Tweede woord zonder haoodletter in labes
+- [x] **Bespreekpunt** Figuren 5 Contact persoonen gemeenten publiek zichtbaar (dat klopt, nu we publiceren hebben afgeschaald gaat RBAC op objecten. Het object contacterpsoon is in princiepe openbaar)
+- [ ] Figuur 6 Er kan worden geklikt op items die niet doorzoekbaar zijn of geen pagina hebben (contactpersonen en koppelingen)
+
+Bespreek punten
+- Filters geven bewust niks terug als er niks te filteren is, dat staat ook in de tekst uitgelegd
+- Volgen het RBAC model is alleen gebruik beveiliegd, waar hier over word gesproken is dat je gebruik kan afleiden. Dat klopt, maar dat is een gevolg van publiseren afschalen en combinatie met het opgezete RBAC model
+
+Figuur 1
+
+
+Figuur 2
+
+
+Figuur 3
+
+
+Figuur 4
+
+
+Figuren 5
+
+
+
+Figuur 6
+
+
+Figuur 7
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2026-01-22)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @markbacker (2026-01-22)
+
+'_Nav bespreek punten
+ Filters geven bewust niks terug als er niks te filteren is, dat staat ook in de tekst uitgelegd_'
+
+Punt hier is dat je nu ook niet meer ziet welke filters actief zijn, waardoor je ze dus ook niet meer kunt uitzetten. Het uitgangspunt is dat je als gebruiker altijd ziet welke filters aanstaan.
+
+
+### Reactie 3 — @rubenvdlinde (2026-01-28)
+
+Om een of andere reden heier terecht gekomen, maar van moet dus nog een keus maken over wel/geen pagina voor applicatie versies / koppelingen / contactpersonen. Voor al deze object typen staan pagina's klaar.
+
+Persoonlijk ben ik fan van consistentie. Dus ik zou zeggen voor alles een pagina.
+
+### Reactie 4 — @markbacker (2026-02-04)
+
+Fout in figuur 6 staat in ander issue. Daarmee wordt deze geaccepteerd
diff --git a/issues/337.md b/issues/337.md
new file mode 100644
index 00000000..35053ec4
--- /dev/null
+++ b/issues/337.md
@@ -0,0 +1,66 @@
+# #337 — Applicatie melden wizard
+
+**Status:** CLOSED | **Labels:** Aanbod, Bevinding, Beheer
+**Auteur:** @rubenvdlinde | **Datum:** 2026-01-22
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/337
+
+---
+
+## Beschrijving
+
+Bij het testen van zoeken zijn de volgende bevindingen gevonden
+
+- [X] **Figuur 1** De applicatie melden wizard toont Facility X ipv BGV's (Dat is in Fiuur 2 opgelost, zat hem in gemma inladen)
+- [X] Figuur 3 Eerste keer laden zonder (verplichte) standaardversies (dit is in Figuur 4 opgelsot)
+- [X] Figuur 5 Wizard labels (check allemaal) (dit is in figuur 6 opgelost)
+- [x] Fiiguur 7 laden duurt erg lang
+
+Figuur 1
+
+
+Figuur 2
+
+
+Figuur 3
+
+
+Figuur 4
+
+
+Figuur 5
+
+
+Figuur 6
+
+
+Figuur 7
+
+
+
+---
+
+## Reacties (2)
+
+### Reactie 1 — @github-actions (2026-01-22)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @Makkmetp (2026-02-04)
+
+Bij het testen van zoeken zijn de volgende bevindingen gevonden
+
+- [X] **Figuur 1** De applicatie melden wizard toont Facility X ipv BGV's (Dat is in Fiuur 2 opgelost, zat hem in gemma inladen)
+✅Dit is getest en niet meer gezien.
+- [X] Figuur 3 Eerste keer laden zonder (verplichte) standaardversies (dit is in Figuur 4 opgelsot)
+✅Dit is opgelost door de volgorde van laden aan te passen en dit onder de 1,5 seconden te doen vanaf het moment dat je start met het formulier.
+- [X] Figuur 5 Wizard labels (check allemaal) (dit is in figuur 6 opgelost)
+✅Deze zijn overgenomen vanuit de ppt. Er kunnen nog wel andere issues over open staan.
+- [x] Fiiguur 7 laden duurt erg lang
+✅Kan inderdaad beter.
diff --git a/issues/340.md b/issues/340.md
new file mode 100644
index 00000000..c9029864
--- /dev/null
+++ b/issues/340.md
@@ -0,0 +1,111 @@
+# #340 — Bevindingen op tussenoplevering Zoeken
+
+**Status:** OPEN | **Labels:** Zoeken, Wijziging
+**Auteur:** @markbacker | **Datum:** 2026-01-23
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/340
+
+---
+
+## Beschrijving
+
+## Performance
+- **Opbouw filters** is te langzaam.
+- **Zoeken op tekst en opbouw van filters** verloopt traag:
+ - Zoeken op "jeugd": lijst verschijnt na ~2 seconden, filters worden bijgewerkt na ~4 seconden (soms langer).
+ - Terug naar volledige lijst: lijst verschijnt na ~3 seconden, filters meteen.
+ - Zoeken op "pim": lijst en filters binnen ~3 seconden.
+ - Terug naar volledige lijst: lijst binnen ~3 seconden, filters na ~10 seconden.
+- **Filter 'Geregistreerd door' op leveranciers**: lijst verschijnt na ~1 seconde, filters na ~7 seconden.
+
+## Sorteren
+- Standaard sortering is nog **Naam - A naar Z**.
+- Toelichting nodig bij **“Meest relevant”**.
+- Toelichting nodig op **datum van sortering**:
+ - Datum zou zichtbaar moeten zijn op de kaartjes (was in een vorige versie aanwezig).
+ - Gebruik **'Eerste registratie' (@self.created)** als datum, beschikbaar voor alle objecttypen.
+- Sorteren na **zoeken op tekst** werkt niet:
+ - Wijzigen van sortering (bijvoorbeeld A naar Z of Z naar A) heeft geen effect.
+
+## Filters
+- Filter **Schema** is verdwenen: gebruik voor dit filter het label **Type** (minder technisch dan schema of objecttype, maar blijft helaas wel vaag. Open voor suggesties).
+
+## Bugs
+- Filter aanzetten en vervolgens zoeken op tekst: de indicatie welk filter actief is, verdwijnt.
+
+## Tekstuele aanpassingen
+- Filter **Soort dienst** hernoemen naar **Diensttype** (zoals in de wizard Dienst).
+
+
+---
+
+## Reacties (7)
+
+### Reactie 1 — @github-actions (2026-01-23)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Op het hernoemen van soort dienst na staa tdit nu allemaal live op -> https://performance.accept.opencatalogi.nl/zoeken?_page=1
+
+
+### Reactie 3 — @rubenvdlinde (2026-02-03)
+
+Naar onze herinering was de datum op de kaartjes vroeger de published date en werd die als onduidenlijk ervaren en op verzoek verwijderd. We kunnen daar uiteraard created.date neerzetten maar dan is het een (klein) wijzigings verzoek.
+
+### Reactie 4 — @markbacker (2026-02-03)
+
+Klopt, en ik leverde in de csv-bestanden in published date de created date. Met het uitzetten van publiceren hebben we in overleg besloten de kolom self@published te verwijderen en daarvoor in de plaats self@created aan te gaan leveren om te gaan gebruiken.
+
+Geen wijziging, maar een gevolg van het uitzetten van publiceren.
+
+
+### Reactie 5 — @rubenvdlinde (2026-02-13)
+
+Het uitzetten van publiceren was een wijziging, op verzoek van VNG. Die nu tot een reeks aan obeservaties de gevolg wijzigingen leid. Niet te min, zo als al vaker aangegeven properen we hier coulant mee om te gaan zodat het project daadwerkenlijk af komt. @remko48 zou jij of @SudoThijn het datum veld kunnen omzetten naar published? (Ik ga er even vanuit dat dit een klein ding is).
+
+
+### Reactie 6 — @rubenvdlinde (2026-02-23)
+
+Even opletten, we hebben mondeling afgesproken dat sorteeren standaar A-Z moet zijn omdat "Meest relevant" onduidenlijk is. Mocht uit de test naar voren komen dat het toch wat ander moet zijn dan kan dat nog niet handmatig worden aangepast.
+
+### Reactie 7 — @Makkmetp (2026-02-26)
+
+**@markbacker kan jij deze testen?** Gebruik 'Eerste registratie' (@self.created) als datum, beschikbaar voor alle objecttypen.
+
+
+Voor de rest zijn alle dezelfde tests uitgevoerd en is er voor alle tests qua tijd een verbetering:
+
+**Performance**
+Zoeken op "jeugd": was ~2 seconden nu 1,5 seconden✅ , filters worden bijgewerkt na ~4 seconden (soms langer) -nieuwe test ~3 seconden ✅
+Terug naar volledige lijst: lijst verschijnt na ~3 seconden - nu binnen ~1,5 seconden ✅, filters meteen.
+Zoeken op "pim": lijst en filters binnen ~3 seconden. Nu binnen 1.5 seconden ✅
+Terug naar volledige lijst: lijst binnen ~3 seconden. Nu binnen ~1.5 seconden ✅, filters na ~10 seconden. Filter nu direct aanwezig. ✅
+Filter 'Geregistreerd door' op leveranciers: lijst verschijnt na ~1 seconde. Is gelijk. ✅, filters na ~7 seconden.Filters direct aanwezig.✅
+
+**Sorteren**
+Standaard sortering is nog Naam - A naar Z. ✅Sortering is Naam - A naar Z
+Toelichting nodig bij “Meest relevant”. ❌ Er is geen toelichting
+Toelichting nodig op datum van sortering: ❌ Er is geen toelichting
+Datum zou zichtbaar moeten zijn op de kaartjes (was in een vorige versie aanwezig). ✅Datum is aanwezig
+
+**@markbacker kan jij deze testen?** Gebruik 'Eerste registratie' (@self.created) als datum, beschikbaar voor alle objecttypen.
+
+Sorteren na zoeken op tekst werkt niet:
+Wijzigen van sortering (bijvoorbeeld A naar Z of Z naar A) heeft geen effect. ✅Sortering na zoeken op A naar Z & Z naar A werkt.
+
+**Filters**
+Filter Schema is verdwenen: gebruik voor dit filter het label Type (minder technisch dan schema of objecttype, maar blijft helaas wel vaag. Open voor suggesties). ✅Het filter is aangepast naar Type
+
+**Bugs**
+Filter aanzetten en vervolgens zoeken op tekst: de indicatie welk filter actief is, verdwijnt. ✅De filters blijven aanwezig.
+
+**Tekstuele aanpassingen**
+Filter Soort dienst hernoemen naar Diensttype (zoals in de wizard Dienst). ✅Diensttype is nu aanwezig
diff --git a/issues/343.md b/issues/343.md
new file mode 100644
index 00000000..8a85cb5b
--- /dev/null
+++ b/issues/343.md
@@ -0,0 +1,67 @@
+# #343 — Zoeken: Filter 'Type koppeling' toevoegen.
+
+**Status:** OPEN | **Labels:** Aanbod, Gebruik, Zoeken, Koppeling, Wijziging
+**Auteur:** @markbacker | **Datum:** 2026-01-28
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/343
+
+---
+
+## Beschrijving
+
+Voor nu even alle filtering op de zoekpagina in dit issue. Later indien gewenst sub-issues van maken
+
+- [ ] Filter 'Type koppeling' toevoegen.
+ - De waarden zijn _extern_ en _intern_
+ - aangeleverd in koppeling.csv, attribuut `koppelingType`
+
+Komt uit #282
+
+---
+
+## Reacties (6)
+
+### Reactie 1 — @github-actions (2026-01-28)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Deze waarden zijn inderdaad onderdeel van de cvs import maar, geen onderdeel van de formulier ontwerpen van 11 december. Hoe zo dit gegeven voor nieuwe koppelingen tot stand moeten komen? Betreft het een wijziging op de koppelingen wizard of het toeveogen van een afgeleid gegeven?
+
+We stellen voor pas wijzigingen door te voeren als afschalen producten is afgerond.
+
+### Reactie 3 — @Makkmetp (2026-01-29)
+
+Het is een afgeleid gegeven net als in de huidige softwarecatalogus:
+- Extern is een koppeling met een buitengemeentelijke voorziening
+- Intern zijn de andere koppeling
+
+### Reactie 4 — @Rem-Dam (2026-02-02)
+
+Laten we het issue zuiver houden. Dus de vraag hier gaat om filtering op specifieke gegevens. Alleen wat geconstateerd wordt is dat er geen input mogelijkheid is voor die gegevens. Dat graag in een apart issue zetten en ons inziens is dat een wijzigingsverzoek waar we conform aanbesteding dat proces moeten doorlopen.
+
+### Reactie 5 — @rubenvdlinde (2026-02-21)
+
+Oke, paar dingen hier over
+
+**Koppelingen vallen onder de volgende RBAC regels**
+ - gebruik-beheerder kan alle koppelingen lezen
+ - aanbod-beheerder kan alleen koppelingen lezen waarvan _organisation of aanbieder gelijk zijn aan hun eigen organisatie
+ Dat betekend dat de funtionaliteit van de zoekpagina welicht afwijkt van wat je zou verwachten. **Je moet ingelogd zijn om koppelingen** (en dus ook dit filter te zien). Hoe je bent ingelog bepaald dan ook nog eens welke koppelingen je te zien krijgt. Dat is dus een gevolg van de RBAC keuze over koppelingen.
+
+Dat betkend dus dat het gedrag tijdens testen kan afwijken van wat je (logischerwijze) als gebruiker zou verwachten maar in lijn is met andere issues. Als het gedrag moet worden aangepast dan moeten we daar even over refinen :)
+
+### Reactie 6 — @Makkmetp (2026-03-02)
+
+@markbacker Pak jij deze op om te testen?
+Het facets gedeelte wordt btw nu wel erg complex met wat er getoond wordt voor een gebruiker:
+Leverancier > 283 resultaten
+Organisatietype > Leverancier >2658 resultaten
+Geregistreerd door > Leverancier 1419 resultaten
diff --git a/issues/344.md b/issues/344.md
new file mode 100644
index 00000000..238770ad
--- /dev/null
+++ b/issues/344.md
@@ -0,0 +1,35 @@
+# #344 — Zoeken: Geen resultaten bij het selecteren van het Gravenbeheercomponent. Niet ingelogd.
+
+**Status:** OPEN | **Labels:** Aanbod, Wijziging
+**Auteur:** @Makkmetp | **Datum:** 2026-01-28
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/344
+
+---
+
+## Beschrijving
+
+Wanneer je via de Home > Organisaties gaat en daar het Gravenbeheercomponent selecteert bij de referentiecomponenten, dan zijn er 0 resultaten. Dit terwijl er wel resultaten horen te zijn. Er zijn namelijk leveranciers die een applicatie met dit referentiecomponent hebben opgevoerd.
+
+https://softwarecatalogus.accept.opencatalogi.nl/zoeken?%40self%5Bschema%5D=16&referentieComponenten%5B%5D=6d928d61-febe-4994-9b20-3df92ad1cf6c&_page=1
+
+
+
+
+---
+
+## Reacties (2)
+
+### Reactie 1 — @github-actions (2026-01-28)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie ook slide 3, Dit klopt, dit is ontstaan omdat het filter voor schema’s is uitgezet. Deze staat nu de b omgeving weer aangezet (beide op verzoek) en daarmee is de bevinding ook verdwenen.
diff --git a/issues/345.md b/issues/345.md
new file mode 100644
index 00000000..bcda1fb2
--- /dev/null
+++ b/issues/345.md
@@ -0,0 +1,57 @@
+# #345 — Zoeken: toegevoegde dienst verschijnt niet in filters
+
+**Status:** OPEN | **Labels:** Aanbod, Zoeken
+**Auteur:** @Makkmetp | **Datum:** 2026-01-28
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/345
+
+---
+
+## Beschrijving
+
+Leverancier Pino heeft een de dienst Pino Services toegevoegd
+
+Zoeken met filters gaat niet
+- Diensttypes niet aangevuld
+- Filter Type=Dienst ontbreekt
+
+Via onbekende waarde 'eigen-organisatie' komt Pino wel naar boven
+- Met 6 resultaten ipv 5.
+
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2026-01-28)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie ook slide 5, : "eigen-organisatie" is een tet configuratie die per abuis op de acceptatie omgeving terecht is gekomen, het niet goed vindbaar zijn van diensten is een technische fout die inmiddels is opgelost op de B omgeving.
+
+### Reactie 3 — @Makkmetp (2026-02-26)
+
+✅Diensttype is nu beschikbaar om op te filteren bij het zoeken.
+
+
+
+Na filteren op Leverancier: Centric:
+
+
+Na filter op Leverancier: Centric & Diensttype:Applicatiebeheer
+
+
+Na filter op:
+Leverancier: Centric & Type: Dienst
+Deze vormgeving is anders dan de voorgaande. Hiervoor is issue #438 aangemaakt.
+
diff --git a/issues/346.md b/issues/346.md
new file mode 100644
index 00000000..1b49716b
--- /dev/null
+++ b/issues/346.md
@@ -0,0 +1,35 @@
+# #346 — Zoeken: paginering werkt niet
+
+**Status:** OPEN | **Labels:** Aanbod, Zoeken
+**Auteur:** @Makkmetp | **Datum:** 2026-01-28
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/346
+
+---
+
+## Beschrijving
+
+- Filter op standaardversie=WCAG (78 resultaten)
+- Sortering default gelaten (nu meest relevant)
+- Bladeren tussen pagina 1,2,3 en 4 toont dezelfde applicaties
+
+
+
+
+---
+
+## Reacties (2)
+
+### Reactie 1 — @github-actions (2026-01-28)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie ook slide 6, hier is inderdaad echt een fout in de applicatie geschoten gedurende de perfomance refactor. Deze is inmiddels opgelost en op de B omgeving doet de paginering het weer.
diff --git a/issues/347.md b/issues/347.md
new file mode 100644
index 00000000..28afc6e7
--- /dev/null
+++ b/issues/347.md
@@ -0,0 +1,35 @@
+# #347 — Zoeken: Dienstkaartje toont array
+
+**Status:** OPEN | **Labels:** Aanbod, Wijziging
+**Auteur:** @Makkmetp | **Datum:** 2026-01-28
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/347
+
+---
+
+## Beschrijving
+
+- De ondersteunde diensttypen worden als array getoond. Graag deze zonder array tonen
+- En wat wordt bedoelt met concept?
+
+
+
+---
+
+## Reacties (2)
+
+### Reactie 1 — @github-actions (2026-01-28)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie ook slide 7, de status concept komt uit het datamodel maar het weergeen van arrays was een grafische fout. Hierdoor zijn de diensttypen wel leesbaar en bruikbaar. Alleen niet netjes
+
+Deze is op de B Omgeving inmiddels opgelost.
diff --git a/issues/348.md b/issues/348.md
new file mode 100644
index 00000000..55ba0885
--- /dev/null
+++ b/issues/348.md
@@ -0,0 +1,41 @@
+# #348 — Het aantal standaarden komen niet overeen bij Centric Begraven tussen de huidige softwarecatalogus en de nieuwe
+
+**Status:** OPEN | **Labels:** Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-01-28
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/348
+
+---
+
+## Beschrijving
+
+- https://softwarecatalogus.accept.opencatalogi.nl/publicatie/45003fe7-3a1c-520e-bbee-8eb3c212c657
+- https://www.softwarecatalogus.nl/pakket/centric-begraven
+Tussen het aantal standaarden bij Centric Begraven op de huidige softwarecatalogus en de vernieuwde zit een verschil in hoeveel er getoond worden in de vernieuwde. In beide situatie was er niet ingelogd.
+
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2026-01-28)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie ook slide 8, Dit zullen we verder moeten uitzoeken, we hebben vorige week met @markbacker een aantals stekproeven gedaan en die zagen er goed uit.
+
+Dat gezegd hebbende is datamigratie een apart perceel en daarmee geen onderdeel van producten afschalen.
+
+### Reactie 3 — @rubenvdlinde (2026-02-03)
+
+Dit bleek een bug in de weergave te zijn.
diff --git a/issues/349.md b/issues/349.md
new file mode 100644
index 00000000..26746021
--- /dev/null
+++ b/issues/349.md
@@ -0,0 +1,87 @@
+# #349 — Zoeken: UUID’s onder standaarden filter.
+
+**Status:** OPEN | **Labels:** Referentiearchitectuur, Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-01-28
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/349
+
+---
+
+## Beschrijving
+
+Kan Mark niet verklaren. Is geen geldig Object ID.
+Dit is de standaard. Wordt hier een Archi id getoond?
+
+
+
+
+---
+
+## Reacties (7)
+
+### Reactie 1 — @github-actions (2026-01-28)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie ook slide 10, hier zullen we even in moeten duiken. Gaan we ook doen maar is onderdeel van perceel datamigratie en niet van aanbod.
+
+### Reactie 3 — @rubenvdlinde (2026-02-03)
+
+Dit lijkt op de B omgeving niet meer voor te komen, maar wel controlleren nog
+
+### Reactie 4 — @markbacker (2026-02-04)
+
+Ik zie dat de sortering van het filter standaardversies rond de 'id-' in de soep loopt. Hier gebeurt iets raars.
+
+
+
+
+
+### Reactie 5 — @rubenvdlinde (2026-02-16)
+
+@WilcoLouwerse dubbelchecken op de b omgeving
+
+### Reactie 6 — @rubenvdlinde (2026-02-21)
+
+Dit speelt nu op niet meer, er zijn geen id-verwijzingen meet in referntiecomponenten en/of standaardversies
+
+
+
+Wel zijn er 4 applicaties die naar een niet bestaande standaard versie (c2eded21-8db0-11e3-67ab-0050568a6153) verwijzen
+
+
+
+
+
+**Even achtergrond (Hoe de koppeling werkt)**
+Modules verwijzen naar GEMMA-referentiecomponenten via het veld referentieComponenten in de module CSV. Dit veld bevat één of meerdere kommagescheiden UUIDs. Die UUIDs komen overeen met de Object ID (propid-2) van ApplicationComponent-elementen in het GEMMA ArchiMate XML-bestand.
+
+De keten is dus:
+`module.referentieComponenten → GEMMA element (propid-2 / Object ID) → ApplicationComponent`
+
+**Over de UUID c2eded21-8db0-11e3-67ab-0050568a6153**
+Deze UUID bestaat niet in de aangeleverde GEMMA_reactivated.xml. Hij komt ook niet voor als identifier, Object ID (propid-2), of Object ID sync (propid-57) van enig element. De UUID is dus verouderd (hoogstwaarschijnlijk een element dat in een eerdere GEMMA-versie bestond maar in de reactivated versie is verwijderd of hernummerd).
+
+Wat opvalt: alle 4 modules uit de schermafbeelding (Geoboxx, Gouw6 BAG, GT-BAG, UDS BAG) verwijzen in de CSV naar 65eaff18-c9e2-4f97-83b1-d9fb3aa366ad, wat de huidige BAG-beheercomponent is in de GEMMA. Die bestaat wél gewoon. Het lijkt er dus op dat er in de database van de huidige softwarecatalogus applicatie nog een oudere verwijzing naar c2eded21 bestaat die niet (meer) in de CSV-export zit (mogelijk een directe databasewaarde die bij het heractiveren van standaarden in de reactivated GEMMA over het hoofd is gezien).
+
+### Reactie 7 — @Makkmetp (2026-03-02)
+
+❌Onder standaardversies staan nog een aantal UUID's:
+
+
+
+---
+
+## Kruisverwijzingen
+
+_Toegevoegd op 2026-03-02 op basis van API-testresultaten._
+
+- **Gerelateerd aan #333:** Onderdeel van het bredere UUID-in-filters probleem (#333). #333 betreft de datamigratie-correctie; #349 is het standaarden-specifieke deel.
diff --git a/issues/350.md b/issues/350.md
new file mode 100644
index 00000000..94f90fe6
--- /dev/null
+++ b/issues/350.md
@@ -0,0 +1,38 @@
+# #350 — De link achter de gebruikersnaam laten verwijzen naar Mij account
+
+**Status:** CLOSED | **Labels:** Aanbod, Wijziging
+**Auteur:** @Makkmetp | **Datum:** 2026-01-28
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/350
+
+---
+
+## Beschrijving
+
+- Het verwijzen vanuit de gebruikersnaam bovenaan naar mijn account is logischer i.p.v. naar het dashboard. Dat staat er namelijk al naast. Graag dit aanpassen.
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2026-01-28)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie ook slide 12, Dat lijkt ons een goed idee :) Dus pakken we graag op
+
+Deze variant is echter binnen de specs, en we stellen voor pas wijzigingen door te voeren als afschalen producten is afgerond.
+
+### Reactie 3 — @Makkmetp (2026-02-23)
+
+✅De link verwijst naar Mijn account en werkt.
diff --git a/issues/351.md b/issues/351.md
new file mode 100644
index 00000000..d82841b8
--- /dev/null
+++ b/issues/351.md
@@ -0,0 +1,48 @@
+# #351 — Het laden van de tabbladen gaat ongelijk
+
+**Status:** OPEN | **Labels:** Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-01-28
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/351
+
+---
+
+## Beschrijving
+
+Het laden van de standaarden bij een applicatie gaat erg ongelijk.
+
+- Standaarden en Geschikt voor staan er direct
+- Organisaties een paar tellen later
+- En weer een paar tellen later applicatie versies en koppelingen
+
+Dit is erg storend.
+Reden hiervoor is dat er verschillende bronnen worden geraadpleegd.
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2026-01-28)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie ook slide 13, Dit heeft te maken met een configuratie fout op de acceptie omgeving. Hoewel het "technisch" altijd zo is dat tabbladen ongelijk laden zou dit dusdanig snel moeten zijn dat de gebruiker hier geen last van heeft. Daarbij is het lastig om "last" te defineren en meetbaar te maken.
+
+Op dit moment laden de tabladen in de B omgeving in een halve seconde (zie ook https://performance.accept.opencatalogi.nl/publicatie/02359ffa-11f8-5ead-9a1e-f22513123956, mind you omgeving zonder garanties) de vraag is even of dat voldoende is ofdat we aanvullende maatregelen moeten nemen. Het is in ieder geval ruim binnen de kader van de performance criteria
+
+
+
+### Reactie 3 — @Makkmetp (2026-02-26)
+
+✅Het laden gaat sneller en de tabbladen zijn korter na elkaar beschikbaar.
+❔Na een video opname zag ik het pas, maar eerst wordt er voor Applicatieversie eerst een "cirkel met een i" erin geladen, alvorens snel over te springen naar het "grafiek icoon". Is dat een bewuste keuze? Het zorgt voor extra onrust in het laden van de pagina.
+
+
diff --git a/issues/352.md b/issues/352.md
new file mode 100644
index 00000000..be2b761c
--- /dev/null
+++ b/issues/352.md
@@ -0,0 +1,81 @@
+# #352 — Mijn account - Contactpersoon bij applicatie publiceren is niet veranderd ondanks aanpassing zojuist.
+
+**Status:** OPEN | **Labels:** Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-01-28
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/352
+
+---
+
+## Beschrijving
+
+De wijzigingen waren
+
+- hoofdletters bij voor en achternaam
+- een tussenvoegsel toegevoegd
+
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2026-01-28)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie ook slide 15 uit de powerpoint, er is een oversight in de backend logica geslopen. Vanuit de mijn gegevens pagina wordt de user acount bijgewerkt in Nextcloud. Bij het bijwerken van de user acount wordt echter niet meteen ook het contactpersoon bijgewerkt terwijl het contactpersoon object wordt gebruikt voor alle weergaven.
+
+Hoewel dit onderdeel is van reeds geacapteerde fase 1, en sinds dien niet aangepast hebben we hem uiteraard wel opgepakt, opgelast en de B omgeving klaargezet.
+
+### Reactie 3 — @Makkmetp (2026-02-26)
+
+De volgende aanpassingen zijn gedaan via Mijn account > Bewerken:
+- tussenvoegsel toegevoegd > "van"
+- getal achter achternaam geplaatst > "1"
+
+
+
+Overzicht van Applicaties:
+❌Na enkele keren refreshen, meer dan een minuuut wachten is de naam niet aangepast in de kolom Contactpersoon.
+
+
+Applicatie publiceren:
+✅Bij contactpersonen is de naam gewijzigd.
+
+
+Diensten overzicht:
+❌Na enkele keren refreshen, meer dan een minuuut wachten is de naam niet aangepast in de kolom Contactpersoon..
+
+
+
+Dienst publiceren:
+✅Bij contactpersonen is de naam gewijzigd.
+
+
+
+Naam van ingelogde gebruiker:
+✅De naam van de ingelogde gebruiker is bovenaan aangepast
+
+
+
+My account:
+✅De gegevens zijn aangepast.
+
+
+
+Overzicht Contactpersoon:
+❌Het tussenvoegsel ontbreekt. Zie #367
+
+
+
+
diff --git a/issues/353.md b/issues/353.md
new file mode 100644
index 00000000..a18f90cf
--- /dev/null
+++ b/issues/353.md
@@ -0,0 +1,48 @@
+# #353 — Mijn account – Je “functie” wordt niet aangepast na bewerken en opslaan. Cache legen werkt ook niet
+
+**Status:** CLOSED | **Labels:** Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-01-28
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/353
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2026-01-28)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie ook slide 16, Dit is dezelfde bevinding als onder -> https://github.com/VNG-Realisatie/Softwarecatalogus/issues/352
+
+### Reactie 3 — @Makkmetp (2026-02-26)
+
+✅De functie van een gebruiker wordt na het wijzigen opgeslagen onder My Account.
+
+
+
+
+
+
+
+---
+
+## Kruisverwijzingen
+
+_Toegevoegd op 2026-03-02 op basis van API-testresultaten._
+
+- **Zelfde oorzaak als #352:** Dezelfde onderliggende bug als #352 (contactpersoon wordt niet bijgewerkt vanuit Nextcloud-account). Opgelost met org-scoped contactpersoon lookup.
diff --git a/issues/354.md b/issues/354.md
new file mode 100644
index 00000000..80afd87b
--- /dev/null
+++ b/issues/354.md
@@ -0,0 +1,66 @@
+# #354 — Diensten - incomplete lijst applicaties
+
+**Status:** OPEN | **Labels:** Aanbod, Wijziging, IGS review
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/354
+
+---
+
+## Beschrijving
+
+Kan voor de dienst niet selecteren uit alle applicaties, slechts een (vreemde/willekeurige) subset. De oplossing zoals bij de referentiecomponenten lijkt een goede oplossing.
+
+
+
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie ook slide 18, technisch werkt deze dropdown en kan je er in zoeken naar applicaties. Praktisch kan de vormgeving inderdaad beter en is het voorstel om de oplossing van referentie componenten over te nemen een goed voorstel.
+
+Deze variant is echter binnen de specs, en we stellen voor pas wijzigingen door te voeren als afschalen producten is afgerond.
+
+### Reactie 3 — @Makkmetp (2026-02-24)
+
+❌ De gisteren opgevoerde Applicatie SaaS is niet vindbaar in wizard Dienst publiceren. Via het overzicht van Applicaties > ...Acties > Dienst publiceren kan er wel een dienst toegevoegd worden aan de applicatie. De gisteren opgevoerde applicaties "korf" en "bal" zijn wel vindbaar. Voor ons is onduidelijk wat de reden hiervoor kan zijn.
+
+
+
+
+
+
+
+
+
+### Reactie 4 — @markbacker (2026-02-25)
+
+Het zoeken met de dropdown naar applicaties gaat zowel in de Diensten als bij het zoeken naar applicatieB van koppelingen onvoorspelbaar en onduidelijk
+
+Zoeken op chap levert eerst niets op. Zoeken op chap1 toont dan wel chap1. Daarna zoeken op chap toont wel chap1, maar nog steeds niet chap2prem.
+
+Komt er op neer dat je pas vind wat je zoekt als je exact weet hoe die heet. Dat is geen zoeken ...
+
+### Reactie 5 — @Makkmetp (2026-02-26)
+
+❌ Onder de leverancier Fortuna is een applicatie opgevoerd genaamd SaaS. Deze wordt niet gevonden, wanneer je de wizard Dienst publiceren opent en zoekt naar de applicatie. Ook niet na langere tijd wachten. Op deze manier is er geen Dienst aan toe te voegen. Andere applicaties worden wel gevonden, maar dat is vaak na het twee keer proberen.
+
+
+
+Via applicatie overzicht > ...Acties > Dienst publiceren is het wel mogelijk om een dienst toe te voegen aan deze applicatie.
+
+
+
diff --git a/issues/355.md b/issues/355.md
new file mode 100644
index 00000000..ff433942
--- /dev/null
+++ b/issues/355.md
@@ -0,0 +1,65 @@
+# #355 — Diensten: Export geeft allerlei UUID's
+
+**Status:** CLOSED | **Labels:** Aanbod, Wijziging
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/355
+
+---
+
+## Beschrijving
+
+De export vanuit het overzicht Diensten geeft allerlei UUID’s i.p.v. teksten. Voor het exporteren en importeren werkt dat, maar voor de leesbaarheid van een gebruiker niet.
+
+Er zijn meerdere opties denkbaar:
+- 1 leesbare optie zonder UUID's en een export en import optie met UUID's
+- Of een combinatie met leesbare tekst en UUID's, welke ook geëxporteerd en geïmporteerd kan worden.
+Zie #15
+
+
+
+---
+
+## Reacties (6)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie ook slide 19 uit de powerpoint, het zou voor de gebruikers vriendenlijkheid inderdaad een goede stap zijn om ook een niet uuid variant te maken.
+
+Deze variant is echter binnen de specs, en we stellen voor pas wijzigingen door te voeren als afschalen producten is afgerond.
+
+### Reactie 3 — @Makkmetp (2026-01-29)
+
+De oplossing is de combinatie met leesbare tekst en UUID's, welke ook geëxporteerd en geïmporteerd kan worden.
+Net als in de huidige softwarecatalogus
+
+### Reactie 4 — @rubenvdlinde (2026-02-12)
+
+Deze dubleerd met #15
+
+### Reactie 5 — @rubenvdlinde (2026-02-19)
+
+Export is aangepast conform wens
+
+### Reactie 6 — @Makkmetp (2026-02-26)
+
+Issue gaat verder in #15.
+Dit issue wordt gesloten omdat deze dubbel is, maar niet geaccepteerd.
+
+---
+
+## Kruisverwijzingen
+
+_Toegevoegd op 2026-03-02 op basis van API-testresultaten._
+
+- **Duplicaat van #15:** #355 is een duplicaat van #15 (export). Beide vereisen leesbare namen naast UUIDs in exports.
diff --git a/issues/356.md b/issues/356.md
new file mode 100644
index 00000000..70299b6f
--- /dev/null
+++ b/issues/356.md
@@ -0,0 +1,48 @@
+# #356 — Diensten: geen tussenvoegsel bij namen
+
+**Status:** CLOSED | **Labels:** Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/356
+
+---
+
+## Beschrijving
+
+Ondanks het opvoeren van een tussenvoegsel van een naam, wordt deze niet getoond in het overzicht in de kolom Contactpersoon.
+
+Geen tussenvoegsel bij Peter “de” Steam
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie ook slide 20, Dit is dezelfde bevinding als onder -> https://github.com/VNG-Realisatie/Softwarecatalogus/issues/352
+
+### Reactie 3 — @Makkmetp (2026-02-25)
+
+✅In de kolom Contactpersoon van het overzicht van Diensten gaat het nu goed.
+
+
+
+---
+
+## Kruisverwijzingen
+
+_Toegevoegd op 2026-03-02 op basis van API-testresultaten._
+
+- **Zelfde oorzaak als #352:** Dezelfde onderliggende bug als #352 (tussenvoegsel niet zichtbaar in diensten-overzicht). Oorzaak: contactpersoon-sync.
diff --git a/issues/357.md b/issues/357.md
new file mode 100644
index 00000000..432e3072
--- /dev/null
+++ b/issues/357.md
@@ -0,0 +1,58 @@
+# #357 — Diensten: Diensttype en Type wordt door elkaar gebruikt
+
+**Status:** OPEN | **Labels:** Aanbod, IGS review
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/357
+
+---
+
+## Beschrijving
+
+De termen Diensttype en Type worden door elkaar gebruikt en geven verwarring wat nu wat is.
+De term is Diensttype voor de rollen Functioneel beheer, technisch beheer, etcetera.
+Graag consistentie in brengen en de term eigen-organisatie verwijderen of niet tonen.
+
+
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie ook slide 21, Dit betreft een configuratie fout over wat er in de tabellen wordt getoond, we zijn daarbij vergeten technische properties te verbergen waardoor deze terug komen in de overzichts tabbelen. We hebben dit inmiddels aangepast in de B omgeving
+
+### Reactie 3 — @rubenvdlinde (2026-02-20)
+
+We hebben overal all .... Type er uitgehaald en faceting gerefactord om aan de hand van het oorspronklenlijke datamodel te werken.
+
+### Reactie 4 — @Makkmetp (2026-02-23)
+
+Nu komt de term Soort dienst opeens naar voren. Deze lijkt een dubbeling van Diensttype aangezien de vulling hetzelfde is.
+
+
+
+### Reactie 5 — @markbacker (2026-02-24)
+
+In het Schema Diensten in Openregister staan nog steed Type en Diensttype onder elkaar. Dit is een potentiele bron voor fouten.
+
+Sterker, het filter Organisatietype toont ook waarden van type van Diensten.
+
+**Fout filter**
+
+
+
+**Dubbeling in OpenRegister**
+
+
diff --git a/issues/358.md b/issues/358.md
new file mode 100644
index 00000000..5b252074
--- /dev/null
+++ b/issues/358.md
@@ -0,0 +1,66 @@
+# #358 — Diensten: De status "Concept" wordt nog op verschillende plekken getoond
+
+**Status:** CLOSED | **Labels:** Aanbod, Wijziging
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/358
+
+---
+
+## Beschrijving
+
+De term status:Concept komt op meerdere plekken voor, zoals in de Wizard, zoekresultaten en het overzicht.
+Graag deze verwijderen of niet tonen. Dit lijkt nog onderdeel te zijn van publiceren.
+
+
+
+---
+
+## Reacties (7)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie ook slide 22, de status concept is onderdeel van het data model en een direct gevolg van een eis uit de aanbesteding dat dingen als concept moeten kunnen worden neergezet.
+
+We stellen voor pas wijzigingen door te voeren als afschalen producten is afgerond.
+
+### Reactie 3 — @Makkmetp (2026-01-29)
+
+@markbacker en @Makkmetp bespreken.
+
+### Reactie 4 — @Makkmetp (2026-01-29)
+
+We hebben het niet in het PvE terug kunnen vinden. Deze graag verwijderen.
+
+De enige PvE met concept erin naast de architectuur concepten zijn:
+
+
+
+### Reactie 5 — @rubenvdlinde (2026-02-03)
+
+Dan zit die alleen in het datamodel en moet die daaruit worden gehaald inderdaad
+
+### Reactie 6 — @rubenvdlinde (2026-02-12)
+
+@remko48 deze is verwijderd uit het datamodel, ook graag verwijderen uit voorkant.
+
+### Reactie 7 — @Makkmetp (2026-02-25)
+
+✅Concept komt niet voor in de kolommen bij het overzicht van je diensten
+✅Concept staat niet meer op de kaartjes wanneer je een applicatie hebt geselecteerd in de wizard Dienst publiceren
+✅In de zoekresultaten is de term Concept ook verdwenen van de kaartjes.
+
+
+
+
+
diff --git a/issues/359.md b/issues/359.md
new file mode 100644
index 00000000..6fe6310f
--- /dev/null
+++ b/issues/359.md
@@ -0,0 +1,57 @@
+# #359 — Diensten wizard: Uw dienst publiceren - tekst aanpassen
+
+**Status:** CLOSED | **Labels:** Aanbod, Tekstuele wijzigingen
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/359
+
+---
+
+## Beschrijving
+
+De tekst achter de “i” bij het zoeken naar Applicaties komt niet overeen met de powerpoint. Graag deze aanpassen naar de tekst uit de powerpoint van 17-12-2025.
+
+
+
+
+---
+
+## Reacties (6)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Dit klopt, dit heeft vermoedenlijk te maken met het reseten van de omgeving op maandag ochtend. We hebben vrijdag wel alle teksten op de omgeving aangepast maar niet in de configuratie. Vermoedenlijk zijn die daarna dus weer overschreven en hebben we dat maandag bij het doorlopen niet gezien. We zijn inmiddels begonnen met het doorlopen van alle teksten in de configuratie.
+
+Overgens gebruiken we geen powerpoints voor wijzigingen en gaan we conform de mail van 22 jan van @markbacker uit van de issues [#316](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/316) tot en met [#328](https://github.com/VNG-Realisatie/Softwarecatalogus/issues/328) voor textuele wijzigingen.
+
+### Reactie 3 — @rubenvdlinde (2026-02-03)
+
+Dit is geen regressie deze teksten zaten al in de wizards toen ze werden geacepteerd. En in de fasen daarna. Maar zijn inderdaad nog niet in lijn met de laatste powerpoint.
+
+Enfin, wordt nu doorgevoerd.
+
+### Reactie 4 — @rubenvdlinde (2026-02-11)
+
+@remko48 dubbelchecken en dan naar @WilcoLouwerse
+
+### Reactie 5 — @WilcoLouwerse (2026-02-17)
+
+
+
+De tekst zoals aangegeven in de powerpoint^
+
+### Reactie 6 — @Makkmetp (2026-02-25)
+
+✅ De tekst komt overeen met de tekst in de powerpoint
+
+
diff --git a/issues/360.md b/issues/360.md
new file mode 100644
index 00000000..ac32b5ac
--- /dev/null
+++ b/issues/360.md
@@ -0,0 +1,41 @@
+# #360 — Diensten wizard – Uw dienst publiceren: Meerdere i komen niet overeen met ppt
+
+**Status:** CLOSED | **Labels:** Aanbod, Tekstuele wijzigingen
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/360
+
+---
+
+## Beschrijving
+
+Alle i(nformatie) in stap 2 bij het opvoeren van de gegevens van een dienst, komen niet overeen met de powerpoint verstuurd op 17-12-2025.
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie ook #359 voor verdere tekst en uitleg
+
+### Reactie 3 — @WilcoLouwerse (2026-02-17)
+
+De tekst hiervoor zoals beschreven in de powerpoint:
+
+
+### Reactie 4 — @Makkmetp (2026-02-25)
+
+✅De tekst staat nu onder "i" bij de labels conform de powerpoint.
diff --git a/issues/361.md b/issues/361.md
new file mode 100644
index 00000000..16289f3c
--- /dev/null
+++ b/issues/361.md
@@ -0,0 +1,40 @@
+# #361 — Diensten wizard – Uw dienst publiceren: inconsistentie in labels
+
+**Status:** CLOSED | **Labels:** Aanbod, Tekstuele wijzigingen
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/361
+
+---
+
+## Beschrijving
+
+De labels op het controle formulier komen niet overeen met de labels van de invoervelden. Graag de labels van de wizards uit de powerpoint van 17-12-2025 gebruiken.
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie ook https://github.com/VNG-Realisatie/Softwarecatalogus/issues/359 voor verdere tekst en uitleg. Dit is geen regressie deze teksten zaten al in de toen de fase werd geacepteerd. Maar zijn inderdaad nog niet in lijn met de laatste powerpoint.
+
+Enfin, wordt nu doorgevoerd.
+
+### Reactie 3 — @Makkmetp (2026-02-25)
+
+✅De velden op het controle formulier komen nu overeen met de labels in de wizard
+
+
diff --git a/issues/362.md b/issues/362.md
new file mode 100644
index 00000000..97dcbfce
--- /dev/null
+++ b/issues/362.md
@@ -0,0 +1,42 @@
+# #362 — Diensten wizard – Uw dienst publiceren: onlogische tekst bovenaan de aanmeld-stap
+
+**Status:** CLOSED | **Labels:** Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/362
+
+---
+
+## Beschrijving
+
+Het is niet logisch dat bovenaan de pagina “Dienst succesvol aangemeld” van het formulier de tekst van “Uw diensten publiceren” staat. De dienst is dan namelijk aangemeld. Graag deze tekst daarboven verwijderen.
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Goed idee, maar we stellen voor pas wijzigingen door te voeren als afschalen producten is afgerond.
+
+### Reactie 3 — @rubenvdlinde (2026-02-11)
+
+@remko48 graag mondeling aan @SudoThijn overdragen
+
+### Reactie 4 — @Makkmetp (2026-02-25)
+
+✅De tekst “Uw diensten publiceren” wordt niet meer getoond wanneer de dienst succesvol is aangemeld.
+
+
diff --git a/issues/363.md b/issues/363.md
new file mode 100644
index 00000000..3d17c40c
--- /dev/null
+++ b/issues/363.md
@@ -0,0 +1,42 @@
+# #363 — Diensten wizard – Uw dienst publiceren: catalogus i.p.v. softwarecatalogus
+
+**Status:** CLOSED | **Labels:** Aanbod, Tekstuele wijzigingen, Wijziging
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/363
+
+---
+
+## Beschrijving
+
+In de tekst in het groen wordt gesproken over catalogus i.p.v. softwarecatalogus. Graag dit aanpassen naar softwarecatalogus.
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Goed idee, maar we stellen voor pas wijzigingen door te voeren als afschalen producten is afgerond.
+
+### Reactie 3 — @rubenvdlinde (2026-02-11)
+
+@SudoThijn sowiezo ook even een code search doen voor catalogus i.p.v. softwarecatalogus en software catalogus i.p.v. softwarecatalogus (dus met onterechte spatie)
+
+### Reactie 4 — @Makkmetp (2026-02-25)
+
+✅Catalogus is aangepast naar softwarecatalogus
+
+
diff --git a/issues/364.md b/issues/364.md
new file mode 100644
index 00000000..449d6655
--- /dev/null
+++ b/issues/364.md
@@ -0,0 +1,50 @@
+# #364 — Contactpersonen: e-mailadres is leeg
+
+**Status:** OPEN | **Labels:** Aanbod, IGS review
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/364
+
+---
+
+## Beschrijving
+
+Ondanks dat de gebruiker net is aangemaakt, is het e-mailadres leeg. Graag ervoor zorgen dat deze is gevuld met het opgevoerd e-mailadres.
+
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Dit is dezelfde bevinding als onder -> https://github.com/VNG-Realisatie/Softwarecatalogus/issues/352
+
+### Reactie 3 — @Makkmetp (2026-02-24)
+
+✅Het mailadres is nu aanwezig in formulier.
+❌Het bewerken van het mailadres is niet mogelijk. Deze geeft bij het opslaan een 400-error. Zie daarvoor issue #365
+
+
+
+
+
+---
+
+## Kruisverwijzingen
+
+_Toegevoegd op 2026-03-02 op basis van API-testresultaten._
+
+- **Zelfde oorzaak als #352:** Dezelfde onderliggende bug als #352 (e-mailadres leeg na aanmaken contactpersoon). Oorzaak: contactpersoon-sync.
diff --git a/issues/365.md b/issues/365.md
new file mode 100644
index 00000000..d3041820
--- /dev/null
+++ b/issues/365.md
@@ -0,0 +1,60 @@
+# #365 — Contactpersonen: error bij het opslaan van een contactpersoon
+
+**Status:** OPEN | **Labels:** Aanbod, IGS review
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/365
+
+---
+
+## Beschrijving
+
+Na het bewerken van een bestaand contactpersoon wordt 400-error getoond bij het opslaan.
+
+
+
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Dit betreft inderdaad een bug die onderdeel is van de fase organisatie (niet aanbod), hij is inmiddels opgelost op de B omgeving.
+
+### Reactie 3 — @Makkmetp (2026-02-03)
+
+Dit gebeurt alleen wanneer je een rol hebt geselecteerd.
+
+
+### Reactie 4 — @rubenvdlinde (2026-02-12)
+
+Dubleir met #73 en #65
+
+### Reactie 5 — @Makkmetp (2026-02-24)
+
+❌De bug is nog steeds aanwezig. Nu is het een 400-error wanneer je een e-mailadres aanpast en deze opslaat.
+
+
+
+
+
+
+
+---
+
+## Kruisverwijzingen
+
+_Toegevoegd op 2026-03-02 op basis van API-testresultaten._
+
+- **Duplicaat van #73, #65:** #365 is een specifieke bug (400-fout bij opslaan) binnen de bredere scope van #65 (gebruikersbeheer) en #73 (contactpersonen beheer).
diff --git a/issues/366.md b/issues/366.md
new file mode 100644
index 00000000..60626367
--- /dev/null
+++ b/issues/366.md
@@ -0,0 +1,62 @@
+# #366 — Contactpersonen: veld Rollen niet consistent
+
+**Status:** OPEN | **Labels:** Aanbod, Wijziging, IGS review
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/366
+
+---
+
+## Beschrijving
+
+Via het formulier is als leverancier een gebruiker toegevoegd.
+Het veld rollen heeft weinig toegevoegde waarde aangezien het altijd Aanbod beheerders voor leveranciers zijn.
+
+Wanneer ik daarna geen rol selecteer en dan bij de gebruiker in de Back-end kijk, heeft deze gebruiker wel een rol gekregen. In de front-end niet en deze worden ook niet getoond in het overzicht.
+- Graag het veld Rollen niet tonen bij leveranciers. Straks wel bij gemeenten aangezien daar wel meer rollen zijn.
+- Graag zorgen dat de back-end en front-end hetzelfde tonen welke rol iemand heeft.
+
+Andere oplossingen zijn ook welkom, maar dan wel graag eerst bespreken.
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie slide 27, Dit betreft een wijzigings verzoek, maar aangezien deze rechtstreeks gerelateerd is aan RBAC en daarme de veiligheid van de applicatie stellen we voor deze wél op te pakken binnen het boven geschetste voorstel. Wel zouden we hem dan graag nog even refinnen.
+
+
+
+### Reactie 3 — @Makkmetp (2026-02-24)
+
+✅Het veld rollen is niet meer zichtbaar in het formulier.
+✅Aangezien het veld niet meer zichtbaar is, is niet te controleren of op beide plekken dezelfde rol staat.
+❔Het refinen voor de RBAC zoals aangegeven dient nog te gebeuren.
+
+
+
+
+
+### Reactie 4 — @markbacker (2026-02-25)
+
+Bij een leverancier is het veld rollen niet meer zichtbaar.
+
+❌ Bij een gemeente zie je wel een veld rollen en daar gaat het mis.
+- nieuwe gebruiker krijgt geen rol
+- bewerken van een gebruiker kan ik een rol geven, maar deze wordt niet opgeslagen en is niet meer terug te vinden.
+- je kan kiezen uit de onbekende rol 'organisatie-beheerder'
+
+
diff --git a/issues/367.md b/issues/367.md
new file mode 100644
index 00000000..bb055b77
--- /dev/null
+++ b/issues/367.md
@@ -0,0 +1,60 @@
+# #367 — Contactpersonen: Tussenvoegsel wordt niet getoond
+
+**Status:** OPEN | **Labels:** Organisatie en configuratie, Aanbod, IGS review
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/367
+
+---
+
+## Beschrijving
+
+Het opgevoerde tussenvoegsel wordt niet getoond bij Contactpersonen
+
+
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie ook slide 27, Dit is dezelfde bevinding als onder -> https://github.com/VNG-Realisatie/Softwarecatalogus/issues/352
+
+### Reactie 3 — @Makkmetp (2026-02-24)
+
+❌Dit is niet opgelost. Dezelfde fout zit er nog steeds in.
+
+
+
+
+
+### Reactie 4 — @markbacker (2026-02-25)
+
+ik zie wel een tussenvoegsel
+
+
+
+### Reactie 5 — @Makkmetp (2026-02-25)
+
+Voor de verduidelijking:
+in de kolom naam staat de voornaam & achternaam zonder tussenvoegsel. Het tussenvoegsel staat wel in de kolom tussenvoegsel.
+Graag de kolom Naam aanpassen, zodat alleen de voornaam getoond wordt.
+
+---
+
+## Kruisverwijzingen
+
+_Toegevoegd op 2026-03-02 op basis van API-testresultaten._
+
+- **Zelfde oorzaak als #352:** Dezelfde onderliggende bug als #352 (tussenvoegsel niet getoond in contactpersonen-overzicht). Oorzaak: contactpersoon-sync.
diff --git a/issues/368.md b/issues/368.md
new file mode 100644
index 00000000..10f0713e
--- /dev/null
+++ b/issues/368.md
@@ -0,0 +1,41 @@
+# #368 — Applicatie publiceren: Zonder een richting aan te geven is de koppeling op te voeren
+
+**Status:** CLOSED | **Labels:** Aanbod, Wijziging
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/368
+
+---
+
+## Beschrijving
+
+Ondanks dat het veld verlicht zou zijn, is het mogelijk om zonder een richting aan te geven de koppeling op te voeren.
+Er werd al nagedacht dat "Richting" een mogelijk default waarde is.
+Graag zorgen dat "Richting" geen default waarde meer is en dat daar altijd één van de volgende waarden opgevoerd moet worden:
+<->Bi-directioneel
+A -> B
+B -> A
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie slide 30, er miste inderdaad een validatie regel op dit veld in de wizard. Die validatie regel mist overigens als zinds de aller eerste versie van de wizard (dus voortschreidend inzicht). Maar we hebben deze inmiddels opgepakt en neergezet op de B omgeving.
+
+### Reactie 3 — @Makkmetp (2026-02-23)
+
+✅Zonder het opgeven van een richting is het aanmaken van een koppeling niet mogelijk.
diff --git a/issues/369.md b/issues/369.md
new file mode 100644
index 00000000..11357956
--- /dev/null
+++ b/issues/369.md
@@ -0,0 +1,50 @@
+# #369 — Applicatie publiceren: de aangemaakte koppeling is niet zichtbaar
+
+**Status:** CLOSED | **Labels:** Aanbod, Koppeling
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/369
+
+---
+
+## Beschrijving
+
+Een aangemaakte koppeling via de wizard Applicatie publiceren is nog niet zichtbaar in het overzicht van de koppelingen.
+
+
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie slide 31, Dit was RBAC bug, is inmiddels opgelost op de B omgeving
+
+### Reactie 3 — @Makkmetp (2026-02-23)
+
+✅ De zojuist opgevoerde koppelingen zijn zichtbaar in het overzicht van Koppelingen.
+Één koppeling is aangemaakt tijdens het aanmaken van de applicatie en daarna aangevuld.
+De andere koppeling Bal <> Mijn PGB Portaal is via de koppelingen wizard aangemaakt.
+
+Alleen nog te testen met een geïmporteerde organisatie
+
+
+
+### Reactie 4 — @Makkmetp (2026-02-23)
+
+✅Voor een geïmporteerde organisatie met een nieuw aangemaakte gebruiker is dit opgelost.
+
+### Reactie 5 — @markbacker (2026-02-24)
+
+Akkoord
diff --git a/issues/370.md b/issues/370.md
new file mode 100644
index 00000000..d2f4726b
--- /dev/null
+++ b/issues/370.md
@@ -0,0 +1,74 @@
+# #370 — Applicatie: teveel kolommen worden getoond
+
+**Status:** OPEN | **Labels:** Organisatie en configuratie, Aanbod, Wijziging
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/370
+
+---
+
+## Beschrijving
+
+In de lijst om kolommen te tonen op de overzichtspagina van Applicaties worden o.a.
+Type
+Applicatietype
+Omvat
+Onderdeel van
+Beoordelingen
+Kwetsbaarheden getoond.
+Geregistreerd door
+
+getoond. Deze betekenis van deze velden is niet duidelijk, lijken dubbel of zijn een experiment en daarom graag verbergen.
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie slide 33, Dit is gewoon het datamodel waarin deze objecten en de daarna verwijzende properties bestaan, maar we hebben hem aangepast en naar de B omgeving gebracht.
+
+### Reactie 3 — @Makkmetp (2026-02-03)
+
+Algemene stelregel:
+Beheertabellen – alleen kolommen met data die via de wizards kan beheren.
+
+Controleren voor
+- Applicaties
+- Diensten
+- Koppelingen
+
+
+### Reactie 4 — @Makkmetp (2026-02-23)
+
+✅De genoemde kolommen "Type, Applicatietype, Omvat, Onderdeel van, Beoordelingen, Kwetsbaarheden getoond, Geregistreerd door" worden niet meer getoond.
+
+Er zijn nog wel punten die aandacht vragen voor dit overzicht:
+❔Een applicatieversie van een SaaS applicatie wordt niet direct toegevoegd. Op een later moment wel. Hoe werkt dit?
+❌Compliance: het is onduidelijk wat daar hoort te staan.
+❔De naam van een koppeling staat niet volledig op het overzicht. Even later wel. Hoe werkt dit?
+❔UUID is zichtbaar bij een koppelingnaam. Paar minuten later is dit hersteld. Hoe werkt dit?
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/issues/371.md b/issues/371.md
new file mode 100644
index 00000000..36792318
--- /dev/null
+++ b/issues/371.md
@@ -0,0 +1,50 @@
+# #371 — Applicatie: UUID onder compliance
+
+**Status:** OPEN | **Labels:** Aanbod, Wijziging, IGS review
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/371
+
+---
+
+## Beschrijving
+
+In het overzicht wordt een UUID getoond onder Compliance. Wat wordt er bedoelt met deze kolom? Hoe wordt er omgegaan wanneer een applicatie compliance is aan meerdere standaarden?
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie slide 35, Dit is een gevlog van het datamodel (compliance objecten hebben geen naam), maar we hebben hem aangepast op de B omgeving door hem niet meer weer te geven (ipv daarvan de kollom standaardVersies).
+
+### Reactie 3 — @Makkmetp (2026-02-23)
+
+❌De kolom Compliance is nog steeds zichtbaar en nu met één of meerdere keren de naam van de applicatie.
+
+
+
+En bij een geïmporteerde leverancier is de kolom gevuld met een "-".
+
+
+
+---
+
+## Kruisverwijzingen
+
+_Toegevoegd op 2026-03-02 op basis van API-testresultaten._
+
+- **Vervallen door #430:** Vervallen: kolom verwijderd conform #430 (Compliancy kolom verwijderen uit beheertabel).
diff --git a/issues/372.md b/issues/372.md
new file mode 100644
index 00000000..5c5fe391
--- /dev/null
+++ b/issues/372.md
@@ -0,0 +1,49 @@
+# #372 — Applicaties: Kolom Contactpersoon toont geen tussenvoegsel
+
+**Status:** CLOSED | **Labels:** Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/372
+
+---
+
+## Beschrijving
+
+De kolom contactpersoon toont geen tussenvoegsel in het overzicht.
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie slide 36, Dit is dezelfde bevinding als onder -> https://github.com/VNG-Realisatie/Softwarecatalogus/issues/352
+
+### Reactie 3 — @Makkmetp (2026-02-23)
+
+✅Het tussenvoegsel wordt getoond in het applicatie overzicht en de selectielijst van contactpersonen in de wizard Applicatie publiceren
+
+
+
+### Reactie 4 — @markbacker (2026-02-24)
+
+Akkoord
+
+---
+
+## Kruisverwijzingen
+
+_Toegevoegd op 2026-03-02 op basis van API-testresultaten._
+
+- **Duplicaat van #352:** Duplicaat van #352. Dezelfde onderliggende bug: contactpersoon wordt niet bijgewerkt vanuit Nextcloud-account.
diff --git a/issues/373.md b/issues/373.md
new file mode 100644
index 00000000..7614357e
--- /dev/null
+++ b/issues/373.md
@@ -0,0 +1,58 @@
+# #373 — Applicatie: Gekoppelde diensten worden niet getoond
+
+**Status:** OPEN | **Labels:** Aanbod, IGS review
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/373
+
+---
+
+## Beschrijving
+
+In het applicatie overzicht van een leverancier worden de opgevoerde diensten niet getoond.
+Wat gaat er gebeuren wanneer een leverancier 10 verschillende diensten opvoert voor één applicatie?
+Graag de diensten tonen.
+Alternatief is een link te tonen met het aantal diensten. De link gaat naar de https://softwarecatalogus.accept.opencatalogi.nl/beheer/diensten?_search= en toont alle diensten die bij de applicatie horen.
+
+Graag bespreken wat de opties zijn.
+
+
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie slide 37, er bleek voor deze tabel inderdaad een weergave optie te missen voor diensten. Die hebben we inmiddels toegevoegd aan de B omgeving. Deze miste overigens ook al toen de fase Aanbod werdt geaccepteerd.
+
+### Reactie 3 — @rubenvdlinde (2026-02-09)
+
+Dubbleur met #377
+
+### Reactie 4 — @Makkmetp (2026-02-23)
+
+❌ Ondanks dat een dienst al eerder is opgevoerd, is deze niet zichtbaar in het overzicht bij de applicaties.
+
+
+
+
+
+### Reactie 5 — @Makkmetp (2026-02-25)
+
+
+
+
+
+
+
diff --git a/issues/374.md b/issues/374.md
new file mode 100644
index 00000000..bcff8774
--- /dev/null
+++ b/issues/374.md
@@ -0,0 +1,74 @@
+# #374 — Applicaties: Standaarden, Standaarden GEMMA en Standaardversies?
+
+**Status:** CLOSED | **Labels:** help wanted, Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/374
+
+---
+
+## Beschrijving
+
+In het overzicht van de applicaties staan de kolommen of deze kunnen geselecteerd worden:
+Standaarden, Standaarden GEMMA en Standaardversies
+Wat is het verschil tussen de eerste twee? Standaarden is leeg. Deze graag niet tonen al dit niets toevoegd.
+En de term Standaarden GEMMA graag aanpassen naar Standaarden. De standaarden worden ook wettelijk opgelegd zoals WCAG en niet door de GEMMA. Dat ze opgehaald worden uit de GEMMA is duidelijk.
+
+
+
+
+---
+
+## Reacties (8)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+zie slide 38, dit is een gevolg van het wijzigings verzoek omschakelen naar standaard versies (en hoort dus eigenlijk nog bij dat wijzigings verzoek en is destijds niet opgevallen). We hebben de titels inmiddels aangepast op de B omgeving
+
+### Reactie 3 — @Makkmetp (2026-01-29)
+
+Alleen standaardversies tonen.
+Het is dubbelop om de standaarden ook te tonen.
+
+### Reactie 4 — @rubenvdlinde (2026-02-03)
+
+Is het een probleem als de gebruiker standaarden wel zelf kan selecteren? Op dit moment staan de kollomen namenlijk default uit maar heeft de gebruiker nog wel zelf de mogenlijkheid de tabel aan te passen.
+
+### Reactie 5 — @Makkmetp (2026-02-03)
+
+Hoe bedoel je deze?
+Dat Standaarden nog als kolom geselecteerd kan worden?
+
+### Reactie 6 — @rubenvdlinde (2026-02-23)
+
+We zijn gemeenschapenlijk vergeten deze woensdag te bespreken, kwamen wij vrijdag achter. Terwijl hierboven een vraag uitstaat :(
+
+Klein stukje context, deze properties bestaan omdat we vanuit amef te maken hebben met amef id's, en vng id's en dan nog een keer compliancy objecten om ook urls en documenten op vast te leggen. Om als deze dingen aan elkaar te kunnen knopen zijn technische koppel properties nodig. Dat zijn de hier genoemde velden, we kunnen dus niet zonder.
+
+Op dit moment hebben we configuratie opties om dee propeties buiten de default tabel weergave te houden, echter de gebruiker kan zelf extra properties toevoegen aan de tabel weergave (we kunnen overigens nog een keer overwegen wat de toegevoegde waarde van die functionaliteit is). We kunnen de properties ook niet te hard verbergen want sommige worden gebruikt in de cards.
+
+Hence dat we de vraag hadden of er kon worden geleeft met de functionaliteit as is (dus de kollomen komen niet standaard terug in de tabel maar kunnen wel door de gebruiker worden toegevoegd) omdat er anders een volledige feature moet worden ontwikkeld voor 3 technische kollomen.
+
+
+
+### Reactie 7 — @Makkmetp (2026-02-23)
+
+@markbacker laten we deze morgen bespreken.
+
+Ik zie namelijk ook iets geks gebeuren in de kolom Compliance:
+
+
+
+### Reactie 8 — @markbacker (2026-02-24)
+
+De standaardversies kolom is nu goed. Issue #430 is nieuw in de applicatiebeheertabel
diff --git a/issues/375.md b/issues/375.md
new file mode 100644
index 00000000..09491599
--- /dev/null
+++ b/issues/375.md
@@ -0,0 +1,60 @@
+# #375 — Applicaties: versie voor SaaS applicaties?
+
+**Status:** OPEN | **Labels:** Aanbod, IGS review
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/375
+
+---
+
+## Beschrijving
+
+Wanneer je via Applicaties het overzicht ziet en kijkt onder Applicatieversies zie je geen versie staan bij een SaaS applicatie. In het verleden is er afgesproken dat een SaaS applicatie een default versie krijgt.
+Graag deze toevoegen.
+
+
+
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie ook slide 39, In principe vangt de wizard dit af door altijd een versie aan te maken, hoe ben je in deze casus terecht gekomen?
+
+### Reactie 3 — @Makkmetp (2026-01-29)
+
+Getest met verschillende opties:
+- Saas
+- SaaS en On Premise
+- On premise verwijderen
+- Saase verwijderen
+Mocht je er niet uitkomen, let me know
+
+### Reactie 4 — @Makkmetp (2026-02-23)
+
+❌Wanneer je een applicatie aanmaakt met hosting SaaS, dan krijgt de applicatie geen versie.
+
+
+
+### Reactie 5 — @Makkmetp (2026-03-04)
+
+Aanvullend op dit issue:
+- Wanneer je de hosting van een applicatie wijzigt van een On-premise naar SaaS-variant, dan blijven de "oude" On-premise varianten bestaan.
+Een logische werking zou zijn, dat bij het veranderen van een On-premise variant naar een SaaS-variant, de on-premise versies niet meer getoond/beschikbaar zijn.
+Hieronder staat een voorbeeld hoe dat eruit ziet wanneer je een applicatie veranderd van On-premise naar SaaS
+
+
+
+
diff --git a/issues/376.md b/issues/376.md
new file mode 100644
index 00000000..0bdf1f8f
--- /dev/null
+++ b/issues/376.md
@@ -0,0 +1,48 @@
+# #376 — Applicaties: labels wizard en tabel zijn anders
+
+**Status:** OPEN | **Labels:** Aanbod, Tekstuele wijzigingen, IGS review
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/376
+
+---
+
+## Beschrijving
+
+De labels in de beheertabel wijken af van de wizard.
+Het zijn er meer, spelling is anders of zelf helemaal anders
+Vermoedelijk zijn de labels alleen in de Wizards aangepast, niet in OpenRegister
+Graag de labels aanpassen naar de labels uit de wizards en powerpoint van 17-12-2025
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie ook slide 42, zelfde bevinding als #359. Dit is geen regressie deze teksten zaten al in de tabbelen toen ze werden geacepteerd. En in de fasen daarna. Maar zijn inderdaad nog niet in lijn met de laatste powerpoint.
+
+Enfin, wordt nu doorgevoerd.
+
+### Reactie 3 — @Makkmetp (2026-02-23)
+
+Over twee kolommen zijn nog vragen:
+❔Compliance - waar komt deze kolom vandaan? Deze wordt ook telkens gevuld met de naam van de applicatie en staat niet in de wizard.
+❔Applicatie Versies - de correcte schrijfwijze is Applicatieversies.
+
+
+
+
+
diff --git a/issues/377.md b/issues/377.md
new file mode 100644
index 00000000..165ee685
--- /dev/null
+++ b/issues/377.md
@@ -0,0 +1,65 @@
+# #377 — Applicaties: tabel toont diensten niet
+
+**Status:** OPEN | **Labels:** Aanbod, IGS review
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/377
+
+---
+
+## Beschrijving
+
+Voor de applicatie PinoApp wordt de dienst PinoApp beheer niet getoond
+Kolom Diensten is leeg
+Andersom wordt bij de dienst de applicatie wel getoond
+
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Zie ook slide 43, er miste inderdaad een weergave voor dienst in deze tabel, die mist overigens als sinds voor het accepteren van aanbod. We hebben hem toegevoegd en neergezet op de B omgeving.
+
+### Reactie 3 — @Makkmetp (2026-02-24)
+
+❌Ingelogd als een nieuwe gebruiker van Centric is een dienst aangemaakt voor Key2GBA-V. Deze dienst komt niet terug in het overzicht van de applicaties onder de kolom Diensten.
+
+❌Ingelogd als gebruiker van een nieuwe leverancier. Via Applicaties > ...Acties > Dienst publiceren verschillende diensten aangemaakt en via Diensten > Toevoegen. De aangemaakte diensten worden niet getoond in de kolom Diensten onder Applicaties. Onder Diensten staan ze wel.
+
+
+
+
+
+
+
+
+
+### Reactie 4 — @markbacker (2026-02-25)
+
+Ik zie mijn toegevoegde dienst wel verschijnen in de tabel en de diensten wizard
+
+
+
+
+
+---
+
+## Kruisverwijzingen
+
+_Toegevoegd op 2026-03-02 op basis van API-testresultaten._
+
+- **Duplicaat van #373:** API-tests voor #377 draaien onder de acceptatiecriteria van #373 (applicatie tabel toont diensten niet). Beide issues betreffen dezelfde fix: extend array + custom renderer + API parameter fix.
diff --git a/issues/378.md b/issues/378.md
new file mode 100644
index 00000000..2fd74b3f
--- /dev/null
+++ b/issues/378.md
@@ -0,0 +1,64 @@
+# #378 — Applicatie: Standaarden na wijzigen veranderd
+
+**Status:** OPEN | **Labels:** wontfix, Aanbod, IGS review
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/378
+
+---
+
+## Beschrijving
+
+Wanneer je via Menu Applicaties een applicatie bekijkt en via de knop Acties > Bewerk standaarden selecteert. Dan hebben de standaarden nog de juiste waarden en kleuren. Sla je deze lijst op, dan wijzigt alles naar Ondersteund.
+
+
+
+---
+
+## Reacties (7)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Dit lijkt inderdaad een Bug, @remko48 zou jij deze willen oppakken?
+
+### Reactie 3 — @Makkmetp (2026-01-29)
+
+#384 - wanneer alleen de wizards gebruik gaan worden, kunnen de standaarden onder een tab-geplaatst worden.
+
+### Reactie 4 — @rubenvdlinde (2026-02-03)
+
+Deze komt inderdaad te vervallen als we alles naar wizards omzetten
+
+### Reactie 5 — @Makkmetp (2026-02-04)
+
+Ik zet hem nog even op je naam @rubenvdlinde, zodat jullie deze nog kunnen testen wanneer de wijzigingen zijn doorgevoerd.
+
+### Reactie 6 — @Makkmetp (2026-02-23, bijgewerkt 2026-03-02)
+
+✅De standaarden zijn nu alleen te bewerken via de Wizard en zodoende blijven de kleuren correct.
+
+❌Nu lijkt de naamgeving van het opgevoerde document wat als bewijsmateriaal dient een andere naam te krijgen. Dit is opgevoerd in #442
+Het volgende is opgevoerd bij de applicatie Bal:
+
+
+
+
+
+Wanneer je nu de applicatie bekijkt via Overzicht Applicaties > Bekijken applicatie, dan is de naamgeving van het document veranderd naar "Bewijs_."
+
+
+
+### Reactie 7 — @Makkmetp (2026-03-02)
+
+@markbacker #442 is aangemaakt en gaat verder met de incorrecte naam van het bestand.
+Het onderwerp waar dit issue voor is aangemaakt lijkt daarmee opgelost. Eens?
diff --git a/issues/379.md b/issues/379.md
new file mode 100644
index 00000000..a90a3440
--- /dev/null
+++ b/issues/379.md
@@ -0,0 +1,83 @@
+# #379 — Applicatie: verschillende manier van tonen compliancy
+
+**Status:** OPEN | **Labels:** Aanbod, IGS review
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/379
+
+---
+
+## Beschrijving
+
+Een standaarden die een applicatie ondersteund worden op 3 manieren getoond.
+
+- De tabel op de beheerpagina is goed (klopt conform huidige SWC)
+- De tabel in een tab onder een gevonden applicatie is niet compleet en toont compliancy niet correct
+- De bullet list op de controlepagina publiceren Applicatie toont alleen compliancy, niet wat niet ondersteund wordt
+
+_Maak de tabel zoals op de beheerpagina (en voorkom dubbelingen in de code)_
+
+
+Beheer applicatiedetails: https://softwarecatalogus.accept.opencatalogi.nl/beheer/applicaties/e5dc58de-4ccc-484b-bd7c-60dd131955ac
+
+Zoeken applicatiedetail
+Niet ondersteunde standaarden ontbreken. De bovenste 2 zijn "compliant"
+Zie: https://softwarecatalogus.accept.opencatalogi.nl/forms/applicatie?type=eigen&id=e5dc58de-4ccc-484b-bd7c-60dd131955ac
+
+Wizard controlepagina: https://softwarecatalogus.accept.opencatalogi.nl/forms/applicatie?type=eigen&id=e5dc58de-4ccc-484b-bd7c-60dd131955ac
+
+
+
+
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+@Makkmetp goed gevonden! we gaan even uitzoeken waarom deze verschillen dat zou inderdaad niet moeten. DIt verklaar dan ook meteen #348 en waarom bij de vergelijking van @markbacker en @rubenvdlinde de aantallen wel overeen kwamen. Overigens zit dit er dan al in sinds aanbod.
+
+### Reactie 3 — @remko48 (2026-02-16)
+
+- Zo als hierboven gevraagd is het op de controle pagina nu dezelfde tabel
+- Zo als hierboven gevraagd is het nu op de applicatie detail pagina dezelfde tabel
+
+
+
+
+
+### Reactie 4 — @Makkmetp (2026-02-23)
+
+✅Een test uitgevoerd met de applicatie Bal van leverancier Fortuna. 38 standaarden komen op beide overzichten terug.
+❔De volgorde in beide lijsten is wel anders. In het 2e overzicht (onder applicatie bewerken) staat de correcte volgorde. Is het een bewuste keuze dat er verschil zit tussen de volgorde van de lijsten?
+
+
+
+
+
+
+### Reactie 5 — @markbacker (2026-02-25)
+
+Zie #284
+Alleen standaardversies met een status=`in gebruik` en status=`in ontwikkeling` moeten getoond worden.
+
+Als een applicatie een standaard ondersteund met status= `einde ondersteuning` of status= `teruggetrokken` dan moeten deze als toegevoegde standaarden getoond worden.
+
+---
+
+## Kruisverwijzingen
+
+_Toegevoegd op 2026-03-02 op basis van API-testresultaten._
+
+- **Opgelost via #430:** Opgelost door het verwijderen van de tabelkolom conform #430 (Compliancy kolom verwijderen).
diff --git a/issues/380.md b/issues/380.md
new file mode 100644
index 00000000..a9ccf610
--- /dev/null
+++ b/issues/380.md
@@ -0,0 +1,52 @@
+# #380 — Applicatie: compliance aantallen komen niet overeen
+
+**Status:** CLOSED | **Labels:** Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/380
+
+---
+
+## Beschrijving
+
+Wizard stap standaarden toont alle referentiecomponenten die een standaardversie opleggen, de beheerpagina toont er één.
+
+Graag zorgen dat alle standaardversies op beide getoond worden.
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Scherp gespot, ik koppel hem even aan #379. Trouwens geen wonder dat deze niet eerder gespot is want hij is geniepig maar hij zit er dus al in sinds Aanbod.
+
+### Reactie 3 — @Makkmetp (2026-02-23)
+
+✅De aantallen komen overeen met het aantal opgevoerde verplichte standaarden die gekoppeld zijn aan een referentiecomponent.
+
+
+
+
+
+
+
+---
+
+## Kruisverwijzingen
+
+_Toegevoegd op 2026-03-02 op basis van API-testresultaten._
+
+- **Zelfde oorzaak als #379, #430:** Dezelfde oorzaak als #379 (inconsistente compliance-weergave). Beide opgelost door #430.
diff --git a/issues/381.md b/issues/381.md
new file mode 100644
index 00000000..fe19badf
--- /dev/null
+++ b/issues/381.md
@@ -0,0 +1,45 @@
+# #381 — Applicaties: non-compliant vervangen door niet ondersteund
+
+**Status:** OPEN | **Labels:** Aanbod, Wijziging, IGS review
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/381
+
+---
+
+## Beschrijving
+
+Wijzig de tekst non-compliant naar niet ondersteund inclusief de rode kleur.
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Lijkt me prima, we stellen wel voor pas wijzigingen door te voeren als afschalen producten is afgerond.
+
+### Reactie 3 — @Makkmetp (2026-02-23, bijgewerkt 2026-03-02)
+
+✅Letterlijk genomen is Non-compliant inderdaad aangepast naar Niet ondersteund.
+
+Alleen zie ik nog meerdere variaties in het controle overzicht bij het publiceren van een Applicaties of bij het bekijken van een applicatie:
+Tekstueel: Compliant - Ondersteund
+Verschillende kleuren: Niet ondersteund in Grijs en Rood. Graag eenduidigheid in de kleuren en teksten:
+Compliant - witte tekst, groene achtergrond
+Niet ondersteund - witte tekst, grijze achtergrond
+Ondersteund - - witte tekst, bruine/oranje achtergrond
+
+
+
+
diff --git a/issues/382.md b/issues/382.md
new file mode 100644
index 00000000..5253d2f7
--- /dev/null
+++ b/issues/382.md
@@ -0,0 +1,43 @@
+# #382 — Applicatie: compliancy link werkt niet
+
+**Status:** CLOSED | **Labels:** Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/382
+
+---
+
+## Beschrijving
+
+Als ik bij PinoApp doorklik op een compliancy link klik, dan ga ik niet naar een externe website
+Link pino.nl/compliancy verwijst naar https://softwarecatalogus.accept.opencatalogi.nl/pino.nl/compliancy
+
+Graag de link zo maken dat er een pagina opent met de correcte link.
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Dit heeft zo als besroken te maken met de router, pino.nl/compliancy is eigenlijk geen geldigde url en wordt dus ook niet zo herkend. We hebben deze inmiddels opgepakt
+
+### Reactie 3 — @Makkmetp (2026-02-23)
+
+ ✅De link is aanklikbaar en opent nu de bijbehorende pagina.
+
+
+
+
diff --git a/issues/383.md b/issues/383.md
new file mode 100644
index 00000000..b9faac42
--- /dev/null
+++ b/issues/383.md
@@ -0,0 +1,43 @@
+# #383 — Applicatie: selectie vakken werken niet
+
+**Status:** CLOSED | **Labels:** Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/383
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Deze is niet aangepast dus dan zit die er al in sinds aanbod. @remko48 kan jij deze oppakken?
+
+Toegiging: het lijkt eerder wel gewerkt te hebben dus dan is hetregressie door de wesenlijke wijziging (perfomance) onder producten afschalen. En moet vóór finale accepatie worden opgepakt. We pakken hem dan ook definitief op ná producten afschalen.
+
+### Reactie 3 — @Makkmetp (2026-02-23)
+
+✅Het selecteren kan weer. Na selectie kunnen de geselecteerde applicaties ook verwijderd worden.
+❔Een selectie van applicaties werkt niet voor het exporteren. Bij het exporteren wordt alles geëxporteerd en niet alleen de geselecteerde applicaties.
+
+### Reactie 4 — @markbacker (2026-02-25)
+
+Dat wordt denk ik een kwestie van smaak. Is geen eis om te kunnen selecteren welke je exporteert.
+
+Goed genoeg
diff --git a/issues/384.md b/issues/384.md
new file mode 100644
index 00000000..3a583947
--- /dev/null
+++ b/issues/384.md
@@ -0,0 +1,60 @@
+# #384 — Applicaties: eenduidige manier van bewerken
+
+**Status:** CLOSED | **Labels:** Aanbod, Wijziging
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/384
+
+---
+
+## Beschrijving
+
+Er zijn nu meerdere manieren om een applicatie te kunnen bewerken. En elke keer zijn er verschillende manieren. Graag alles via de wizards laten gaan.
+
+- Applicaties > Bewerken
+- Applicaties > Bekijken > Bewerken (de overige opties behalve "verwijderen" hier uitschakelen. Dan is de bug Bewerk standaarden hier alvast opgelost #378 )
+
+
+---
+
+## Reacties (6)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-01-29)
+
+Lijkt me een goed idee, volgensmij is de afspraak dat we alles via de wizards doen. Zit er natuurlijk al wel in sinds aanbods.
+
+### Reactie 3 — @Makkmetp (2026-02-03)
+
+Graag lange omschrijving toevoegen aan wizards, zodat alle bewerken via de wizards gedaan kunnen worden en niet via een aparte optie.
+
+### Reactie 4 — @rubenvdlinde (2026-02-03)
+
+De lange omschrijving zat niet in de design powerpoints, maar is wel randvoorwaardenlijk voor alles via de wizards laten lopen. We pakken hem dan ook op ná producten afschalen.
+
+### Reactie 5 — @rubenvdlinde (2026-02-11)
+
+Dit issue gaat eigenlijk over twee punten:
+
+- [ ] Consistent overal wizards gebruiken (dus ook vanuit tabelen, zoek interface en detail paginas) ipv formulieren
+- [ ] Controleren od de lange omschrijving is opgenomen in alle wizards.
+
+Gezien het formaat vna het wijzigings verzoek bouw @SudoThijn controle @remko48 en dan pas ter test aan @WilcoLouwerse
+
+### Reactie 6 — @Makkmetp (2026-02-23)
+
+✅Vanaf Applicaties overzicht > ...Acties > Bewerken wordt de Wizard gestart
+✅Vanaf Applicaties overzicht > ...Acties > Bekijken > ...Acties > Bewerken wordt de Wizard gestart
+
+Mogelijk hebben we het niet over hetzelfde of is de verkeerde term gebruikt, maar de Uitgebreide omschrijving zie ik wel in de ppt staan.
+
+
diff --git a/issues/385.md b/issues/385.md
new file mode 100644
index 00000000..235785bb
--- /dev/null
+++ b/issues/385.md
@@ -0,0 +1,45 @@
+# #385 — Applicatie: Geen huidige versie in gebruik
+
+**Status:** CLOSED | **Labels:** Aanbod, Wijziging
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/385
+
+---
+
+## Beschrijving
+
+Aan de rechterkant in het grijze blok staat: Geen huidige versie in gebruik. Onder het tabblad versie staan Centric Begraven (Centric) staan meerdere versies met de status “In gebruik.”
+
+Versies staan allemaal al onder het tabblad Versies + het aantal, dan kan de "Huidige versie" verwijderd uit het grijze blok worden.
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-02-03)
+
+Dus samenvattende "Huidige versie" verwijderen uit grijze blok?
+
+### Reactie 3 — @Makkmetp (2026-02-03)
+
+Ja, klopt.
+Graag verwijderen uit het grijze blok. De versies staan al onder het tabblad versies
+
+### Reactie 4 — @Makkmetp (2026-02-23)
+
+✅De versies zijn verwijderd uit het grijze blok. Het tabblad versies is nu gevuld met alle versies.
+
+
diff --git a/issues/386.md b/issues/386.md
new file mode 100644
index 00000000..158f52d4
--- /dev/null
+++ b/issues/386.md
@@ -0,0 +1,44 @@
+# #386 — Applicaties – Uw applicatie publiceren: andere labels
+
+**Status:** CLOSED | **Labels:** Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/386
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-02-03)
+
+Dit is geen regressie deze teksten zaten al in de wizards toen ze werden geacepteerd. Overgigens zijn deze teksen ooit op verzoek verlengd (dus gaan afwijken van het schema). Nu niet meer zo relevant we kunnen ze ook gelijk trekken.
+
+Enfin, wordt nu doorgevoerd.
+
+### Reactie 3 — @rubenvdlinde (2026-02-11)
+
+@remko48 dubbelcheck end an naar @WilcoLouwerse
+
+### Reactie 4 — @Makkmetp (2026-02-23)
+
+✅De teksten zijn als in de ppt.
+
+
+
+
diff --git a/issues/387.md b/issues/387.md
new file mode 100644
index 00000000..72d2c50e
--- /dev/null
+++ b/issues/387.md
@@ -0,0 +1,49 @@
+# #387 — Applicaties – Uw applicatie publiceren: i niet aanwezig
+
+**Status:** CLOSED | **Labels:** Aanbod, Tekstuele wijzigingen
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/387
+
+---
+
+## Beschrijving
+
+De i(nformatie) wordt bij alle labels voor een versie gemist.
+
+
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-02-03)
+
+Is aangepast op B omgeving
+
+### Reactie 3 — @rubenvdlinde (2026-02-03)
+
+We zijn deze inmiddels aan het oppakken (we zijn alle tekstuele wijzigingen aan het doorlopen) maar deze oversight blijk ook te zitten in de issues zo als opgehaald in https://github.com/VNG-Realisatie/Softwarecatalogus/issues/359 voor tekst. Dat zijn de tekstuele wijzigingen voor de gebruiks wizards. Supder duf maar daar zitten de aanbod wizards dus niet tussen. Overigens is dit dus weer geen regressie deze teksten zaten al in de wizards toen ze werden geacepteerd.
+
+Enfin, wordt nu doorgevoerd.
+
+### Reactie 4 — @rubenvdlinde (2026-02-11)
+
+@remko48 dubbelcheck en dan naar @WilcoLouwerse
+
+
+### Reactie 5 — @Makkmetp (2026-02-23)
+
+✅Teksten komen overeen met de ppt
+
+
diff --git a/issues/389.md b/issues/389.md
new file mode 100644
index 00000000..dfff60a6
--- /dev/null
+++ b/issues/389.md
@@ -0,0 +1,42 @@
+# #389 — Applicaties – Uw applicatie publiceren: link verdwijnt na klikken (2)
+
+**Status:** CLOSED | **Labels:** Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/389
+
+---
+
+## Beschrijving
+
+Na het klikken op de link “een overzicht van alle standaarden” opent de pagina in een andere tab. Dat is goed. Wanneer je dan weer teruggaat dan wordt de link niet meer onderstreept. Na een tijdje verschijnt de onderstreping weer.
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-02-03)
+
+@remko48 heeft aangegeven dat dit VNG huisstijl is.
+
+### Reactie 3 — @Makkmetp (2026-02-03)
+
+@Makkmetp zoekt dit uit en komt erop terug.
+Het heeft met de staging regels te maken op VNG.nl
+
+### Reactie 4 — @Makkmetp (2026-02-04)
+
+✅De werking komt overeen met VNG.nl.
+De techniek is anders + de link opent in een nieuw tabblad, waardoor het anders lijkt.
diff --git a/issues/390.md b/issues/390.md
new file mode 100644
index 00000000..5218af95
--- /dev/null
+++ b/issues/390.md
@@ -0,0 +1,49 @@
+# #390 — Applicaties – Uw applicatie publiceren: labels komen niet overeen
+
+**Status:** CLOSED | **Labels:** Aanbod, Tekstuele wijzigingen
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/390
+
+---
+
+## Beschrijving
+
+Labels uit de wizard komen niet overeen met het controle formulier. Graag aanpassen naar de wizard en consistent maken.
+
+
+
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @Makkmetp (2026-02-03)
+
+Op B-omgeving is dit opgelost. Nog testen.
+
+### Reactie 3 — @rubenvdlinde (2026-02-03)
+
+We zijn deze inmiddels aan het oppakken (we zijn alle tekstuele wijzigingen aan het doorlopen) maar deze oversight blijk ook te zitten in de issues zo als opgehaald in https://github.com/VNG-Realisatie/Softwarecatalogus/issues/359 voor tekst. Dat zijn de tekstuele wijzigingen voor de gebruiks wizards. Supder duf maar daar zitten de aanbod wizards dus niet tussen. Overigens is dit dus weer geen regressie deze teksten zaten al in de wizards toen ze werden geacepteerd.
+
+Enfin, wordt nu doorgevoerd.
+
+### Reactie 4 — @rubenvdlinde (2026-02-11)
+
+@remko48 dubbelcheck en dan nar @WilcoLouwerse
+
+### Reactie 5 — @Makkmetp (2026-02-23)
+
+✅De termen komen overeen met de labels uit de wizard.
+
+
diff --git a/issues/391.md b/issues/391.md
new file mode 100644
index 00000000..70fb0acf
--- /dev/null
+++ b/issues/391.md
@@ -0,0 +1,59 @@
+# #391 — Testen met een gebruiker van een bestaande organisatie
+
+**Status:** OPEN | **Labels:** Aanbod, IGS review
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/391
+
+---
+
+## Beschrijving
+
+Om verschillende redenen is de mogelijkheid om te testen met een geïmporteerde gebruiker van een geïmporteerde organisatie uitgezet.
+Het testen met een dergelijke gebruiker is onderdeel van de acceptatie. @rubenvdlinde Laten we afstemmen wanneer deze mogelijk beschikbaar is, zodat het één en ander vergeleken kan worden met de huidige softwarecatalogus.
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @Makkmetp (2026-02-03)
+
+Zie #392
+
+Data import is doen, daarna kan @Makkmetp testen
+
+### Reactie 3 — @rubenvdlinde (2026-02-15)
+
+Dit issue is een dubleur van @392 inderdaad, dus die moeten samen worden getest.
+
+### Reactie 4 — @rubenvdlinde (2026-02-20)
+
+Even opletten, de geimporteerde gebruikers hebben ongeldige email adressen mee gekregen vanuit de import. Dus je moet eerst het email adres aanpassen voordat je activeerd.
+
+### Reactie 5 — @Makkmetp (2026-02-25)
+
+❌Het aanpassen van de ongeldige mailadressen is niet mogelijk, doordat deze karakters bevat die niet zijn toegestaan. Via de front-end is er een 400-error, wanneer je het wachtwoord wijzigt.
+
+✅ Het aanmaken van een nieuwe gebruiker via de back-end werkt wel en daarmee kan je inloggen onder een geimporteerde organisatie.
+
+
+
+
+
+---
+
+## Kruisverwijzingen
+
+_Toegevoegd op 2026-03-02 op basis van API-testresultaten._
+
+- **Duplicaat van #392:** #391 en #392 zijn duplicaten — beide betreffen het activeren van gebruikers voor geïmporteerde organisaties.
diff --git a/issues/392.md b/issues/392.md
new file mode 100644
index 00000000..da3eff65
--- /dev/null
+++ b/issues/392.md
@@ -0,0 +1,61 @@
+# #392 — Back-end: geimporteerde gebruiker geeft error bij omzetten naar user
+
+**Status:** OPEN | **Labels:** Aanbod, IGS review
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/392
+
+---
+
+## Beschrijving
+
+Mogelijk heeft dit te maken met #391 , anders hierbij het issue:
+
+Een nieuw aangemaakt contactpersoon bij een nieuwe organisatie wordt direct omgezet naar een “user”. Bij een ingeladen organisatie wordt een nieuw aangemaakt contactpersoon geen user
+
+
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-02-03)
+
+Zie slide 49, Peter wil graag testen dus dan meot deze open staan. Verz
+
+### Reactie 3 — @Makkmetp (2026-02-03)
+
+@Makkmetp wil dit graag testen.
+Hiervoor dient de data-import nog gedaan worden.
+
+
+### Reactie 4 — @rubenvdlinde (2026-02-03)
+
+Blokade op activeren oude personen opheven bij opnieuw neerzetten acc omgeving, dit issue is daarmee en dubleur van #391
+
+### Reactie 5 — @Makkmetp (2026-02-24)
+
+✅Het aanmaken van een nieuwe gebruiker voor een geïmporteerde organisatie is gelukt.
+❌Een geïmporteerde gebruiker omzetten, zodat daarmee ingelogd kan worden is niet gelukt. In eerste instantie door de karakters die in het mailadres staan en niet mogen volgens NextCloud. Daarna via de front-end geprobeerd via Contactpersonen > ...Acties > Bewerken. Het wijzigen van het mailadres in het veld lukt, alleen het opslaan daarna geeft daarna een 400-error. Dit is ook getest met een net nieuw aangemaakte leverancier en haar contactpersonen en deze geeft ook een 400-error.
+
+
+
+
+
+---
+
+## Kruisverwijzingen
+
+_Toegevoegd op 2026-03-02 op basis van API-testresultaten._
+
+- **Duplicaat van #391:** #392 en #391 zijn duplicaten — beide betreffen het activeren van gebruikers voor geïmporteerde organisaties.
diff --git a/issues/393.md b/issues/393.md
new file mode 100644
index 00000000..e64df44a
--- /dev/null
+++ b/issues/393.md
@@ -0,0 +1,61 @@
+# #393 — Backend: fouten in voorzieningenregister
+
+**Status:** OPEN | **Labels:** Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/393
+
+---
+
+## Beschrijving
+
+@markbacker en @rubenvdlinde pakken jullie deze samen op?
+
+
+
+---
+
+## Reacties (8)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-02-03)
+
+Zie slide 50, backend is volgensmij out scope producten maar in scope non functionals. Voorstel is dan ook om deze op te pakken na het afronden van Aanbod.
+
+### Reactie 3 — @Makkmetp (2026-02-03)
+
+De excel heeft in een eerder stadium gewerkt.
+
+Het datamodel is deels geschoond. Er zijn wel kolommen gedubbeld zodat functionaliteiten blijven werken.
+Het verwijderen van dubbelingen heeft flinke impact op properties, business logica en RBAC, waardoor mogelijk nieuwe issues naar voren komen. (technical debt).
+
+### Reactie 4 — @rubenvdlinde (2026-02-03)
+
+voor de excel export geld, dit is regressie door de wesenlijke wijziging (perfomance) onder producten afschalen. En moet vóór finale accepatie worden opgepakt (het is ook zowiezo een non-functional). We pakken hem dan ook definitief op ná producten afschalen.
+
+Voor het datamodel geld dat het oorspronkenlijke data model (met alles op type) gemeenschapenlijk is vastgesteld. Het aanpassen daarvan en de wizards (verus het toevoegen van een technische collom wat nu is gebeurd) is dan ook een wijzigings verzoek. Hier moeten we nog even over nadenken omdat de impact (lees risico) groot is.
+
+### Reactie 5 — @rubenvdlinde (2026-02-18)
+
+Vraag @markbacker kunnen we deze scopen?
+
+### Reactie 6 — @rubenvdlinde (2026-02-18)
+
+Afspraak: We brengen dit isue terug tot de backend moet fucntioneren in ieder geval schema opvragen, api documentatie opvragen, exports.
+
+### Reactie 7 — @rubenvdlinde (2026-02-20)
+
+
+
+### Reactie 8 — @rubenvdlinde (2026-02-23)
+
+Let op dat de exports nu per register/schema combinatie zijn (lang verhaal, maar anders exporteerd je in één keer de geheele software catalogus en dat bleek niet realistisch)
diff --git a/issues/394.md b/issues/394.md
new file mode 100644
index 00000000..8241febd
--- /dev/null
+++ b/issues/394.md
@@ -0,0 +1,52 @@
+# #394 — Contactpersonen van gemeenten publiekelijke zichtbaar
+
+**Status:** OPEN | **Labels:** Aanbod, Wijziging
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/394
+
+---
+
+## Beschrijving
+
+
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @Rem-Dam (2026-02-02)
+
+@Makkmetp ik kan nergens in de eisen vinden dat er een contactpersoon voor een gemeente moet zijn en ook niet dat die zichtbaar moet zijn. Hij komt wel voor in het datamodel. Dus kijkende naar de aanbesteding is de bug dus dat de contactpersoon nu zichtbaar is en dat die niet zichtbaar moet zijn, correct?
+
+### Reactie 3 — @rubenvdlinde (2026-02-03)
+
+Zie slide 52, Dit is regressie door de wesenlijke wijziging (afschalen published) onder producten afschalen. En moet vóór finale accepatie worden opgepakt.
+
+TIjdens het inhoudenlijk doornemen van dit issue zijn we tot het voortscheidend inzicht gekomen dat we dit het beste kunnen oppakken met het uitbreiden van de RBAC functionaliteti. We pakken hem dan ook definitief op ná producten afschalen.
+
+### Reactie 4 — @Makkmetp (2026-02-24)
+
+In de huidige softwarecatalogus is de werking als volgt:
+wanneer je bent ingelogd als leverancier, dan kan je zien welke gemeenten je applicatie hebben afgenomen en van de die gemeenten kan je de contactinformatie van die gemeente zien.
+Wil je bij een gemeente kijken die geen afnemer is van een pakket, dan krijg je een foutmelding 403.
+
+❌In de vernieuwde softwarecatalogus zijn gemeenten die geen pakket hebben afgenomen of hebben opgevoerd van de leverancier, vindbaar en is de contactinformatie te gebruiken.
+
+
+
+
+
+
+
+
diff --git a/issues/395.md b/issues/395.md
new file mode 100644
index 00000000..55e46dfd
--- /dev/null
+++ b/issues/395.md
@@ -0,0 +1,36 @@
+# #395 — Menu linkerkant verdwijnt
+
+**Status:** CLOSED | **Labels:** Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/395
+
+---
+
+## Beschrijving
+
+Wanneer je via Applicaties het overzicht opent en daarna op F5 of ctrl+r toetst, dan verdwijnt het menu aan de linkerkant.
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-02-03)
+
+Dit was een grafische, bug is inmiddels opgelost op de B omgeving
+
+### Reactie 3 — @Makkmetp (2026-02-25)
+
+✅ Na meerdere keren de f5 of CTRL+r combinatie te hebben ingetoetst blijft het linkermenu zichtbaar.
diff --git a/issues/396.md b/issues/396.md
new file mode 100644
index 00000000..c5859e87
--- /dev/null
+++ b/issues/396.md
@@ -0,0 +1,76 @@
+# #396 — Verouderde NextCloud versie
+
+**Status:** CLOSED | **Labels:** Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/396
+
+---
+
+## Beschrijving
+
+In de back-end staat een waarschuwing dat de huidige versie niet meer wordt ondersteund.
+Wanneer gaan we over naar een gesupporte versie?
+
+
+
+---
+
+## Reacties (8)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-02-03)
+
+Zie ook slide 59, zo als mondeling toegelicht is dit onderdeel van ci & cd vraagstuk en dus géén onderdeel van producten. Moet wel worden opgepakt voor definitieve acceptatie.
+
+### Reactie 3 — @Makkmetp (2026-02-03)
+
+In de toelichting is uitgelegd dat bij nieuwe versies regelmatig issues nar voren komen. Voor productie willen we een ondersteunde stabiele versie hebben.
+
+VNG wilt op een ondersteunde en stabiele versie draaien.
+VNG wilt niet op de laatste release zitten vanwege kinderziekten, maar wel op de ene laatste versie (-1).
+-3 versie wordt uitgefaseerd, vanuit -2 overgaan naar -1.
+Er komen drie versies per jaar uit. Dat betekent elk kwartaal/4 maanden een testcycli.
+In CI/CD wordt dit verder besproken.
+
+In de volgende acceptatie test willen we testen op de -1 versie.
+
+### Reactie 4 — @rubenvdlinde (2026-02-16)
+
+Even opletten zo als Wilco scherp constateerde betekend die dat we naar nextcloud 32.0.0. moeten, immers 33 komt de 18e (woensdag uit) het kan gek lopen. Zie ook https://github.com/nextcloud/server/wiki/Maintenance-and-Release-Schedule
+
+Waar we even op moeten letten @markbacker en @Makkmetp is de versie 33 redenlijk wat breaking changes bevat (zie ook https://docs.nextcloud.com/server/latest/developer_manual/app_publishing_maintenance/app_upgrade_guide/upgrade_to_33.html) we zullen die dus goed moeten doortesten (na de volgende levering) we gaan voor deze week naar 32 (dat is dan vanaf donerdag -1)
+
+Op dit moment is de code getest met nextcloud 32, dat is ook de versie waar de pentest op is gedaan. Dus die komt met de volgende deploy meer naar VNG accept en dan klopt alles.
+
+### Reactie 5 — @rubenvdlinde (2026-02-18)
+
+Afspraak we gaan vrijdag testen op versie 32.
+
+### Reactie 6 — @rubenvdlinde (2026-02-19)
+
+De nieuwe omgevingen staan bijde op nc 32
+
+### Reactie 7 — @Makkmetp (2026-02-23)
+
+✅ De omgeving draait nu op 32.
+33 is net live per 18-2-2026.
+
+Wat wel opvalt zijn nog de overige errors in de log.
+
+
+
+
+
+### Reactie 8 — @markbacker (2026-02-25)
+
+Akkoord
diff --git a/issues/397.md b/issues/397.md
new file mode 100644
index 00000000..931ba643
--- /dev/null
+++ b/issues/397.md
@@ -0,0 +1,67 @@
+# #397 — Pagina aanmaken via CMS
+
+**Status:** CLOSED | **Labels:** Aanbod, Wijziging
+**Auteur:** @Makkmetp | **Datum:** 2026-01-29
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/397
+
+---
+
+## Beschrijving
+
+Ondanks meerdere pogingen is het niet gelukt om de tekst van de algemene voorwaarden aan te passen. Graag hier een handleiding voor.
+
+
+
+---
+
+## Reacties (8)
+
+### Reactie 1 — @github-actions (2026-01-29)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @Rem-Dam (2026-01-29)
+
+Hallo Peter, er staat al lang een verzoek open voor het aanleveren van deze teksten zodat wij ze er goed in kunnen zetten, zie ook https://github.com/VNG-Realisatie/Softwarecatalogus/issues/182
+
+### Reactie 3 — @rubenvdlinde (2026-02-03)
+
+Zie ook slide 60, CMS is geen onderdeel van producten afschalen. Vanuit het PvE is wijzigigen teksen echter een algemeen ding dus we pakken deze mee onder non-functionals
+
+### Reactie 4 — @Makkmetp (2026-02-03)
+
+Een eerder stadium heeft het gewerkt. Na alle wijzigingen krijgen wij het nu niet werkend.
+
+### Reactie 5 — @rubenvdlinde (2026-02-03)
+
+Correct, dit is regressie door de wesenlijke wijziging (perfomance) onder producten afschalen. En moet vóór finale accepatie worden opgepakt (het is ook zowiezo een non-functional). We pakken hem dan ook definitief op ná producten afschalen.
+
+### Reactie 6 — @rubenvdlinde (2026-02-12)
+
+Dubleerd met #332
+
+### Reactie 7 — @rubenvdlinde (2026-02-23)
+
+Handleidingen staan op https://vng-realisatie.github.io/Softwarecatalogus/docs/Handleidingen :)
+
+### Reactie 8 — @Makkmetp (2026-02-25)
+
+✅Het aanmaken van een pagina is gelukt.
+
+Zie voor overige opmerkingen over het aanmaken van pagina #332
+Daar is een uitgebreide test gedaan met het inrichten van de pagina en issues gevonden.
+
+---
+
+## Kruisverwijzingen
+
+_Toegevoegd op 2026-03-02 op basis van API-testresultaten._
+
+- **Duplicaat van #332:** #397 is een duplicaat van #332 (Voorpagina inrichten). Beide betreffen CMS-pagina beheer.
diff --git a/issues/398.md b/issues/398.md
new file mode 100644
index 00000000..dac86106
--- /dev/null
+++ b/issues/398.md
@@ -0,0 +1,136 @@
+# #398 — Zoeken: Filter met UUID's onder leveranciers
+
+**State:** open
+**Created:** 2026-02-03T12:18:57Z
+**Labels:** Aanbod
+
+## Description
+
+UUID's nog onder leveranciers.
+De oorzaak is nog onbekend.
+Dit kan kan front-end, back-end of datamigratie zijn (bijvoorbeeld de naam is leeg).
+
+
+
+## Comments
+
+---
+
+**Comment by github-actions[bot]** — 2026-02-03T12:19:07Z
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+---
+
+**Comment by Makkmetp** — 2026-02-03T15:15:16Z
+
+@markbacker heeft gecontroleerd of er leveranciers zonder naam bestaan. Dit is niet het geval.
+Er lijkt modules te zijn die verwijzen naar leverancier, die niet bestaan in leveranciers lijst. (Orphans) Mark gaat dit na.
+
+---
+
+**Comment by markbacker** — 2026-02-03T16:51:20Z
+
+Nazoeken in datamigratie csv's
+- bestaan er applicaties die 1 van onderstaande id's als aanbieder hebben. Zo ja, waarom worden deze aanbieders dan niet als organisatie aangeleverd.
+ - 236cb622-f9d2-5e31-ad03-d54fb036b68a
+ - 2bddf1c5-06cc-5dc1-9e44-fb936d68a28d
+ - a748dbad-8bce-5e72-a848-b3e3a787d232
+ - b281986f-77c7-568c-9712-055dba0f54da
+ - da63cb18-a487-52e4-9970-f01bfa9057fb
+
+---
+
+**Comment by markbacker** — 2026-02-03T17:00:14Z
+
+Ja, er bestaan applicaties met als aanbieder 1 van bovenstaande id's.
+Nog uit te zoeken waarom de bijbehorende organisatie ontbreekt, voor nu kan ik deze niet vinden.
+
+Oorzaak van dit issue ligt bij het maken van de datamigratie-bestanden.
+
+---
+
+**Comment by markbacker** — 2026-02-03T17:00:46Z
+
+Uit igs gehaald
+
+---
+
+**Comment by markbacker** — 2026-02-04T09:57:50Z
+
+Zie ook #23
+
+---
+
+**Comment by rubenvdlinde** — 2026-02-15T20:05:34Z
+
+Omdat het een onderzoeks issue VNG zijde is haal ik mezelf er even vanaf en plaats ik hem in refinement (zodat die even uit ons focus gebied verdwijnt anders loop ik er elke dag op te klikken)
+
+---
+
+**Comment by rubenvdlinde** — 2026-02-21T10:02:01Z
+
+Bij het doorlopen van de aangeleverde data (we kwamen deze bij igs weer tegen) hebben we een inconsistentie gevonden tussen de module- en organisatieregistratie. In totaal verwijzen **12 modules** naar een aanbieder-UUID die niet voorkomt in het organisatiebestand. Dit betekent dat de betreffende organisaties wel als aanbieder zijn opgegeven bij een module, maar niet (meer) bestaan in de organisatiedataset.
+
+---
+
+## Overzicht getroffen modules
+
+| Module | Ontbrekende aanbieder UUID |
+|---|---|
+| Iprox.open - complete publicatie-oplossing voor de Woo | `108f0876-5d19-5800-858c-e04ba9ebdd33` |
+| Regeldienst.nl Vragenbomen | `cd5fcd3b-4b4a-5d9d-814a-5e44c3f904b9` |
+| Regeldienst.nl B1 taal check | `cd5fcd3b-4b4a-5d9d-814a-5e44c3f904b9` |
+| We Watch Buurtpreventie App | `b281986f-77c7-568c-9712-055dba0f54da` |
+| Diamond Forms, Flows & Docs | `a748dbad-8bce-5e72-a848-b3e3a787d232` |
+| Workboard procesmanagement | `a748dbad-8bce-5e72-a848-b3e3a787d232` |
+| Energiemonitor | `a748dbad-8bce-5e72-a848-b3e3a787d232` |
+| Silicon Low Code platform | `a748dbad-8bce-5e72-a848-b3e3a787d232` |
+| Document Builder Server | `2bddf1c5-06cc-5dc1-9e44-fb936d68a28d` |
+| Apollo | `da63cb18-a487-52e4-9970-f01bfa9057fb` |
+| Juno | `da63cb18-a487-52e4-9970-f01bfa9057fb` |
+| Zorg-Portaal | `236cb622-f9d2-5e31-ad03-d54fb036b68a` |
+
+---
+
+## Unieke ontbrekende organisaties
+
+Er zijn **5 unieke organisatie-UUIDs** die ontbreken in de organisatiedata:
+
+| UUID | Gekoppeld aan # modules |
+|---|---|
+| `a748dbad-8bce-5e72-a848-b3e3a787d232` | 4 modules |
+| `cd5fcd3b-4b4a-5d9d-814a-5e44c3f904b9` | 2 modules |
+| `da63cb18-a487-52e4-9970-f01bfa9057fb` | 2 modules |
+| `108f0876-5d19-5800-858c-e04ba9ebdd33` | 1 module |
+| `b281986f-77c7-568c-9712-055dba0f54da` | 1 module |
+| `2bddf1c5-06cc-5dc1-9e44-fb936d68a28d` | 1 module |
+| `236cb622-f9d2-5e31-ad03-d54fb036b68a` | 1 module |
+
+Dit heeft een aantal gevolgens
+- Er staan uuid's in de voorkant
+- De voorkant doet extra calls om dit the herstellen, word langsamer
+
+Mogenlijke oplossingen zijn de modulers verwijderen uit de import of the organisaties toevoegen.
+
+---
+
+**Comment by Makkmetp** — 2026-03-02T16:07:09Z
+
+Eén van de UUID's is van onderstaande leverancier:
+
+
+---
+
+## Kruisverwijzingen
+
+_Toegevoegd op 2026-03-02 op basis van API-testresultaten._
+
+- **Gerelateerd aan #333:** Gerelateerd aan #333 (UUIDs in filters). #398 betreft specifiek de leveranciers-filter.
diff --git a/issues/399.md b/issues/399.md
new file mode 100644
index 00000000..0ad91143
--- /dev/null
+++ b/issues/399.md
@@ -0,0 +1,50 @@
+# #399 — Versies: een versie van een applicatie van een andere leverancier levert een foutmelding
+
+**Status:** OPEN | **Labels:** Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-02-03
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/399
+
+---
+
+## Beschrijving
+
+Wanneer je als leverancier Steam inlogt en je klikt op een versie van een applicatie van Centric dan krijg je de melding "Kon publicatie niet laden."
+Bij jezelf kan het wel en krijg je de mogelijkheid om te bewerken.
+In alle gevallen kan je doorklikken en alleen als eigenaar/leverancier van de versie, moet je deze kunnen bewerken via de wizard Applicatie publiceren
+
+
+
+---
+
+## Reacties (6)
+
+### Reactie 1 — @github-actions (2026-02-03)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-02-03)
+
+Dit hangt op RBAC, moeten we wel refinnen wanneer wel en niet.
+
+### Reactie 3 — @Makkmetp (2026-02-03)
+
+@rubenvdlinde graag horen we je voorstel i.v.m. de complexiteit.
+
+### Reactie 4 — @rubenvdlinde (2026-02-15)
+
+In trand van het lezen van bovenstaande beschrijving is RBAC nu zo aangepast dat applicatie versies mogen worden geraadpleegd door adnere leveranciers. @remko48 dat betkend nog wel dat de apllicatie versie pagina online moet denk ik?
+
+### Reactie 5 — @rubenvdlinde (2026-02-16)
+
+@WilcoLouwerse online testen, en als het fout gaat moet de pagina welicht nog online dus terugschuiven naar @remko48 tenzij er een 404 vanuit het published endpoint komt. In dat geval doorschuiven naar @rubenvdlinde
+
+### Reactie 6 — @rubenvdlinde (2026-02-19)
+
+Published endpoint is aangepast
diff --git a/issues/4.md b/issues/4.md
new file mode 100644
index 00000000..07f9a993
--- /dev/null
+++ b/issues/4.md
@@ -0,0 +1,112 @@
+# #4 — Als aanbod-beheerder wil ik mijn pakketten eenmalig registreren en classificeren op basis van de referentiearchitecturen van de voor mij relevante sector(en)
+
+**Status:** CLOSED | **Labels:** Aanbod, PvE eis, Bevinding, Buiten scope oplevering
+**Auteur:** @rubenvdlinde | **Datum:** 2025-02-06
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/4
+
+---
+
+## Beschrijving
+
+opdat mijn pakket vindbaar is voor afnemers uit meerdere sectoren met hun eigen filteringangen
+https://conduction.atlassian.net/browse/VSC-283
+
+---
+
+## Reacties (12)
+
+### Reactie 1 — @github-actions (2025-02-06)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2025-03-19)
+
+@Makkmetp Zou deze naar sprint 4 mogen? aangezien we met de voorkant begonnen zijn
+
+### Reactie 3 — @Makkmetp (2025-04-16)
+
+@rubenvdlinde het issue staat nu in sprint 6 aangezien we daar nu in zitten.
+
+### Reactie 4 — @rubenvdlinde (2025-05-12)
+
+@remko48 dit issue gaat over het voorzieningen model, daar moet de refenentie input worden omgezet naar een multiselect aan de hand van de refentiecomponenten, referentie componenten kunnen worden opgehaald uit de AMEF API aangezien ze van een bepaald element type zijn (@Makkmetp heeft morgen de details).
+
+
+
+### Reactie 5 — @Makkmetp (2025-05-14)
+
+@remko48 de referentiecomponenten kunnen inderdaad opgehaald worden uit het GEMMA Model. Door de unieke id's kunnen relaties opgezet worden tussen de geregistreerde applicaties en de verschillende referentiecomponenten. Het is de bedoeling dat er meerdere referentiecomponenten aan een applicatie gekoppeld kunnen worden.
+
+Is dit voldoende informatie voor je?
+
+### Reactie 6 — @remko48 (2025-05-26)
+
+Om dit te testen moet je het referentie component `BGT-beheercomponent` toevoegen
+
+### Reactie 7 — @rubenvdlinde (2025-05-27)
+
+
+
+### Reactie 8 — @Makkmetp (2025-05-27)
+
+@rubenvdlinde dit issue gaat over de mogelijkheid om een applicatie op te voeren en te koppelen aan verschillende referentiearchitecturen van de verschillende sectoren, zoals waterschappen, gemeenten, ed. Volgens mij hebben we dit issue verschoven naar een later moment.
+
+In ieder geval kan het niet met het bovenstaande formulier, maar wel binnen het formulier Applicatie toevoegen.
+
+
+
+
+### Reactie 9 — @rubenvdlinde (2025-05-28)
+
+@Makkmetp dat klopt het was het verkeerder formulier, sorry :')
+
+We zitten officieel nu wel in de laatste sprint (na deze sprint zitten we in de verlening) en dit formulier is randvoorwaardenlijk voor eht kunnen afronden van de amef export (dat zijn deze gegeven) dus als het kan met dit formulier (en er geen opmerkingen zijn) dan zou ik hem wel graag ter acceptatie aanbieden zodat we volgende week de amef export kunnen afronden.
+
+### Reactie 10 — @Makkmetp (2025-05-28)
+
+@rubenvdlinde dat begrijp ik helemaal.
+
+Het formulier is nodig om het aanbod correct en volledig te registreren, zodat afnemers de applicaties later kunnen toevoegen aan hun applicatielandschap en daaruit een **AMEFF-export** kunnen genereren. Voor een deel is dit al uitgewerkt in [informatiemodel en terminologie](https://vng-realisatie.github.io/Softwarecatalogus-Archi-repository/?view=id-845cb485d3c34051810732df2c0e4590) en de verstuurde powerpoint, maar deze is nog niet compleet.
+
+Wanneer je het [informatiemodel en terminologie](https://vng-realisatie.github.io/Softwarecatalogus-Archi-repository/?view=id-845cb485d3c34051810732df2c0e4590) en de verstuurde powerpoint erbij pakt, zie je dat in dit formulier nog een aantal onderdelen ontbreken:
+
+### Wat nog toegevoegd moet worden:
+- **Logo** – sommige applicaties hebben een herkenbaar merk of beeldmerk
+- **Licentietype** – keuze uit: _Open Source_ of _Closed Source_
+- **Hostingopties**:
+ - _Wordt de applicatie door jullie als leverancier in de cloud gehost?_
+ ➝ **Ja** betekent dat er één versie beschikbaar is (SaaS of “Actueel”), met de default status _In gebruik_.
+ - _Kan de applicatie door de gemeente zelf worden gehost?_
+ ➝ **Ja** betekent dat meerdere versies en statussen opgevoerd kunnen worden, eventueel naast de actuele SaaS-versie.
+
+### Wat op dit moment (nog) overbodig is:
+- Applicatietype
+- Categorie
+- Functionaliteiten
+- Doelgroepen
+
+---
+
+Tot slot: leveranciers mogen aangeven welke diensten zij zelf verlenen op deze applicatie.
+**Wordt dat onderdeel opgenomen in dit formulier, of komt daar een apart formulier voor?**
+
+
+### Reactie 11 — @Makkmetp (2025-12-18)
+
+Dit betreft de Wizard Applicatie publiceren van leveranciers.
+
+
+
+### Reactie 12 — @markbacker (2026-01-29)
+
+Het koppelen van geschikt voor referentiecomponenten aan applicaties werkt goed.
+
+Het kunnen classificeren op basis van meerdere sectorale referentiearchitecturen is voor nu buiten scope
+
diff --git a/issues/400.md b/issues/400.md
new file mode 100644
index 00000000..af18196d
--- /dev/null
+++ b/issues/400.md
@@ -0,0 +1,34 @@
+# #400 — Koppeling - Opslaan van een koppeling geeft een foutmelding
+
+**Status:** CLOSED | **Labels:** Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-02-03
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/400
+
+---
+
+## Beschrijving
+
+Het opslaan van een koppeling geeft een foutmelding.
+
+
+
+---
+
+## Reacties (2)
+
+### Reactie 1 — @github-actions (2026-02-03)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @Makkmetp (2026-02-25)
+
+✅Koppelingen worden succesvol opgeslagen
+
+
diff --git a/issues/401.md b/issues/401.md
new file mode 100644
index 00000000..d836fbe2
--- /dev/null
+++ b/issues/401.md
@@ -0,0 +1,267 @@
+# #401 — Koppeling - geïmporteerde koppelingen kaartjes zijn leeg
+
+**Status:** OPEN | **Labels:** Aanbod, Wijziging, IGS datamigratie
+**Auteur:** @Makkmetp | **Datum:** 2026-02-03
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/401
+
+---
+
+## Beschrijving
+
+Geïmporteerde koppelingen kaartjes zijn leeg wanneer je ingelogd bent als andere leverancier.
+
+- Richting van de PIMS@all koppelingen klopt niet
+- 2 link->recht, 2 bidirectioneel
+
+
+Opvragen koppelingdetails geeft foutmelding
+
+
+Let op! Oude omgeving en opnieuw te testen op vernieuwde softwarecatalogus:
+https://softwarecatalogus.accept.opencatalogi.nl/publicatie/bf99f698-e606-5de1-bf6f-0627a9dbffa1
+
+
+
+---
+
+## Reacties (7)
+
+### Reactie 1 — @github-actions (2026-02-03)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-02-04)
+
+Dat klopt, zie ook https://github.com/VNG-Realisatie/Softwarecatalogus-datamigratie/tree/main/data de import bevat geen @name collom voor de koppelingen die we via de nieuwe catalogus aanmaken zetten we deze data wel (trekken we samen). Ik noteer hem een als de wens om dat ook te doen voor de imports. Of willen jullie deze zelf zetten en toevoegen?
+
+### Reactie 3 — @rubenvdlinde (2026-02-12)
+
+We hebben er voor nu maar voor gekozen om bij import de naam te defaulten om dit op te losen. Ged nog steeds als de koppelingen geen korte beschrijving hebben zullen de kaartjes leeg aanvoelen maar daar kunnen we weinig aan doen.
+
+### Reactie 4 — @rubenvdlinde (2026-02-16)
+
+@WilcoLouwerse als je opnieuw importeerd zou dit zichzelf goed moeten zetten
+
+### Reactie 5 — @rubenvdlinde (2026-02-23)
+
+Een geldige koppeling vereist aan de ene kant altijd een **moduleA**, en aan de andere kant een **moduleB** of een **buitengemeentelijke voorziening**. Bij het doorlopen van de aangeleverde data zijn **32 koppelingen** gevonden die hier niet aan voldoen. Er zijn ook 1.788 koppelingen gevonden met ongeldige standaardversies. Dat leid tot grafische en technische fouten in de voorkant.
+
+| Probleemtype | Aantal |
+|---|---|
+| moduleB én buitengemeentelijkeVoorziening ontbreken | 18 |
+| moduleA ontbreekt | 11 |
+| Beide kanten ontbreken | 3 |
+| Ongeldige standaardversies | 1.788 |
+| **Totaal** | **1.820** |
+
+## Overzicht ongeldige koppelingen
+
+| ID | Van (pakket) | Naar (pakket/voorziening) | moduleA | moduleB | Buitengem. voorziening | Probleem |
+|---|---|---|---|---|---|---|
+| `01be2c56…` | Centric Begraven | — | ✓ | ✗ | ✗ | moduleB én buitengemeentelijkeVoorziening ontbreken |
+| `1b57ca2d…` | — | Key2Financien | ✗ | ✓ | ✗ | moduleA ontbreekt |
+| `1e8c0b3c…` | — | NHR - Handelsregister | ✗ | ✗ | ✓ | moduleA ontbreekt |
+| `2997cfd8…` | — | X-Works | ✗ | ✓ | ✗ | moduleA ontbreekt |
+| `29c39a58…` | — | — | ✗ | ✗ | ✗ | Beide kanten ontbreken |
+| `406bbe27…` | — | LIAS Enterprise | ✗ | ✓ | ✗ | moduleA ontbreekt |
+| `41223fa8…` | — | — | ✗ | ✗ | ✗ | Beide kanten ontbreken |
+| `50aa33d1…` | — | JUBES - JUstitie BErichten Service | ✗ | ✗ | ✓ | moduleA ontbreekt |
+| `564833f5…` | VastgoedBeheerSysteem VBSonlineé | — | ✓ | ✗ | ✗ | moduleB én buitengemeentelijkeVoorziening ontbreken |
+| `5b37d91c…` | Cipers iSeries | — | ✓ | ✗ | ✗ | moduleB én buitengemeentelijkeVoorziening ontbreken |
+| `5cbd8222…` | Makelaarsuite | — | ✓ | ✗ | ✗ | moduleB én buitengemeentelijkeVoorziening ontbreken |
+| `6c4e3af2…` | Squit XO | — | ✓ | ✗ | ✗ | moduleB én buitengemeentelijkeVoorziening ontbreken |
+| `7273e7d9…` | Rx.Mission | — | ✓ | ✗ | ✗ | moduleB én buitengemeentelijkeVoorziening ontbreken |
+| `7612201b…` | Key2Datadistributie | — | ✓ | ✗ | ✗ | moduleB én buitengemeentelijkeVoorziening ontbreken |
+| `794dafaa…` | COMPAS | — | ✓ | ✗ | ✗ | moduleB én buitengemeentelijkeVoorziening ontbreken |
+| `7f36a874…` | Key2Datadistributie | — | ✓ | ✗ | ✗ | moduleB én buitengemeentelijkeVoorziening ontbreken |
+| `7ff24ef2…` | Gouw Belastingen | — | ✓ | ✗ | ✗ | moduleB én buitengemeentelijkeVoorziening ontbreken |
+| `882a66c4…` | — | GISVG | ✗ | ✓ | ✗ | moduleA ontbreekt |
+| `95a430fb…` | — | Motion | ✗ | ✓ | ✗ | moduleA ontbreekt |
+| `ae2c5832…` | NedPlan | — | ✓ | ✗ | ✗ | moduleB én buitengemeentelijkeVoorziening ontbreken |
+| `b890533e…` | Neuron (geo) Gegevensmagazijn | — | ✓ | ✗ | ✗ | moduleB én buitengemeentelijkeVoorziening ontbreken |
+| `bcf72459…` | — | Makelaarsuite | ✗ | ✓ | ✗ | moduleA ontbreekt |
+| `bf9ff354…` | Key2Financien | — | ✓ | ✗ | ✗ | moduleB én buitengemeentelijkeVoorziening ontbreken |
+| `c3cd01fb…` | UNIT4 Decade | — | ✓ | ✗ | ✗ | moduleB én buitengemeentelijkeVoorziening ontbreken |
+| `c8a8323e…` | — | BRK - Basisregistratie Kadaster | ✗ | ✗ | ✓ | moduleA ontbreekt |
+| `cb04ad64…` | — | — | ✗ | ✗ | ✗ | Beide kanten ontbreken |
+| `d7113bd0…` | — | Suwinet | ✗ | ✓ | ✗ | moduleA ontbreekt |
+| `d9d3bdf8…` | JOIN Zaak en Document | — | ✓ | ✗ | ✗ | moduleB én buitengemeentelijkeVoorziening ontbreken |
+| `de28857a…` | Neuron BAG | — | ✓ | ✗ | ✗ | moduleB én buitengemeentelijkeVoorziening ontbreken |
+| `ea91f22d…` | — | DigiD | ✗ | ✗ | ✓ | moduleA ontbreekt |
+| `f6576f86…` | IBurgerzaken | — | ✓ | ✗ | ✗ | moduleB én buitengemeentelijkeVoorziening ontbreken |
+| `f9bd3097…` | NedWIBON | — | ✓ | ✗ | ✗ | moduleB én buitengemeentelijkeVoorziening ontbreken |
+
+
+# Missends standaardversies?
+Er is nog een punt koppelingen verwijzen in de cvs bij stnadaard versie naar niet bestaande bestaande standaardversies in gemma (ik kan altans de uuids niet in GEMMA restored vinden. Hierdoor staan er uuids onder het kopje standaardversies in de kaartjes. Het grappige is dat de namen ook in de export zitten dus het lijken echt standaarden te zijn.
+
+> Totaal: 137 unieke standaardversies
+
+| Standaardversie UUID | Naam | Aantal koppelingen |
+|---|---|---|
+| `46a26a56-4820-439f-804a-56448ba1164a` | StUF BG 3.10 | 525 |
+| `91928a35-59db-4783-b40d-3fe31147cef3` | StUF BG 2.04 | 285 |
+| `a572f253-f76d-406e-8a8c-255c38eb791a` | StUF ZKN 3.10 | 73 |
+| `a201709d-09e9-4fcf-a4b4-d7a9e5bd7efd` | Documentcreatieservices 1.1 | 60 |
+| `59c025ee-7e67-4913-99bf-e6dd9c55412d` | GBA koppelvlak (actueel) | 52 |
+| `684c50f0-46c5-4cbb-88d0-042835d9e270` | Zaak- en documentservices 1.2 | 49 |
+| `d84e6b8c-3ebf-46c0-a56d-fcaf14b9bb16` | StUF HR 3.00 | 47 |
+| `8add43d4-0e42-432a-bd97-87db73338dd3` | StUF LVO 3.12 | 46 |
+| `cb3207c9-ac9f-49fa-be07-4f33bd50e8ca` | Zaak- en documentservices 1.0 | 41 |
+| `e214a72b-abd4-4081-bba1-940127731776` | MijnOverheid Berichtenbox koppelvlak 1.8 | 37 |
+| `d5040887-125e-4e25-aa0a-2560af8e0d8c` | GBA koppelvlak LO GBA 3.9 | 37 |
+| `4b4a9606-f7af-4e7e-b730-9f99d60f1d78` | StUF WOZ LV koppelvlak 03.12 | 32 |
+| `f6aff5e6-fa8a-448a-9433-a1cddf53a6ed` | DigiD SAML koppelvlak (actueel) | 30 |
+| `0d6c4d6b-bdd0-4956-977d-b065deda3ffe` | Documentcreatieservices 1.0 | 30 |
+| `419ba65d-7202-4195-babd-e6a1d493bfd4` | BRK Levering 1.2.1 | 28 |
+| `90ecec54-7ace-4ba4-8ee3-3009b273654c` | StUF Jeugdzorg (actueel) | 26 |
+| `b8844332-e17b-4133-b251-7ef495365a63` | StUF Geo IMGeo 1.2 (BGT) | 24 |
+| `6138b617-b541-4ffe-b15b-3841b285fe74` | StUF BAG 3.10 | 24 |
+| `10f03dfc-bf41-45c4-a74b-102570d4f6f2` | StUF BG 3.10 | 21 |
+| `57a5a000-fe8f-449f-8681-77ecb07fc80e` | PDOK services (actueel) | 20 |
+| `c16c70f9-be09-4da7-aece-8934bc6275f5` | BAG LV koppelvlak 1.3 | 19 |
+| `da125fb3-4b26-49d2-9eb8-803880599c53` | iWmo voor gemeenten 2.0 | 18 |
+| `6ee8a456-0673-4d8c-adfa-c19085fdca5b` | Prefill eFormulieren services 1.0 | 18 |
+| `0f9a7492-2941-4016-8232-62cd8ab34b6c` | Afsprakenstelsel eHerkenning - Koppelvlak DV-HM 1.9 | 17 |
+| `49bc7eee-dee3-4f4f-bb9a-0ad61b7c71a7` | BAG LV koppelvlak 1.2.1 | 16 |
+| `58956352-ac3e-4265-a6d9-4523847721e9` | StUF Wkpb 0102 | 14 |
+| `5c0933fa-d754-496e-bd77-14b503c1bcd9` | Betalen en invorderen services 1.0 | 13 |
+| `08826442-d8b7-49e4-bcfe-988a17c25875` | BAG-GBA 1.2 koppelvlak | 13 |
+| `9fb50214-cf14-41b3-a456-b14485889612` | IMGeo (BGT) 2.1 | 12 |
+| `2350b79a-5b8d-4ecf-a5c0-32417092539b` | StUF Geo IMGeo 1.3 (BOR) | 12 |
+| `36fc0f55-8d37-439f-a59e-785fea38ce4d` | GBA koppelvlak LO GBA 3.10 | 12 |
+| `1830f74f-6566-4cbe-8531-24bb68129784` | OWMS 4.0 | 11 |
+| `b8428649-e1ab-4d49-86a2-8598c0409f0b` | StUF WOZ 03.12 | 11 |
+| `2c11bc5a-4d6e-4fcd-a7fc-6b91f6f703b6` | BRK Levering 1.1 | 11 |
+| `41e64c64-ce9a-4ad6-8091-75d95a7ef414` | RO Standaarden (actueel) | 11 |
+| `2fca69e5-c20e-48cf-8399-6fe57b60dccc` | Zaak- en documentservices 1.1 | 11 |
+| `d160870e-17cf-4509-bc6e-fbf181477a7a` | SuwiML berichtstandaard 3.1 | 11 |
+| `eac104a2-9827-46cf-be55-f41387c84d1f` | MijnOverheid Berichtenbox koppelvlak 1.5 | 11 |
+| `bad73517-2cf4-456b-b6cf-4b07e5acc20b` | DigiD CGI koppelvlak (actueel) | 9 |
+| `b6b34b59-6d11-4a9d-997e-ae1146f498a5` | StUF-koppelvlak Geo BAG 1.0 | 9 |
+| `bc69620e-5532-46a5-81f5-b03596fb36c2` | CMIS 1.0 | 9 |
+| `76f48a97-e758-4a6d-86eb-507103a3bab1` | StUF Geo IMGeo 1.1.1 | 9 |
+| `125f5294-db9f-4910-891c-c3102da9636c` | iJw voor gemeenten 2.0 | 8 |
+| `6d56c655-6716-477e-9f33-3b64a74c36ea` | CMIS 1.1 | 8 |
+| `c5b3013d-ee04-4271-8015-30fb2517adf2` | MijnOverheid WOZ-inzage koppelvlak 1.1 | 8 |
+| `b2a77881-c1a8-4c63-805c-9a03e367ac3e` | StUF LVO 3.11 | 8 |
+| `e3f16bd0-860c-4c90-b85c-b40140f19436` | iWmo voor gemeenten 2.3 | 7 |
+| `068a01cb-0f0e-4701-9dae-3e044197e4e9` | BRK Levering 2.0 | 7 |
+| `bc0289bc-409c-4dd4-9630-763333093dd6` | iWmo voor gemeenten 3.0 | 7 |
+| `536e48e9-d74e-4be1-8a85-1de4dac4d66f` | iJw voor gemeenten 2.3 | 7 |
+| `6a7aa891-b445-48de-9b7e-3563a42b0324` | StUF Jeugdzorg (actueel) | 7 |
+| `16e94a83-9499-4e3b-bd1b-66a8545c84c9` | iWmo voor gemeenten 2.1 | 6 |
+| `5a06ba22-3907-4b92-91c0-9a31e395c444` | Zaken API-standaard v1.x | 6 |
+| `446b06a8-9906-4856-bdce-639282d057cc` | Afsprakenstelsel eHerkenning - Koppelvlak DV-HM 1.9 | 6 |
+| `ff7179f7-440e-4918-8c6e-3d430ea21eb6` | StUF onderlaag 3.01 | 6 |
+| `63465380-25c0-4cf9-ad27-a8aeef02e3a9` | Digikoppeling adapter intern 1.0 | 6 |
+| `8d07098e-be2f-4822-95e7-72da1f25e141` | iJw voor gemeenten 2.4 | 5 |
+| `113b6fc8-ced9-46b6-bf13-1c78e732d4be` | RSGB 2.01 | 5 |
+| `1e5a9623-2a48-44d9-8fb0-694e083199d2` | StUF ZKN 3.10 | 5 |
+| `2256cd6e-d85c-479f-9fc9-216a6bdad5f4` | BAG Bevragen 2.0 | 5 |
+| `6bc1858b-4059-4536-80e4-45d1e280c818` | iJw voor gemeenten 1.0 | 5 |
+| `b934f52f-a170-4e6c-a65b-d24d30993c63` | StUF Geo IMGeo 1.2 (BGT) | 5 |
+| `99331eed-ba49-4608-898e-3cdda9aaaff9` | iWmo voor gemeenten 2.2 | 4 |
+| `dfbd1ec7-8a39-4ac6-baf6-4078a63dcca7` | StUF ZKN 2.01 | 4 |
+| `4edb406c-f544-4b31-b35b-4074e5a79ed9` | STAM - Aanvragen en meldingen (actueel) | 4 |
+| `c7f88541-8fd3-49c2-a9d4-2623497e83db` | Digikoppeling ebMS 1.0 / 1.1 | 4 |
+| `85de1d54-874a-412f-a8a2-d6dcaf537838` | iWmo voor gemeenten 1.0 | 4 |
+| `15f4a79a-b121-4dcb-a9a1-f1efe4578990` | Samenwerkende Catalogi 4.0 | 4 |
+| `79d214b4-53a1-48d7-9ab7-8654cc255d8b` | BAG Extract 2.1 | 4 |
+| `895f3d52-14cd-4492-a449-3fef9a7ad660` | Documenten API-standaard v1.x | 4 |
+| `e9913866-dde0-4c64-b163-02fe4762e241` | StUF EF 3.15 | 4 |
+| `8041f2ef-bd51-49bb-b1c5-1b5acb319c46` | BMKL 1.8 (Berichten Model Kabels en Leidingen) | 4 |
+| `3c6ac257-7816-459b-be5f-eff21717cc09` | MijnOverheid Berichtenbox koppelvlak 1.8 | 4 |
+| `dcf55e8b-0dfe-4d6f-894b-a7fb96cd8296` | Webrichtlijnen versie 2 (WRv2) | 4 |
+| `60533192-9437-4ae9-b1a6-d4df8a3636a7` | BAG Compact 1.0 | 4 |
+| `7533dd8e-3e35-4ccd-8fee-98c3da0c24ae` | RGBZ 1.0 | 3 |
+| `d2b4c180-4a09-4e4b-8d4f-c40d1d190a95` | PDF/A-1 (NEN-ISO 19005-1:2005 en) | 3 |
+| `52fbdbe4-aea1-46c7-87a3-8181759580f0` | Regie- en zaakservices 1.0 | 3 |
+| `5f1e7bac-acf3-4a4a-8cdc-3503b035ebc7` | iJw voor gemeenten 3.0 | 3 |
+| `85964d69-5c93-4af6-b11c-3d44ae1a11b0` | Digikoppeling ebMS 2.0 | 3 |
+| `78d270c7-414b-491e-b8d5-cdf978943a72` | StUF LVO 3.05 | 3 |
+| `c71feb05-1cf5-4de2-99dd-1c93ac94d0ad` | SimplerInvoicing-UBL (actueel) | 3 |
+| `f2b8bc6c-b7fc-47e3-a6c0-d8f1fff88e40` | iWmo voor gemeenten actueel | 3 |
+| `1740ac99-d0ea-4250-8560-34a6c6951ddc` | StUF tax (actueel) | 3 |
+| `2d32c672-fcac-431b-bfe5-dc634edc01b1` | SuwiML berichtstandaard 3.1 | 3 |
+| `2fe10188-ed2a-407f-a08a-2f70311afffe` | UBL-OHNL (actueel) | 2 |
+| `3539546b-1078-4182-83d7-986ee5c6ec86` | Digimelding (actueel) | 2 |
+| `e6b10031-3809-44f3-aa28-5d468de2ccf7` | BRV Interactieve/online gegevensdiensten 2.0 | 2 |
+| `c4cd12ce-b7e7-4a4b-ae7e-48e162a497b6` | iWmo voor gemeenten 2.4 | 2 |
+| `e993792d-fa44-4b5a-96b7-d98e5c79c22d` | StUF WOZ 03.11 | 2 |
+| `e03f171d-08b0-48f1-b225-6a6093fda6ce` | BRT Levering 2.2 | 2 |
+| `541f204c-9893-4abd-adaf-edc5e47e0a31` | StUF LVO 3.10 | 2 |
+| `40d6e9ec-d54e-4e08-8318-17c2d0034800` | Peppol BIS Billing (actueel) | 2 |
+| `0df441b5-37ec-43a1-a82c-43b2939b4302` | IMGeo Grootschalige geografie 2.0 | 2 |
+| `eb6e8e5b-409d-423a-a6c6-fa2c451c8d45` | SETU 1 | 2 |
+| `5e586783-e5d6-4c22-8b32-79722c55d61e` | Autorisaties API-standaard v1.x | 2 |
+| `7d30a771-4d99-48be-ad56-e3147a4cb5ba` | Afsprakenstelsel eHerkenning - Koppelvlak DV-HM 1.9 | 2 |
+| `8a5fb787-778a-4673-a72e-9fe920a9d8a3` | iJw voor gemeenten 2.2 | 2 |
+| `6e3afda0-bc39-473b-a3d5-989f6307809f` | BMKL 1.4 (Berichten Model Kabels en Leidingen) | 2 |
+| `5cbd1965-5cdd-4913-8318-72337836e617` | SAML 2.0 | 2 |
+| `e4d0385d-01cf-4230-b8d7-1c385220be02` | GBA koppelvlak LO GBA 3.8 | 2 |
+| `1839092f-0730-42b7-93ac-6f2b813f21a4` | NLCS (actueel) | 2 |
+| `3efe54fb-fdec-4a59-86b0-5c4eba237f7a` | ArchiMate File Format 3.0 | 2 |
+| `1a037416-95c3-41ce-9db8-91509e33c820` | SuwiML berichtstandaard (actueel) | 2 |
+| `af75e665-5c1c-480d-b9aa-64a939e1f8d9` | MijnOverheid Lopende zaken koppelvlak 1.0 | 2 |
+| `393eb771-bff1-4d9f-aa3e-20bcb9caaa26` | iJw voor gemeenten actueel | 1 |
+| `7b18b7d7-7056-4d85-ac6a-a6e1c6c761fd` | imZTC 2.1 | 1 |
+| `9386c5b7-e361-482c-ad18-2b35007ec68c` | StUF ZTC 3.10 | 1 |
+| `7e0e7d31-8bc3-40b7-a7ba-d8c8c7f48d7f` | StUF BG 2.04 | 1 |
+| `b78dc042-dbb2-45a9-bdea-e8f32197278a` | Zaken API-standaard v1.x | 1 |
+| `be4dc774-959b-4bfa-82f3-44479f3f2c23` | AIG service 1.2 | 1 |
+| `0b4d2ba9-047c-4e13-b980-c730efb7f022` | Samenwerken (actueel) | 1 |
+| `e512e1de-37f4-4e10-b0bd-83558ae3d65d` | StUF EF 3.12 | 1 |
+| `7dafac52-4eef-4f6f-a2af-d607177441ad` | BRV Abonnementen 2.0 | 1 |
+| `b54c4659-7332-4b78-8b46-67b6bf173bfe` | StUF Kadastrale mutatieservices 1.0 | 1 |
+| `edc05380-1ea0-4060-9dbb-fce87755099e` | BAG bevragen API-standaard (actueel) | 1 |
+| `45c38e17-1da9-4492-9d7c-c01eec782875` | Geo-BOR 1.1 | 1 |
+| `7ab9b37c-7b25-497c-9557-b9b00f97ed2d` | PDF/A-2 (ISO 19005-2) | 1 |
+| `cbc7f6f0-a1ed-476c-8869-3823d2a6b795` | i-Spiegel exportspecificatie 1.0 | 1 |
+| `60750a4d-8c29-4f19-a208-c961b29085cf` | imZTC 2.1 Informatiemodel | 1 |
+| `320fc111-2e46-410d-8bf5-0f773df4e495` | BAG LV koppelvlak 1.2.1 | 1 |
+| `0983a655-d688-4f2c-8567-8a1cad856a20` | BRP Personen bevragen API-standaard (actueel) | 1 |
+| `15308ae8-d02c-42de-91fe-ad0051acd42e` | UBL-OHNL 1.9 | 1 |
+| `3a7a61ca-f243-4f3f-98d6-a59342da6997` | Digitoegankelijk versie 1.1.2 | 1 |
+| `5dd962ce-ae8e-44fa-b957-d3af448cef9f` | Digikoppeling WUS 2.0 | 1 |
+| `87d055e5-3c83-449f-9a78-bf0c1899659a` | iJw voor gemeenten 2.3 | 1 |
+| `7315b27e-35a4-464e-afbd-6f4baa4778f2` | ODF 1.2 | 1 |
+| `2e3631b6-a3c5-47a1-a5d6-b57dc86a39e6` | RGBZ 2.0 | 1 |
+| `75478dd6-5e3f-424f-8c29-f9c1c012d7ae` | Webrichtlijnen versie 1 (WRv1) | 1 |
+| `783d6599-c89a-4497-8471-cebcbc55a9a7` | Digikoppeling WUS 3.0 | 1 |
+| `8f227080-169a-47f4-a074-a2f92773007c` | EN 301 549 versie 2.1.2 (WCAG 2.1) | 1 |
+| `ecafcdad-2f47-4903-a6f5-2419c91b6a61` | GEMMA e-formulieren specificatie 1.4 | 1 |
+| `816bc44e-ea72-4aeb-be49-eac4b4da8d37` | StUF protocolbindingen 03.02 | 1 |
+| `ab109cf9-16e2-41db-8bb6-6c6397a3b154` | RSGB 3.0 | 1 |
+| `c567f061-f71f-48ab-8962-3b6c56eb24c5` | imZTC 1.0 | 1 |
+| `ec71a135-8cdb-423a-b62a-f6c73a866ffc` | SimplerInvoicing-UBL 1.1 | 1 |
+| `724a6b93-5216-4199-866d-04f47c9ec2f6` | IMKL Kabels en Leidingen 1.2 | 1 |
+
+### Reactie 6 — @Makkmetp (2026-03-03)
+
+Er gaan nu meerdere issues door elkaar lopen:
+#312 - De koppeling heeft verplicht een naam
+En dit issue.
+
+Laten #312 behouden voor de keuze qua naamgeving en dit issue om de geimporteerde data na te gaan.
+
+### Reactie 7 — @markbacker (2026-03-04)
+
+@rubenvdlinde, dank voor de uitgebreide analyse.
+
+Ik zie in het datamigratie bestand koppelingen.csv bovenstaande fouten nu ook. Oorzaak daarvan kan ik deels verklaren:
+- niet bestaande standaardversie-id's staan verkeerd in de huidige SWC exportdatabase (vermoedelijk de UUID van in Drupal ipv de property Object ID).
+ - Oplossing is om de id's in de datamigratie-scripts op te halen met de standaardversienaam.
+ - Waarschijnlijk is er geen enkele geldige standaardversie
+- ontbrekende ApplicatieA of ApplicatieB vergt nader onderzoek
+
+Het issue over de naam wordt goed gedekt met #312.
+
+Dit issue wordt gemarkeerd als datamigratie issue.
diff --git a/issues/402.md b/issues/402.md
new file mode 100644
index 00000000..66c24760
--- /dev/null
+++ b/issues/402.md
@@ -0,0 +1,39 @@
+# #402 — Verschil tussen Edge en Chrome bij laden applicaties
+
+**Status:** OPEN | **Labels:** Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-02-03
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/402
+
+---
+
+## Beschrijving
+
+Links Edge en rechts Chrome
+Ingelogd met dezelfde gebruiker, maar toch zie ik wat anders.
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2026-02-03)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2026-02-19)
+
+We hebben met 3 mensen getest maar kunnen dit niet reproduceren.
+
+
+
+### Reactie 3 — @remko48 (2026-02-19)
+
+https://backend.accept.opencatalogi.nl/s/w3k9SoKNjZ2caks?dir=/&editing=false&openfile=true
diff --git a/issues/403.md b/issues/403.md
new file mode 100644
index 00000000..0e6c5678
--- /dev/null
+++ b/issues/403.md
@@ -0,0 +1,46 @@
+# #403 — Tekst verwijderen aanpassen
+
+**Status:** CLOSED | **Labels:** Aanbod, Wijziging
+**Auteur:** @Makkmetp | **Datum:** 2026-02-03
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/403
+
+---
+
+## Beschrijving
+
+De tekst bij het verwijderen van een applicatie, dienst of koppeling dient nog aangepast te worden.
+Per objecttype is deze tekst mogelijk anders.
+Tekst nog aanleveren
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2026-02-03)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @markbacker (2026-02-04)
+
+De {applicatie|dienst | koppeling} "" wordt niet gebruikt door gemeenten of samenwerkingen en kan veilig worden verwijderd.
+
+of
+
+De {applicatie|dienst | koppeling} "" wordt gebruikt door onderstaande gemeenten en/of samenwerkingen en kan niet worden verwijderd.
+
+### Reactie 3 — @Makkmetp (2026-02-25)
+
+✅ De tekst voor het verwijderen van een dienst, applicatie of koppeling die niet gebruikt wordt is gelijk aan het voorstel.
+
+De optie wanneer een applicatie in gebruik is door een gemeente of samenwerking is niet getest, omdat het lijkt dat applicaties niet meer door een gemeente of samenwerking in gebruik zijn.
+
+
diff --git a/issues/404.md b/issues/404.md
new file mode 100644
index 00000000..7b62414c
--- /dev/null
+++ b/issues/404.md
@@ -0,0 +1,34 @@
+# #404 — Regelmatig witte schermen
+
+**Status:** OPEN | **Labels:** Organisatie en configuratie
+**Auteur:** @Makkmetp | **Datum:** 2026-02-03
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/404
+
+---
+
+## Beschrijving
+
+Regelmatig komen er witte schermen naar voren in Edge.
+Het terugzetten naar fabrieksinstellingen van de browser is daarin een oplossing.
+Cache en geschiedenis legen helpt niet altijd.
+
+
+
+---
+
+## Reacties (2)
+
+### Reactie 1 — @github-actions (2026-02-03)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-02-11)
+
+We hebben dit gedrag nu al een week niet meer gezien op de test omgeving, ik ga hem doorzetten naar review maar we houden hem nog even aan.
diff --git a/issues/406.md b/issues/406.md
new file mode 100644
index 00000000..4d3754e8
--- /dev/null
+++ b/issues/406.md
@@ -0,0 +1,39 @@
+# #406 — SiteImprove verwijderen
+
+**Status:** CLOSED | **Labels:** Organisatie en configuratie
+**Auteur:** @Makkmetp | **Datum:** 2026-02-04
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/406
+
+---
+
+## Beschrijving
+
+In de code van het template staat een verwijzing naar siteimprove. Deze graag verwijderen, aangezien we daar geen gebruik van (willen) maken. De enige tag van een andere partij is die van Piwik, zoals eerder aangeleverd.
+
+Het betreft:
+
+
+
+
+---
+
+## Reacties (3)
+
+### Reactie 1 — @github-actions (2026-02-04)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-02-04)
+
+@remko48 we moeten even controleren dat we zowiezo maar één env positie hebben voor een tracing en meet script
+
+### Reactie 3 — @Makkmetp (2026-02-23)
+
+✅Siteimprove script wordt niet meer gevonden in de code en lijkt verwijderd.
diff --git a/issues/407.md b/issues/407.md
new file mode 100644
index 00000000..97fb0167
--- /dev/null
+++ b/issues/407.md
@@ -0,0 +1,35 @@
+# #407 — Toegevoegde standaarden verwijzen naar id-id-....
+
+**Status:** OPEN | **Labels:** Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-02-04
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/407
+
+---
+
+## Beschrijving
+
+Wanneer je bij een applicatie naar het tabblad standaarden gaat en klikt op een link van de toegevoegde standaarden, dan wordt "id-" in de link dubbel toegevoegd. Je komt dan terecht op een 404-pagina van GEMMA Online.
+
+Applicatie: https://softwarecatalogus.accept.opencatalogi.nl/publicatie/2bf12d87-4382-4eea-b31b-c97e6d5c82fd
+Toegevoegde standaard link: https://www.gemmaonline.nl/wiki/GEMMA/id-id-beba0771-8db0-11e3-67ab-0050568a6153
+
+
+
+---
+
+## Reacties (2)
+
+### Reactie 1 — @github-actions (2026-02-04)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-02-04)
+
+Check, dit komt doordat we sinds enige tijd de daadwerkenlijke standaard id's gebruiken maar we hier vroeger een hack in hadden. @remko48 kan jij deze oppakken?
diff --git a/issues/408.md b/issues/408.md
new file mode 100644
index 00000000..15b96e0c
--- /dev/null
+++ b/issues/408.md
@@ -0,0 +1,54 @@
+# #408 — Tabblad beschrijving bij Dienst
+
+**Status:** CLOSED | **Labels:** Aanbod
+**Auteur:** @Makkmetp | **Datum:** 2026-02-04
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/408
+
+---
+
+## Beschrijving
+
+Na het aanmaken van een dienst zie ik plots een tabblad "Beschrijving" met een getal erin. Dit tabblad hoort hier niet.
+
+De link naar de dienst is:
+https://softwarecatalogus.accept.opencatalogi.nl/publicatie/cf5ed643-aeb5-4045-94ea-99cb2473aba0
+
+
+
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2026-02-04)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-02-04)
+
+@Makkmetp heb je toevallig 13 als lange.uitgebreide beschrijving opgegeven bij het aanmaken en bewerken van de dienst?
+
+
+### Reactie 3 — @Makkmetp (2026-02-04)
+
+In de uitgebreide omschrijving staat niets:
+
+
+
+### Reactie 4 — @Makkmetp (2026-02-25)
+
+✅ Een nieuwe dienst aangemaakt met dezelfde content. Het tabblad is niet meer zichtbaar.
+
+
+
+
+### Reactie 5 — @markbacker (2026-02-25)
+
+Akkoord
diff --git a/issues/409.md b/issues/409.md
new file mode 100644
index 00000000..92e8c2bd
--- /dev/null
+++ b/issues/409.md
@@ -0,0 +1,41 @@
+# #409 — Footer anders: inlog of uitgelogd
+
+**Status:** CLOSED | **Labels:** Organisatie en configuratie
+**Auteur:** @Makkmetp | **Datum:** 2026-02-04
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/409
+
+---
+
+## Beschrijving
+
+De footers verschillen wanneer je ingelogd bent of uitgelogd.
+
+De privacyverklaring en Algemene voorwaarden wijzen nu ook naar andere pagina’s wanneer je ingelogd bent of niet en dat is raar.
+Graag naar dezelfde laten verwijzen en de footers gelijk maken.
+
+---
+
+## Reacties (4)
+
+### Reactie 1 — @github-actions (2026-02-04)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2026-02-04)
+
+@Makkmetp kan zijn er zijn inderdaad vanuit testen nog twee varianten van de footer menu's zijn, die kunnen we aanpassen maar dan is wel natuurlijk meteen de vervolgvraag hebben we al ergens een definitief lijstje van de footers gemaakt?
+
+### Reactie 3 — @rubenvdlinde (2026-02-11)
+
+We kunnen deze gek genoeg niet repliceren, voor de zekerheid nog één keer dubbelchecken, dan @WilcoLouwerse nog een keer dubbelchekcen en dan terug naar vng
+
+### Reactie 4 — @Makkmetp (2026-02-24)
+
+✅Vreemd, maar gelukkig zijn de footers nu weer gelijk en verwijzen de links naar dezelfde pagina's
diff --git a/issues/410.md b/issues/410.md
new file mode 100644
index 00000000..cdeaed1d
--- /dev/null
+++ b/issues/410.md
@@ -0,0 +1,69 @@
+# #410 — Dashboard schrijfwijze softwarecatalogus
+
+**Status:** OPEN | **Labels:** Organisatie en configuratie, Tekstuele wijzigingen, IGS review
+**Auteur:** @Makkmetp | **Datum:** 2026-02-04
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/410
+
+---
+
+## Beschrijving
+
+Op het dashboard wordt softwarecatalogus op meerdere manieren geschreven. Graag dit aanpassen naar de volgende schrijfwijze:
+
+softwarecatalogus
+
+
+
+
+---
+
+## Reacties (5)
+
+### Reactie 1 — @github-actions (2026-02-04)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @remko48 (2026-02-12)
+
+De schrijfwijze is zoals het in de powerpoint staat
+
+
+### Reactie 3 — @Makkmetp (2026-02-18)
+
+Hierbij de tekst voor de leveranciers:
+
+**Welkom in uw softwarecatalogus**
+
+Via deze omgeving publiceert en beheert u uw aanbod voor gemeenten.
+Hier legt u vast:
+
+- welke applicaties en diensten u aanbiedt
+- welke koppelingen beschikbaar zijn
+- hoe uw oplossing aansluit op de GEMeentelijke Model Architectuur (GEMMA)
+- dat uw applicatie beschikbaar is voor opname in het gemeentelijke applicatielandschap
+
+Wilt u een nieuwe applicatie, dienst of koppeling publiceren? Gebruik dan de acties bovenaan deze pagina.
+Een overzicht van uw reeds gepubliceerde applicaties, diensten en koppelingen vindt u via het linkermenu.
+
+Gemeenten gebruiken deze informatie bij het vergelijken, selecteren en inkopen van applicaties. Zorg daarom dat uw gegevens volledig en actueel zijn.
+
+
+
+### Reactie 4 — @rubenvdlinde (2026-02-19)
+
+Hiermee is #255 beantwoord
+
+### Reactie 5 — @Makkmetp (2026-02-24)
+
+❌Zoals meerdere keren besproken en aangegeven is de schrijfwijze van softwarecatalogus zonder hoofdletter. In de tekst hierboven staat deze ook niet.
+
+
+
+
diff --git a/issues/430.md b/issues/430.md
new file mode 100644
index 00000000..6b28b60b
--- /dev/null
+++ b/issues/430.md
@@ -0,0 +1,32 @@
+# #430 — Applicaties: beheertabel toont kolom Compliancy met applicatienamen
+
+**Status:** OPEN | **Labels:** IGS nieuw
+**Auteur:** @markbacker | **Datum:** 2026-02-24
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/430
+
+---
+
+## Beschrijving
+
+In de Applicaties beheertabel wordt de kolom compliancy getoond met applicatienamen. Dat is gewoon fout.
+
+Oplossing is om deze kolom weg te halen. Per applicatie is er geen zinvolle waarde om te tonen. Dit kan alleen op de applicatiedetailpagina in de ondersteunde .standaardentabel.
+
+
+
+
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-02-24)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/431.md b/issues/431.md
new file mode 100644
index 00000000..72c165f5
--- /dev/null
+++ b/issues/431.md
@@ -0,0 +1,34 @@
+# #431 — Aanmeldpoces: tussenvoegsel niet meer aanwezig
+
+**Status:** OPEN | **Labels:** IGS nieuw
+**Auteur:** @Makkmetp | **Datum:** 2026-02-25
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/431
+
+---
+
+## Beschrijving
+
+In een eerdere fase wat het veld "Tussenvoegsel" nog aanwezig in het aanmeldproces. Zie bijvoorbeeld issue #139.
+Deze is niet meer aanwezig. Graag herstellen.
+
+
+
+
+
+
+
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-02-25)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/432.md b/issues/432.md
new file mode 100644
index 00000000..9b9716f2
--- /dev/null
+++ b/issues/432.md
@@ -0,0 +1,58 @@
+# #432 — Koppeling: Naamgeving van koppeling niet consistent
+
+**Status:** OPEN | **Labels:** Organisatie en configuratie, IGS nieuw
+**Auteur:** @Makkmetp | **Datum:** 2026-02-25
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/432
+
+---
+
+## Beschrijving
+
+Voor het importeren van de koppelingen is #433 aangemaakt.
+
+De naamgeving van een koppeling is op diverse plekken verschillend. Dit kan met de import van de koppelingen te maken hebben. Daarvoor is een ander issue aangemaakt.
+
+Dit is hoe het geregistreerd is:
+
+
+Dit is hoe het wordt getoond in het Koppelingen overzicht:
+
+
+Dit is hoe het wordt getoond onder het overzicht Applicaties in de kolom Koppelingen:
+De koppeling heeft betrekking op Key2Belastingen - deze is nu leeg
+Bij Key2Betalen staan koppelingen met undefind
+
+
+
+Hier een voorbeeld van twee geïmporteerde koppeling voor Key2Betalen. Daarbij valt op dat de tweede applicatie op Select... staat. Maar in het controle formulier wel een applicatie B heeft.
+
+
+
+
+
+Bij het verwijderen van een koppeling heeft deze in het venster ook een andere naam:
+
+
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-02-25)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+---
+
+## Kruisverwijzingen
+
+_Toegevoegd op 2026-03-02 op basis van API-testresultaten._
+
+- **Gerelateerd aan #433:** Gerelateerd aan #433 (import veldmapping). Inconsistente koppeling-namen worden veroorzaakt door foutieve import-mapping.
diff --git a/issues/433.md b/issues/433.md
new file mode 100644
index 00000000..9eedb792
--- /dev/null
+++ b/issues/433.md
@@ -0,0 +1,53 @@
+# #433 — Import: koppelingen lijkt niet goed te gaan
+
+**Status:** OPEN | **Labels:** Organisatie en configuratie, IGS nieuw, IGS datamigratie
+**Auteur:** @Makkmetp | **Datum:** 2026-02-25
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/433
+
+---
+
+## Beschrijving
+
+Dit issue is gerelateerd aan #432, waarbij de naamgeving van koppelingen op diverse plekken anders is.
+
+Het lijkt erop dat de velden niet of de verkeerde velden wel worden gevuld.
+Hier een voorbeeld van twee geïmporteerde koppeling voor Key2Betalen. Daarbij valt op dat de tweede applicatie op Select... staat. Maar in het controle formulier wel een applicatie B heeft.
+
+
+
+
+
+In het overzicht van de koppelingen wordt weer de opgevoerde naam gebruikt.
+
+
+
+In het applicatie overzicht in de kolom koppelingen weer een andere naamgeving met de waarden uit het veld van de 2e applicatie.
+
+Graag ervoor zorgen dat de import in de juiste velden komen.
+
+---
+
+## Reacties (2)
+
+### Reactie 1 — @github-actions (2026-02-25)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @markbacker (2026-03-04)
+
+Zie ook #401. Wordt mogelijk door fouten in import-bestand veroorzaakt
+
+---
+
+## Kruisverwijzingen
+
+_Toegevoegd op 2026-03-02 op basis van API-testresultaten._
+
+- **Gerelateerd aan #432, #401:** Gerelateerd aan #432 (naamconsistentie) en #401 (lege koppeling-kaarten). De import-veldmapping beïnvloedt beide.
diff --git a/issues/434.md b/issues/434.md
new file mode 100644
index 00000000..4f5d0642
--- /dev/null
+++ b/issues/434.md
@@ -0,0 +1,32 @@
+# #434 — Contactpersoon: eerste account van leveranciers niet beschikbaar als contactpersoon
+
+**Status:** OPEN | **Labels:** IGS nieuw
+**Auteur:** @markbacker | **Datum:** 2026-02-25
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/434
+
+---
+
+## Beschrijving
+
+Nieuwe leveranciers Chaplin heeft met het eerste account geen contactpersoon. Kan aan applicatie geen contactpersoon toevoegen.
+
+Een tweede toegevoegde contactpersoon is wel zichtbaar in beheer > contactpersonen en kan ook geselecteerd worden als contactpersoon van een applicatie.
+
+
+
+
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-02-25)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/435.md b/issues/435.md
new file mode 100644
index 00000000..beba11dc
--- /dev/null
+++ b/issues/435.md
@@ -0,0 +1,59 @@
+# #435 — Import applicatie: niet alle geïmporteerde applicaties zichtbaar
+
+**Status:** OPEN | **Labels:** IGS nieuw
+**Auteur:** @markbacker | **Datum:** 2026-02-25
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/435
+
+---
+
+## Beschrijving
+
+❌Het aantal applicaties bij de voorgestelde leverancier Centric komen niet overeen.
+
+- In de huidige softwarecatalogus heeft Centric - 39 pakketten opgevoerd,Shift2 - 26 pakketten en Horlings & Eerbeek Automatisering B.V. - 11 pakketten.
+- In het importbestand module.csv staan er voor Centric ook 39 applicaties die door Centric geregistreerd zijn.
+
+In de vernieuwde softwarecatalogus:
+Zonder inloggen heeft:
+❌Centric - 32 applicaties
+✅Shift2 - 26 applicaties
+✅Horlings & Eerbeek Automatisering B.V - 11 applicaties
+
+Ingelogd als gebruiker van de leverancier:
+❌Centric - 32 applicaties inclusief een zelf aangemaakt pakket
+✅Shift2 - 26 applicaties
+✅Horlings & Eerbeek Automatisering B.V - 11 applicaties
+
+Huidige softwarecatalogus:
+
+
+
+
+
+
+Vernieuwde softwarecatalogus:
+
+
+
+
+Ingelogd als gebruiker van de leverancier:
+
+
+
+
+
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-02-25)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/436.md b/issues/436.md
new file mode 100644
index 00000000..e5e4e50d
--- /dev/null
+++ b/issues/436.md
@@ -0,0 +1,28 @@
+# #436 — error bij het ophalen van het applicatie overzicht
+
+**Status:** OPEN | **Labels:** IGS nieuw
+**Auteur:** @markbacker | **Datum:** 2026-02-25
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/436
+
+---
+
+## Beschrijving
+
+Een error bij het ophalen van het applicatie overzicht:
+
+
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-02-25)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/437.md b/issues/437.md
new file mode 100644
index 00000000..084f88e1
--- /dev/null
+++ b/issues/437.md
@@ -0,0 +1,38 @@
+# #437 — Geimporteerde leverancier: nieuwe koppeling opslaan geeft foutmelding
+
+**Status:** OPEN | **Labels:** Aanbod, Koppeling, IGS nieuw
+**Auteur:** @Makkmetp | **Datum:** 2026-02-26
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/437
+
+---
+
+## Beschrijving
+
+Onder Centric is een nieuwe gebruiker aangemaakt. Daarmee is ingelogd.
+Via Applicaties > ...Acties > Koppeling publiceren zijn meerdere pogingen gedaan om een koppeling toe te voegen aan een mede geimporteerde applicatie. Bij het opslaan komt er telkens een 400-error naar voren.
+In de log staan ook direct meerdere Errors.
+
+Graag ervoor zorgen dat geimporteerde leveranciers bij geimporteerde applicaties ook een Koppeling kunnen publiceren
+
+
+
+
+
+
+
+
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-02-26)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/438.md b/issues/438.md
new file mode 100644
index 00000000..dbfa8414
--- /dev/null
+++ b/issues/438.md
@@ -0,0 +1,47 @@
+# #438 — Zoeken: verschillende vormgeving Diensten na filteren
+
+**Status:** OPEN | **Labels:** Aanbod, Zoeken, IGS nieuw
+**Auteur:** @Makkmetp | **Datum:** 2026-02-26
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/438
+
+---
+
+## Beschrijving
+
+Dit issue is gemaakt naar aanleiding van het testen met filters: #345
+
+ Wanneer je zoekt naar een Dienst en via filters deze naar voren haalt, dan heeft deze verschillende vormgeving.
+
+Na filter op:
+Leverancier: Centric & Type: Dienst dan heeft een Dienst het Dienstype in het zoekresultaat.
+
+
+
+Filter je op:
+Leverancier: Centric & Diensttype:Applicatiebeheer dan heeft het resultaat geen Diensttypen in het zoekresultaat.
+
+
+Filter je op Leverancier: Centric dan heeft het resultaat geen Diensttypen in het zoekresultaat.
+
+
+
+Filter je nergens op dan heeft het resultaat geen Diensttypen in het zoekresultaat.
+
+
+
+Graag zien we dat alle zoekresultaten gelijk zijn en allen het Diensttype bevatten.
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-02-26)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/439.md b/issues/439.md
new file mode 100644
index 00000000..34506e3c
--- /dev/null
+++ b/issues/439.md
@@ -0,0 +1,60 @@
+# #439 — Meerdere Errors: o.a. na het openen van Applicatie-overzicht
+
+**Status:** OPEN | **Labels:** IGS nieuw
+**Auteur:** @Makkmetp | **Datum:** 2026-02-26
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/439
+
+---
+
+## Beschrijving
+
+Tijdens het testen zijn meerdere errors opgetreden. Hieronder worden een aantal errors benoemd. Mogelijk helpt dat om de softwarecatalogus stabieler te maken:
+
+Na het openen van het applicatie-overzicht van de nieuwe leverancier Fortuna kwam er een error naar voren:
+
+
+
+Dit kan een hick-up zijn, maar er staan ook een berg php-warnings in de log.
+
+
+
+Na zoeken op de term "gebruik", daarna klikken op het eerste resultaat, leverde een hele berg errors op in de logging en de publicatie kon niet geladen worden:
+
+
+
+
+
+
+
+
+Een wijziging op een geimporteerde organisatie duurde erg lang en gaf errors in de logging:
+Het ging o.a. om het aanpassen van de beschrijving bij Centric en daarna werden er gebruikers accounts op de achtergrond aangemaakt.
+
+
+
+
+
+
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-02-26)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+---
+
+## Kruisverwijzingen
+
+_Toegevoegd op 2026-03-02 op basis van API-testresultaten._
+
+- **Gerelateerd aan #436:** Vergelijkbaar met #436 (error bij ophalen applicatie-overzicht). Mogelijk dezelfde 401-interceptor bug.
diff --git a/issues/440.md b/issues/440.md
new file mode 100644
index 00000000..4611d7e2
--- /dev/null
+++ b/issues/440.md
@@ -0,0 +1,43 @@
+# #440 — Zoeken: Organisatietype teveel aan opties
+
+**Status:** OPEN | **Labels:** IGS nieuw
+**Auteur:** @Makkmetp | **Datum:** 2026-02-26
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/440
+
+---
+
+## Beschrijving
+
+De facet Organisatietype dient alleen gemeente, samenwerking, leverancier en community te tonen.
+Nu staan er teveel opties.
+
+
+
+
+---
+
+## Reacties (2)
+
+### Reactie 1 — @github-actions (2026-02-26)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @markbacker (2026-03-04)
+
+In het voorzieningen register zie ik dat de schema's Koppeling, Organisatie en Dienst allemaal een property `Type` hebben. Dit naast een `koppelingType`, `organisatieType` en `dienstType`. Blijkbaar toont het filter de waarden van de property `Type` van die drie schema's.
+
+Hoe kan het dat properties met dezelfde naam, maar uit verschillende schema's door elkaar heen gehaald worden?
+
+Daarbovenop worden de `{schema}Type` en `Type` properties verschillend gebruikt
+- `dienstType` en `Type` bevatten dezelfde waardelijst
+- `organisatietype` en `Type` bevatten dezelfde waardelijst
+- maar `koppelingType` bevat als waardelijst intern en extern en `type` de waarden voor het transportprotocol
+
+Dit is heel verwarrend en leidt dus tot fouten
diff --git a/issues/441.md b/issues/441.md
new file mode 100644
index 00000000..c32fb1de
--- /dev/null
+++ b/issues/441.md
@@ -0,0 +1,34 @@
+# #441 — Applicaties: mapping van de versies gaat niet goed bij geimporteerde applicaties
+
+**Status:** OPEN | **Labels:** IGS nieuw
+**Auteur:** @Makkmetp | **Datum:** 2026-02-26
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/441
+
+---
+
+## Beschrijving
+
+Ingelogd als een nieuwe gebruiker van Centric.
+wanneer je naar het Applicatie overzicht gaat en de kolom Applicatie Versies (correcte schrijfwijze is applicatieversies) opent, dan is deze alleen gevuld met een "-".
+Ga je daarna één van deze applicaties bewerken en naar het overzicht van de versies gaat, dan zijn de status en de startdatum status leeg.
+Graag ervoor zorgen dat de informatie op de juiste plek komt.
+
+
+
+
+
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-02-26)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/442.md b/issues/442.md
new file mode 100644
index 00000000..d4a16408
--- /dev/null
+++ b/issues/442.md
@@ -0,0 +1,46 @@
+# #442 — Applicaties: opgevoerd document wijzigt van naam naar bewijs_
+
+**Status:** OPEN | **Labels:** IGS nieuw
+**Auteur:** @Makkmetp | **Datum:** 2026-02-26
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/442
+
+---
+
+## Beschrijving
+
+Wanneer je bij een standaardversie een document opvoert met een specifieke naam, in dit voorbeeld Rapport webrichtlijnen 2026.docx, dan wordt in een ander over zicht de naam bewijs_.docx getoond.
+
+Onderstaande is opgevoerd bij het publiceren van een applicatie:
+
+
+
+
+Ga je daarna de applicatie bekijken, dan staat er bij de standaarden als bewijs een andere naam:
+
+
+https://softwarecatalogus.accept.opencatalogi.nl/beheer/applicaties/931d1a0f-92e5-4111-b619-9a894062854e
+
+Graag ervoor zorgen dat de naam van het document, in dit geval Rapport webrichtlijnen 2026.docx, wordt getoond.
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-02-26)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+---
+
+## Kruisverwijzingen
+
+_Toegevoegd op 2026-03-02 op basis van API-testresultaten._
+
+- **Api-dekking via #378:** API-tests voor #442 draaien onder de acceptatiecriteria van #378 (standaarden na wijzigen veranderd). Beide testen de veldstructuur-integriteit bij bewerkingen.
diff --git a/issues/443.md b/issues/443.md
new file mode 100644
index 00000000..7425fa14
--- /dev/null
+++ b/issues/443.md
@@ -0,0 +1,28 @@
+# #443 — Dienst pagina: diensttypen aan elkaar geschreven.
+
+**Status:** OPEN | **Labels:** IGS nieuw
+**Auteur:** @Makkmetp | **Datum:** 2026-03-02
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/443
+
+---
+
+## Beschrijving
+
+Op de pagina van een dienst, zijn de Diensttype: aan elkaar geschreven. Graag deze scheiden door komma's.
+
+
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-03-02)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/444.md b/issues/444.md
new file mode 100644
index 00000000..418145b1
--- /dev/null
+++ b/issues/444.md
@@ -0,0 +1,34 @@
+# #444 — Vormgeving veranderd bij te lange URL's
+
+**Status:** OPEN | **Labels:** IGS nieuw
+**Auteur:** @Makkmetp | **Datum:** 2026-03-02
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/444
+
+---
+
+## Beschrijving
+
+Wanneer je bij een Organisatie & Applicatie & Diensten een erg lange URL opvoert, dan veranderd de vormgeving. Het grijze vlak wordt alsmaar breder, naar hoe langer de URL wordt.
+
+Graag ervoor zorgen dat het grijze vlak niet breder mag zijn dan 400px waarde. en dat de url op een nette manier wordt afgebroken. Bijvoorbeeld naar x karakters de rest vervangen met drie puntjes "..."
+
+
+
+
+
+
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-03-02)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/445.md b/issues/445.md
new file mode 100644
index 00000000..799b71b1
--- /dev/null
+++ b/issues/445.md
@@ -0,0 +1,31 @@
+# #445 — Nieuwe dienst verkeerde afsluitende pagina
+
+**Status:** OPEN | **Labels:** Aanbod, IGS nieuw
+**Auteur:** @Makkmetp | **Datum:** 2026-03-03
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/445
+
+---
+
+## Beschrijving
+
+Wanneer je eerst een dienst hebt bijgewerkt en daarna via het afsluitende scherm op de knop "Nieuwe dienst aanmelden" klikt en via dat proces een nieuwe Dienst publiceert is de afsluitende pagina "Dienst succesvol geupdatet!"
+
+Dit klopt niet aangezien je een nieuwe dienst hebt gepubliceerd en niet hebt bijgewerkt. Graag ervoor zorgen dat de juiste afsluitende pagina gekoppeld is.
+
+
+
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-03-03)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/446.md b/issues/446.md
new file mode 100644
index 00000000..08cde862
--- /dev/null
+++ b/issues/446.md
@@ -0,0 +1,34 @@
+# #446 — Dienst publiceren: tekstuele inconsistenties
+
+**Status:** OPEN | **Labels:** Aanbod, Tekstuele wijzigingen, IGS nieuw
+**Auteur:** @Makkmetp | **Datum:** 2026-03-03
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/446
+
+---
+
+## Beschrijving
+
+Er zitten meerdere tekstuele inconsistenties in de wizard Dienst publiceren.
+
+- [ ] In de controle pagina staat nog "Dienst registreren" in de blauwe knop rechtsonder. Graag aanpassen naar "Dienst publiceren"
+
+
+- [ ] In de afsluitende pagina van de wizard na het bijwerken van een Dienst staat er de tekst "Dienst succesvol geüpdatet!". Graag dit aanpassen naar "Dienst succesvol bijgewerkt". Zoals ook in het groene vlak staat.
+- [ ] De knop "Nieuwe dienst aanmelden" graag veranderen naar "Nieuwe dienst publiceren", zoals de wizard heet (Dienst publiceren)
+
+
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-03-03)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/447.md b/issues/447.md
new file mode 100644
index 00000000..c7600d05
--- /dev/null
+++ b/issues/447.md
@@ -0,0 +1,38 @@
+# #447 — Zoeken: nieuwe leverancier zonder tussenkomst VNG direct vindbaar
+
+**Status:** OPEN | **Labels:** Organisatie en configuratie, IGS nieuw
+**Auteur:** @Makkmetp | **Datum:** 2026-03-03
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/447
+
+---
+
+## Beschrijving
+
+Zojuist is de leverancier Theekop aangemaakt via het Aanmeldformulier.
+Zonder via de back-end iets aan te passen is deze leverancier direct vindbaar via de zoekmachine van de softwarecatalogus.
+
+
+
+In de back end staat de leverancier ook nog als "Concept".
+
+
+
+Dit zorgt ervoor dat kwaadwillende bezoekers direct zichtbare content kunnen publiceren.
+Zie ook #139.
+
+Graag ervoor zorgen dat aangemelde leveranciers eerst door een triage van de VNG moet, voordat deze zichtbaar/vindbaar wordt voor bezoekers, zowel uitgelogd als ingelogd.
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-03-03)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/448.md b/issues/448.md
new file mode 100644
index 00000000..5cc3ad7f
--- /dev/null
+++ b/issues/448.md
@@ -0,0 +1,61 @@
+# #448 — Overzichtspagina's: verschillende vormgeving en acties
+
+**Status:** OPEN | **Labels:** Aanbod, IGS nieuw
+**Auteur:** @Makkmetp | **Datum:** 2026-03-03
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/448
+
+---
+
+## Beschrijving
+
+Om consistentie uit te stralen en gebruiksgemak te verbeteren is het van belang dat alle overzichtspagina's een gelijke uitstraling hebben en de verschillende acties die daarbij horen. Dit geldt voor bezoekers die geen rechten hebben om deze overzichtspagina's aan te passen. Denk daarbij aan een "andere leverancier" die rond kijkt of een gemeente die je bezoekt of niet ingelogde bezoekers.
+
+De pagina's van een Organisatie, Applicatie en Applicatieversies zijn een voorbeeld voor Diensten en Koppelingen.
+- Links de ruimte voor de korte omschrijving en lange beschrijving.
+- Rechts een grijs informatie blok met daarboven het soort pagina waar je naar kijkt + de mogelijke acties om daar vandaan te doen. En alles wat al onder een tabblad valt hoeft niet getoond te worden in een grijs vlak. Bijvoorbeeld bij Applicatieversie hoeft in het grijze vlak niet de applicatie te staan aangezien deze al onder een tabblad staat
+- Daaronder de tabbalden die van toepassing zijn.
+
+
+
+
+
+
+
+De pagina's van een dienst of koppeling zijn op dit moment nog anders gestyled.
+
+
+
+
+
+Bij deze het verzoek om de styling voor alle pagina's gelijk te maken:
+- Links de ruimte voor de korte omschrijving en lange beschrijving.
+- Rechts een grijs informatie blok met daarboven het soort pagina waar je naar kijkt + de mogelijke acties om daar vandaan te doen. Tussen de grijze blokken geen headers
+- - De acties bij Diensten van andere leveranciers: n.v.t.
+- - De acties bij Koppelingen van andere leveranciers: n.v.t.
+- - De acties bij Applicaties van andere leveranciers: Dienst publiceren, Koppeling publiceren
+- - De acties bij Applicatieversies van andere leveranciers: n.v.t.
+- - De acties bij Organisatie: n.v.t.
+- Daaronder de tabbladen die van toepassing zijn.
+- - Bij Diensten: Applicaties (waar de dienst op van toepassing is), Organisaties (die deze dienst aanbieden)
+- - Bij Koppelingen: Applicaties (waar de koppeling op van toepassing is)
+- - Bij Applicaties: Standaarden, Geschikt voor, Organisaties, Applicatieversies, Diensten, Koppelingen
+- - Bij Applicatieversies: Applicaties (waar de versie op van toepassing is)
+- - Bij Organisatie: Applicaties, Diensten
+
+Mogelijk missen er nog acties of tabbladen.
+Zie de relatie met #101
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-03-03)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/449.md b/issues/449.md
new file mode 100644
index 00000000..3111427b
--- /dev/null
+++ b/issues/449.md
@@ -0,0 +1,33 @@
+# #449 — Handleiding facets configureren klopt niet.
+
+**Status:** OPEN | **Labels:** IGS nieuw
+**Auteur:** @markbacker | **Datum:** 2026-03-03
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/449
+
+---
+
+## Beschrijving
+
+❌De handleiding klopt niet. Deze geeft een pad aan, wat niet te volgen is.
+
+
+Volgens de handleiding kom ik dan terecht op de volgende pagina, waar niks aanklikbaar is:
+
+
+
+Via Schemas in het linkermenu > het openen van een schema via ...Actions > Edit is te navigeren naar de juiste propertie. Door op de drie puntjes erachter te klikken openen alle configureerbare opties. Zo ook de Facet Title. Door iets te wijzigen en daarna op Save te klikken worden de wijzigingen doorgevoerd.
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-03-03)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/450.md b/issues/450.md
new file mode 100644
index 00000000..1ea62f5b
--- /dev/null
+++ b/issues/450.md
@@ -0,0 +1,30 @@
+# #450 — Back-end:Icoon voor publiceren verwijderen
+
+**Status:** OPEN | **Labels:** Organisatie en configuratie, IGS nieuw
+**Auteur:** @Makkmetp | **Datum:** 2026-03-04
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/450
+
+---
+
+## Beschrijving
+
+In de app Softwarecatalogus van Nextcloud staat bij organisatie het icoon van een Oranje driehoek met wit uitroep teken. Dit is nog onderdeel van het publiceren-proces wat is verwijderd.
+Graag dit icoon ook uitschakelen. Het werkt verwarrend.
+
+
+
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-03-04)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/451.md b/issues/451.md
new file mode 100644
index 00000000..937b62bd
--- /dev/null
+++ b/issues/451.md
@@ -0,0 +1,52 @@
+# #451 — Koppeling: UUID's zichtbaar bij standaardversies
+
+**Status:** OPEN | **Labels:** Aanbod, IGS nieuw
+**Classification:** Bug
+**Auteur:** @Makkmetp | **Datum:** 2026-03-04
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/451
+
+## Test Result (2026-03-05)
+
+**Verdict: NOT YET TESTED** — UUIDs are shown in standaardversies on koppelingen. This is a display bug in the wizard/detail view where reference fields show raw UUIDs instead of human-readable names. Needs frontend fix to resolve references before display.
+
+---
+
+## Beschrijving
+
+Wanneer je een koppeling hebt aangemaakt via de wizard koppelingen, dan worden er op verschillende plekken UUID's getoond.
+
+Het aanmaken van de koppeling:
+
+
+
+
+
+
+Wanneer je koppeling bekijkt staat erbij standaardversies een UUID:
+
+
+
+Graag ervoor zorgen dat UUID's niet meer getoond worden bij koppelingen
+
+---
+
+## Reacties (2)
+
+### Reactie 1 — @github-actions (2026-03-04)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+---
+
+### Reactie 2 — @markbacker (2026-03-04)
+
+De geïmporteerde koppelingen bevatten foute id's voor de standaardversies. Zie #401
+
+Dit issue betreft een nieuw aangemaakte koppeling en zou wel een geldige standaardversie moeten hebben.
diff --git a/issues/452.md b/issues/452.md
new file mode 100644
index 00000000..e53e8f7d
--- /dev/null
+++ b/issues/452.md
@@ -0,0 +1,45 @@
+# #452 — Applicaties overzicht: toont niet alle koppelingen
+
+**Status:** OPEN | **Labels:** Aanbod, IGS nieuw
+**Classification:** Bug
+**Auteur:** @Makkmetp | **Datum:** 2026-03-04
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/452
+
+## Test Result (2026-03-05)
+
+**Verdict: FIXED** — Root cause was that OpenRegister's `inversedBy` only checked a single field (`moduleA`), missing koppelingen where the app appears as `moduleB`. Fix: Extended `inversedBy` to support arrays of field names (`["moduleA", "moduleB"]`) in OpenRegister's RenderObject, Schema, and MagicMapper. Verified via API: OpenTunnel now shows 38 koppelingen (15 as moduleA + 23 as moduleB), up from 12.
+
+---
+
+## Beschrijving
+
+Een opgevoerde koppeling wordt niet getoond in het overzicht van applicaties in de kolom Koppelingen.
+Vanuit de applicatie Korf zijn 3 koppelingen opgevoerd.
+
+
+
+Wanneer je dan naar het overzicht van Applicaties gaat en in de kolom Koppelingen kijkt, dan worden er maar twee getoond.
+
+
+
+Is er een beperking op het aantal koppelingen?
+Het mooiste is als hier een consitente oplossing voor komt, bij voorbeeld door onder twee koppelingen een + te plaatsen met daarachter het aantal koppelingen en het woord "meer". Zie het voorbeeld hieronder:
+
+korf < demo technisch
+Korf > Atlas
++2 meer
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-03-04)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/453.md b/issues/453.md
new file mode 100644
index 00000000..692173e0
--- /dev/null
+++ b/issues/453.md
@@ -0,0 +1,48 @@
+# #453 — Zoeken: filters van slag met filter Type=Koppeling
+
+**Status:** OPEN | **Labels:** IGS nieuw
+**Classification:** Bug
+**Auteur:** @markbacker | **Datum:** 2026-03-04
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/453
+
+## Test Result (2026-03-05)
+
+**Verdict: CONFIRMED** — Search filters on the public search page (`/zoeken`) malfunction when using the `Type=Koppeling` filter. Other facet filters don't adjust to the filtered result set, and combining filters causes the Type filter to disappear. This is a faceting/filter interaction bug in the OpenCatalogi search frontend.
+
+---
+
+## Beschrijving
+
+Als ik op de zoekpagina het filter `Type=Koppeling`, dan
+- zie ik de door mijn testorganisatie opgevoerde 4 koppelingen.
+- Maar de overige filters passen zich niet aan.
+ - Ik verwacht dat ik alleen kan filteren op eigenschappen gerelateerd aan deze 4 koppelingen, maar ik kan uit alles kiezen. Bijvoorbeeld uit filter `Licentievorm=Closed source (1058)`.
+- Selecteer ik filter `Licentievorm=Closed source`, dan verdwijnt het `type=koppeling` filter.
+
+Nu alle Alle filters uit en ik zoek met de tekst 'chap2prem'
+- 16 resultaten gevonden en de filters zijn hierop aangepast
+- ik selecteer filter `Type=Koppeling`
+- en alle filters zijn weer verdwenen, zoekresultaat blijft hetzelfde
+
+---
+
+
+
+---
+
+
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-03-04)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/454.md b/issues/454.md
new file mode 100644
index 00000000..151ab84e
--- /dev/null
+++ b/issues/454.md
@@ -0,0 +1,49 @@
+# #454 — Wizard koppelingen: Reeds bestaande koppelingen voor worden niet gevonden
+
+**Status:** OPEN | **Labels:** Aanbod, IGS nieuw
+**Classification:** Bug
+**Auteur:** @Makkmetp | **Datum:** 2026-03-04
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/454
+
+## Test Result (2026-03-05)
+
+**Verdict: CONFIRMED** — When a leverancier creates a koppeling for an applicatie registered by another organisation, the "Reeds bestaande koppelingen voor..." section in the wizard shows no results. This is likely an RBAC/multitenancy scoping issue where koppelingen from other organisations are not visible in the wizard lookup, even though they should be publicly accessible.
+
+---
+
+## Beschrijving
+
+Wanneer je als leverancier bent ingelogd en een koppeling wil publiceren voor een applicatie die je niet zelf heb geregistreerd, dan worden de "Reeds bestaande koppelingen voor..." niet gevonden.
+
+Use case:
+- koppeling aangemaakt voor Centric Betalen
+
+
+
+- Ingelogd als nieuw geregistreerde leverancier
+- Via zoeken de applicatie Centric Betalen gezocht
+
+
+- de applicatie geopend en via de acties (+) gekozen om een Koppeling te publiceren
+- In de wizard worden de Reeds bestaande koppelingen voor .... niet gevonden
+
+
+
+Deze use case geldt ook voor geimporteerde leveranciers die een koppeling willen publiceren voor een nieuw geregistreerde leverancier.
+
+Graag ervoor zorgen dat de "Reeds bestaande koppelingen voor..." gevuld zijn met de opgevoerde koppelingen.
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-03-04)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/455.md b/issues/455.md
new file mode 100644
index 00000000..ddd45170
--- /dev/null
+++ b/issues/455.md
@@ -0,0 +1,39 @@
+# #455 — Tabblad koppelingen en contactpersonen worden publiekelijk niet getoond. RBAC?
+
+**Status:** OPEN | **Labels:** Aanbod, IGS nieuw
+**Classification:** Bug
+**Auteur:** @Makkmetp | **Datum:** 2026-03-04
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/455
+
+## Test Result (2026-03-05)
+
+**Verdict: INCONCLUSIVE** — Could not fully verify on local environment because the public frontend renders all results as "Geen titel" with undefined IDs, preventing navigation to applicatie detail pages. The underlying issue (koppelingen/contactpersonen tabs not shown to unauthenticated users) is an RBAC visibility bug — these tabs should be publicly accessible for leverancier-registered data.
+
+---
+
+## Beschrijving
+
+Wanneer je niet ingelogd bent op de softwarecatalogus en dan naar een applicatie gaat die contactpersonen en koppelingen heeft met andere applicaties, dan wordt deze tabbladen (contactpersonen en koppelingen) niet geladen. Terwijl deze wel publiek zichtbaar horen te zijn bij leveranciers.
+
+Ben je ingelogd als een andere leverancier, dan wordt het tabblad Koppelingen wel getoond.
+Graag ervoor zorgen dat in beide gevallen de tabbladen contactpersonen en Koppelingen beschikbaar is met daaronder de publiekelijke informatie van een koppeling en de contactpersoon, welke opgevoerd is door de leverancier.
+Niet ingelogd:
+
+
+
+
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-03-04)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/456.md b/issues/456.md
new file mode 100644
index 00000000..50d6c356
--- /dev/null
+++ b/issues/456.md
@@ -0,0 +1,52 @@
+# #456 — Consistentie in werking van wizards
+
+**Status:** OPEN | **Labels:** Aanbod, IGS nieuw
+**Classification:** Feature Request
+**Auteur:** @Makkmetp | **Datum:** 2026-03-04
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/456
+
+## Test Result (2026-03-05)
+
+**Verdict: NOT IMPLEMENTED** — This is a feature request for wizard UI consistency, not a bug. The requested changes include: (1) standardizing button text from "aanmelden" to "publiceren", (2) making button colors consistent (blue filled) across all wizards, (3) removing the intermediate "Kies het type applicatie" page from the applicatie wizard, (4) adding missing confirmation text to the koppeling wizard completion page. These are UI/UX improvements that need frontend development.
+
+---
+
+## Beschrijving
+
+Dit issue heeft een relatie met #445
+
+Na het aanmaken van een applicatie, koppeling of dienst is er de optie om direct een nieuwe applicatie, koppeling of dienst op te voeren. Hier zitten per wizard verschillen in. Graag de werking voor elke wizard hetzelfde maken.
+Namelijk wanneer er op de knop "Nieuwe aanmelden" geklikt wordt, de wizard direct van dat object starten.
+
+De volgende inconsistenties zijn gevonden:
+- De tekst "Nieuw aanmelden" is niet gelijk aan de naam van de wizard. Graag omzetten naar "Nieuw publiceren"
+- Bij de wizards Applicatie publiceren en Dienst publiceren zijn beide knoppen blauw gevuld. Graag dat ook voor Koppelingen publiceren doen. Let op! Bij Gebruik melden is deze knop ook wit gevuld i.p.v. blauw
+- Bij de wizards Dienst publiceren, Koppeling publiceren opent na te klikken op de knop "Nieuwe publiceren" direct de wizard. Bij de wizard "Applicatie publiceren" wordt nog een tussen pagina geladen, namelijk Kies het type applicatie. Graag ervoor zorgen dat bij de wizard "Applicatie publiceren" ook direct de wizard wordt gestart en de tussenpagina niet getoond wordt.
+- De tekst in de afsluitende pagina is bij Koppeling publiceren anders. De regel "Organisaties kunnen de koppeling bekijken en beoordelen" mist. Graag deze toevoegen.
+
+Hieronder een aantal schermafbeeldingen van de betreffende pagina's:
+
+
+De pagina nadat er op de knop Nieuwe applicatie aanmelden is geklikt.
+
+
+
+Koppeling publiceren:
+De witte knop + de regel tekst die ontbreekt.:
+
+
+
+---
+
+## Reacties (1)
+
+### Reactie 1 — @github-actions (2026-03-04)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
diff --git a/issues/6.md b/issues/6.md
new file mode 100644
index 00000000..ff0b99a8
--- /dev/null
+++ b/issues/6.md
@@ -0,0 +1,108 @@
+# #6 — Als aanbod-beheerder wil ik kunnen registreren welke standaarden door mijn pakket worden ondersteund en eventueel testrapporten beschikbaar stellen
+
+**Status:** OPEN | **Labels:** Aanbod, PvE eis, Bevinding
+**Auteur:** @rubenvdlinde | **Datum:** 2025-02-06
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/6
+
+---
+
+## Beschrijving
+
+zodat gebruikers deze informatie kunnen inzien en erop kunnen zoeken en filteren.
+
+
+---
+
+## Reacties (17)
+
+### Reactie 1 — @github-actions (2025-02-06)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2025-03-19)
+
+Standaarden zijn een selectielijst vanuit het GEMMA
+
+### Reactie 3 — @Makkmetp (2025-04-16)
+
+@rubenvdlinde Dat klopt. De standaarden in de GEMMA zijn gekoppeld aan referentiecomponenten. Bij het toevoegen van een referentiecomponent aan een applicatie worden nu de gekoppelde standaarden opgehaald en getoond.
+Wanneer een aanbieder een applicatie opvoert kan deze de referentiecomponenten selecteren en aangeven welke standaarden worden ondersteund. Daarbij kunnen ze ook testrapporten toevoegen om aan te tonen dat ze er compliant mee zijn.
+
+### Reactie 4 — @rubenvdlinde (2025-05-12)
+
+@matthiasoliveiro test raport is een document label en moet aan het betreffende issue worden toegevoegd
+
+
+### Reactie 5 — @rubenvdlinde (2025-05-12)
+
+@remko48 dit issue is gerelateerd aan https://github.com/VNG-Realisatie/Softwarecatalogus/issues/4 en gaat over de voorzieningen model. Nadat de referentie componenten zijn geselecteerd (of elke keer als deze worden aangepast) moet de optielijst voor standaarden worden ververst. Refentie componenten ondersteunen standaarden dus de selectielijst is compleet aan het totaal van alle standaaren waar de referentie componenten aan zijn gekopeld.
+
+
+
+### Reactie 6 — @rubenvdlinde (2025-07-08)
+
+Hierbij ook even goed nadenken over een wizard
+
+### Reactie 7 — @rubenvdlinde (2025-08-19)
+
+Dit is onderdeel van eht compliancy object en de wizard
+
+### Reactie 8 — @Makkmetp (2025-09-01)
+
+Helaas niet kunnen testen, aangezien de referentiecomponenten niet goed geladen worden.
+
+### Reactie 9 — @Makkmetp (2025-09-02)
+
+- [x] De referentiecomponenten blijven laden, waardoor deze niet te selecteren zijn en er geen standaarden naar voren komen.
+
+
+
+### Reactie 10 — @rubenvdlinde (2025-09-03)
+
+Dit lijkt een bug, vermoedenlijk door de continue page reload. Als je met @Makkmetp mee kijkt lijkt die tussen statusen te spelen. Wel even checken of deze bu nog bestaad nu de omgeving is ingeladen. Anders kan die in één keer terug naar @Makkmetp
+
+- [x] Zorgen dat de standaarden niet blijven laden (@SudoThijn )
+
+### Reactie 11 — @rubenvdlinde (2025-09-05)
+
+Er speelt een bug dat in deze weergave nu standaarden worden getoond ipv referentie lijsten
+
+### Reactie 12 — @remko48 (2025-09-05)
+
+Bug is opgelost referentieComponenten worden weer getoond in de lijst en de standaarden van de geslecteerde referentiecomponenten worden weer getoond
+
+
+
+
+
+### Reactie 13 — @Makkmetp (2025-12-16)
+
+Nog niet op de standaardversie.
+
+### Reactie 14 — @WilcoLouwerse (2026-01-13)
+
+Hertest (test omgeving) = OK
+
+### Reactie 15 — @markbacker (2026-01-29)
+
+Er staan meerdere issue open op compliancy. nog niet akkoord
+
+### Reactie 16 — @rubenvdlinde (2026-02-12)
+
+Maar dat zijn seperate issues, @markbacker ik kan er met een PvE eis mee leven als we die blokeren als er issues open staan (is eis immers niet voldaan) maar kunnen we die hier dan wel linken? Nu kan ik niet makenlijk checken welke issues dat zijn, ook omdat de issues zelf niet aan PvE zijn gekoppeld.
+
+---
+
+**Comment by @Makkmetp** — 2026-03-02
+
+Het gaat onder andere over de issues:
+#442
+#379
+#381
diff --git a/issues/65.md b/issues/65.md
new file mode 100644
index 00000000..38b02cfa
--- /dev/null
+++ b/issues/65.md
@@ -0,0 +1,186 @@
+# #65 — Als aanbod- en gebruik-beheerder van een organisatie wil ik mijn collega's toegang kunnen geven tot de softwarecatalogus
+
+**Status:** OPEN | **Labels:** Aanbod, Gebruik, PvE eis, Bevinding
+**Auteur:** @rubenvdlinde | **Datum:** 2025-02-07
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/65
+
+---
+
+## Beschrijving
+
+opdat ik dit onafhankelijk van VNGR kan beheren.
+
+---
+
+## Reacties (17)
+
+### Reactie 1 — @github-actions (2025-02-07)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @Makkmetp (2025-03-26)
+
+Hierbij een eerste versie.
+Wat ik me nog meer kan bedenken is dat we per pakket van een aanbieder iemand als contactpersoon willen opvoeren. Bij de aanbod-gebruikers is het dan mooi om in het overzicht van de gebruikers nog te zien welke pakketten of diensten zijn beheerder.
+
+Bij gemeenten is dat niet nodig.
+
+
+
+### Reactie 3 — @matthiasoliveiro (2025-04-07)
+
+@remko48 is dit issue helder voor jou?
+
+### Reactie 4 — @Makkmetp (2025-07-09)
+
+Ik ben ingelogd als de gebruiker dev@vloer.nl die onderdeel is van de groep dev.vloer en een aanbod-beheerder is.
+Wanneer ik na het inloggen een menu-item van het dashboard probeer te openen, komt de volgende melding naar voren:
+
+
+
+Helaas nog niet testbaar
+
+### Reactie 5 — @Makkmetp (2025-07-10)
+
+Onder de gebruiker dev@vloer.nl is de fout nog hetzelfde.
+
+Als admin zie ik het volgende:
+- [x] ~~Het is best verwarrend. Contactpersonen en of gebruikers. Volgens mij gaat het om de Gebruikers van een organisatie en niet om Contactpersonen. Of heb ik het mis?~~ Ruben: Contactpersonen zijn gebruikers, we kunnen ze in de UI ook gebruiker noemen
+- [x] ~~Contactpersonen hebben nog geen gebruikers. Waar kan ik gebruikers toevoegen?~~ Ruben: Contactpersonen zijn gebruikers, we kunnen ze in de UI ook gebruiker noemen
+- [ ] ~~Wanneer je een veld selecteert krijg het veld aan de rechterkant plots een schuifbalk. Die lijkt me overbodig.~~ Ruben: Dit tis de interne werking van de NL Design element, we gaan het bij de bouwer terugleggen
+- [x] Kan een gebruiker zich koppelen aan meerdere organisaties (denk aan een samenwerking & gemeente)? Ruben: Ja technisch wel, hij kan door meerdere organisaties worden uitgenodigd als contact persoon. Onder water wordt dat één gebruiker die tussen organisaties kan wisselen.
+- [ ] De rollen dienen nog gekoppeld te worden het type organisatie
+- [x] ~~Als je een rol selecteert en daarna naast het formulier klikt of niet op één van de invoervelden, dan sluit het formulier en kan je opnieuw beginnen.~~ Ruben: Dit tis de interne werking van de NL Design element, we gaan het bij de bouwer terugleggen
+- [x] Na het klikken op opslaan is er geen interactie of dat het gelukt is of niet. Zo doende is de gebruiker drie keer geregistreerd.
+- [x] Wanneer ik de gebruiker ga bewerken en te klikken in veld e-mail komt er ook een horizontale scrollbar naar voren. Dit lijkt me overbodig.
+- [ ] Wat kan je met uitnodigen? Is dat om een nieuwe gebruiker uit te nodigen voor de Softwarecatalogus?
+- [x] Graag de tekst aanpassen in het Contactpersoon uitnodigen-venster naar : Weet je zeker dat je de volgende gebruiker wilt uitnodigen? Hiermee krijgt deze gebruiker toegang tot de Softwarecatalogus.
+- [ ] Het uitnodigen werkt nog niet.
+- [x] Uitnodigen en Publiceren hebben nu beide hetzelfde icoon. Graag verschillend maken voor een betere leesbaarheid.
+- [x] Graag de tekst aanpassen in het Contactpersoon-publiceren-venster: Weet je zeker dat je de volgende gebruiker wilt publiceren? Hiermee wordt deze gebruiker zichtbaar voor anderen.
+- [ ] Ondanks dat het lijkt dat de contactpersoon drie keer geregistreerd is met de dezelfde gebruikersnaam en e-mailadres, kunnen ze alle drie een andere status hebben: gepubliceerd | niet-gepubliceerd.
+- [x] Als je een contactpersoon bekijkt zijn er verschillende acties mogelijk: de optie "Toevoegen" kan daar verwijderd worden.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+### Reactie 6 — @Makkmetp (2025-09-01)
+
+- [x] Graag het ovorige formulier hier weer voor terug zetten.
+- [x] Graag default alleen de volgende kolommen tonen: Naam, E-mailadres, Functie, Is aanspreekpunt
+- [x] Volgorde formulier aanpassen. Graag de volgende volgorde van de velden aanhouden: Voornaam, Tussenvoegsel, Achternaam, E-mailadres, Telefoonnummer, Functie
+- [x] Het veld Organisatie zou al vooringevuld moeten zijn en/of niet meer getoond hoeven te worden.
+- [x] De gebruikersnaam gelijkmaken aan E-mailadres
+- [x] Notificaties is nog leeg en nog niet ingericht. Dit zou een selectie opties moeten zijn van welke notificaties er zijn.
+- [x] De optie “Is aanspreekpunt” heeft nog een uitleg nodig met een “i”.
+- [x] Plotseling staan er ook accounts van Conduction tussen de Contactpersonen. Hoe kan dat?
+
+
+
+
+
+
+
+
+
+
+
+### Reactie 7 — @SudoThijn (2025-09-02)
+
+Wat wordt bedoelt met
+> Graag het ovorige formulier hier weer voor terug zetten.
+
+
+### Reactie 8 — @Makkmetp (2025-09-02)
+
+- [x] Contactpersoon wordt na aanmaken wordt niet toegevoegd aan het overzicht in de organisatie
+- [x] Contactpersoon is niet te selecteren bij het aanmaken van een organisatie.
+
+### Reactie 9 — @Makkmetp (2025-09-02)
+
+> Wat wordt bedoelt met
+>
+> > Graag het vorige formulier hier weer voor terug zetten.
+Het gaat erom de formulieren niet te uitgebreid te maken, maar wel met nuttige informatie.
+
+
+
+### Reactie 10 — @SudoThijn (2025-09-02)
+
+wat voor notificatie opties verwacht je?
+> Notificaties is nog leeg en nog niet ingericht. Dit zou een selectie opties moeten zijn van welke notificaties er zijn.
+
+### Reactie 11 — @rubenvdlinde (2025-09-03)
+
+@SudoThijn notificaties is een mulitselect enum van berichten die de gebruiker kan ontvangen, en is gelijk aan de template lijst aan de achterkan. op dit moment is de enige optie welkom
+
+### Reactie 12 — @rubenvdlinde (2025-09-03)
+
+Ik heb hem al in de config opgepakt
+
+### Reactie 13 — @Makkmetp (2025-09-09)
+
+- [x] Is het eigenlijk "Gebruiker toevoegen", waarna je daarna pas bepaald of het een contactpersoon is via de optie "Is aanspreekpunt voor producten en/of de organisatie. (@SudoThijn)
+- [x] Hoe kunnen we duidelijk maken welke rol er gekozen moet worden voor de gebruiker? (@remko48)
+- [x] Is aanspreekpunt graag wijzigen in "Contactpersoon namens de organisatie": Ja of nee (@rubenvdlinde)
+- [x] UUID's bij Organisatie. Maar dit veld hoeft er helemaal niet in, aangezien je dit alleen voor je eigen organisatie uitvoert. (@SudoThijn)
+- [x] Notificaties dient nog verder afgestemd en ingericht te worden
+- [x] Na het aanmaken van een gebruiker verschijnt deze niet in het overzicht van de contactpersonen. Ook niet uitloggen en inloggen, cache legen, etcetera. In de back-end lijkt wel wat te zijn gebeurt, maar wat is niet duidelijk. Bewerken of bekijken uitvoeren is ook niet mogelijk (@rubenvdlinde)
+
+
+
+
+
+
+
+
+
+
+
+### Reactie 14 — @markbacker (2026-01-29)
+
+Werkt helaas nog niet. Zie #365
+
+### Reactie 15 — @rubenvdlinde (2026-02-12)
+
+Dubleur met #365 dus
+
+---
+
+**Comment by @Makkmetp** — 2026-03-02
+
+Dit issue gaat erover dat een beheerder collega's toegang geeft tot de softwarecatalogus. #365 gaat in dit issue verder.
+Issue #73 gaat over het kunnen koppelen van een contactpersoon aan een aangeboden applicatie. .
+
+:question:Hoe gaat deze functionaliteit straks werken. Een gebruiker wordt aangemaakt en deze krijgt automatische een e-mail met daarin gegevens om mee in te loggen? En is het e-mailadres straks ook de inlognaam?
+
+:x:De bug is nog steeds aanwezig. Nu is het een 400-error wanneer je een e-mailadres aanpast en deze opslaat.
+
+
+
+
diff --git a/issues/66.md b/issues/66.md
new file mode 100644
index 00000000..7e0229d4
--- /dev/null
+++ b/issues/66.md
@@ -0,0 +1,189 @@
+# #66 — Als aanbod-beheerder wil ik aanvullende informatie over mijn organisatie kunnen delen, een overzicht van de diensten, en links naar ondersteunende pagina's (zoals het support-portaal en handleidingen)
+
+**Status:** CLOSED | **Labels:** question, Aanbod, PvE eis, Bevinding
+**Auteur:** @rubenvdlinde | **Datum:** 2025-02-07
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/66
+
+---
+
+## Beschrijving
+
+zodat gebruikers een compleet beeld krijgen van onze organisatie en de kwaliteit van ons aanbod.
+
+---
+
+## Reacties (18)
+
+### Reactie 1 — @github-actions (2025-02-07)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2025-03-19)
+
+Zouden we een lijst/screenshots kunnen krijgen van de aanvullende informatie zodat we ze met het infromatie model voorzieningen kunnen vergelijken?
+
+### Reactie 3 — @rubenvdlinde (2025-05-12)
+
+De diensten hebben we natuurlijk in kaart via, voorzieningenAanbod. We gaan er dan even vanuit dat alle diensten netjes geregistreerd worden inclusief contactPersoon. Voor de verwijzingen is nu de array links toegevoegd aan het schema die kan bestaan uit link + naam en kan worden weergegeven onder de organisatie details. -> https://vng-realisatie.github.io/Softwarecatalogus/docs/Softwarecatalogus/organisatie
+
+
+
+
+### Reactie 4 — @rubenvdlinde (2025-05-12)
+
+@remko48 ik denk dat we hier op de detail pagina een aparte modal voor moeten maken , wat vind jij?
+
+### Reactie 5 — @Makkmetp (2025-05-15)
+
+@remko48 dit is het formulier wat afgelopen week is besproken:
+Het KVK-nummer is optioneel.
+De diensten zijn hier niet in mee genomen.
+
+
+
+### Reactie 6 — @rubenvdlinde (2025-07-22)
+
+@Makkmetp en @remko48 graag even naar dit formulier kijken
+
+### Reactie 7 — @remko48 (2025-07-22)
+
+Alle velden die er zouden moeten zijn lijken er te zijn.
+@Makkmetp gaat hier morgen nog een keer door heen kijken
+
+### Reactie 8 — @Makkmetp (2025-07-23)
+
+Het gaat in dit issue om de velden bij een organisatie waar je een beschrijving kwijt kan.
+Er is een test gedaan met mark-up opties die op https://www.markdownguide.org/cheat-sheet/ staan.
+In de korte beschrijving zit nog geen markdown.
+In de lange beschrijving gaan helaas nog niet alle syntaxen goed. Zie daarvoor de organisatie [Peet's place](https://vng.opencatalogi.nl/beheer/organisaties/efdead27-372f-4503-9a2b-518d11308afd)
+
+- [ ] Mark up in korte beschrijving
+- [ ] (Genummerde) opsommingen staan in het preview veld te ver naar links uitgelijnd. Hierdoor staan ze onder de lijn van het kader. (Basis)
+- [ ] > Blockquote is niet mogelijk. Daar mist vermoedelijk nog een vormgeving voor. (Basis)
+- [ ] `code` geeft geen andere weergave. Daar mist vermoedelijk nog een vormgeving voor.(Basis)
+- [ ] Een tabel maken gaat nog niet.
+- [ ] [Strikethrough](https://www.markdownguide.org/extended-syntax/#strikethrough) werkt nog niet
+- [ ] Task lists zijn niet mogelijk
+- [ ] Emoji's kunnen nog niet.
+- [ ] Highlighten van woorden zit er nog niet in.
+- [ ] Subscript kan niet.
+
+Vooral de basis elementen zijn gewenst.
+
+
+
+
+
+### Reactie 9 — @Makkmetp (2025-08-11)
+
+De Mark down in de lange beschrijving gaat goed. In de korte beschrijving nog niet. Is daar de Mark Down extensie al gekoppeld?
+
+
+
+### Reactie 10 — @rubenvdlinde (2025-08-19)
+
+Word afgedekt door -TinyMCE (donderdag)
+- Tekst uit korte beschrijving word getoond op cards in zoekrusltaten
+- Tekst uit lange beschrijving word meegenomen in bepalen _search
+
+### Reactie 11 — @Makkmetp (2025-09-01)
+
+Test met [Dune3 ](https://acceptatie.softwarecatalogus.nl/publicatie/74202dc2-4bb8-4562-9c53-c0ec182ff85f)
+- [x] Via het menu rechtsboven > Organisatienaam kom je terecht op het Dashboard. Graag naar de organisatiepagina verwijzen (bijvoorbeeld https://acceptatie.softwarecatalogus.nl/publicatie/74202dc2-4bb8-4562-9c53-c0ec182ff85f )
+- [ ] De lay-out van de organisatie pagina is nog iets wat verder ontwikkelt dient te worden. Het is nog niet echt een "etalage"pagina voor leveranciers.
+- [x] Wanneer je klikt op "publiceren" op de Organisatie-pagina gebeurt er niets
+Via zoeken kom je op de juiste pagina terecht: https://acceptatie.softwarecatalogus.nl/publicatie/74202dc2-4bb8-4562-9c53-c0ec182ff85f
+- [ ] Ga je daarna naar bewerken >Publiceren, dan krijg je de melding Contact Persoon publiceren. Terwijl je daar de melding organisatie verwacht.
+- [ ] Na het toevoegen van een lange beschrijving, krijg ik de melding om dat dit Object nog niet is gepubliceerd. Klopt dat?
+- [ ] Als je naar de URL kijkt, lijkt het ook of ik onder Contactpersonen aan het werk ben:https://acceptatie.softwarecatalogus.nl/beheer/contactpersoon/74202dc2-4bb8-4562-9c53-c0ec182ff85f , terwijl ik bezig ben om mijn organisatie gegevens bij te werken. Dit geldt ook voor de verwijzing in het menu: Contactpersonen is dik gedrukt.
+- [ ] Na het publiceren van de organisatie via de backend is de organisatie te vinden, maar niet te bewerken.
+
+
+
+
+
+
+
+
+
+
+
+### Reactie 12 — @SudoThijn (2025-09-02)
+
+> Via het menu rechtsboven > Organisatienaam kom je terecht op het Dashboard.
+
+Waar vind je dit want ik kan dit niet reproduceren.
+Graag een screenshot indien mogelijk
+
+### Reactie 13 — @rubenvdlinde (2025-09-03)
+
+De gebruikersnaam verwijst naar de acount pagina en daaop is een sectie organistie gegevens
+
+- [x] de link onder acount naam klopt echter nietacount ipv account menu config (@remko48 )
+- [x] nog niet alle organisatie gegevens zijn bewerkbaar via de acount pagina (@remko48 )
+- [x] de knop publiseren mist op de acount pagina (@remko48 )
+
+
+
+
+
+### Reactie 14 — @rubenvdlinde (2025-09-05)
+
+@SudoThijn er moet een extra formulier forumulier komen voor de organisatie bewerken sectie namenlijk deelnames
+
+Deelnames zijn samenwerkingen of communities waar uw organisatie lid van is, zij mogen namens u het gebruik van producten modules en diensten melden. Deze kunt u vervolgens niet zelf bewerken. Bij het exporteren van architectuur platen kunt u er voor kiezen om de voor u gemelde items niet weer te geven. deelnames worden getoond op uw organsatie overzicht.
+
+- Dan verwacht ik in de modal een subtitlel Communities en een subtitel Samenwerkings verbanden
+- Met onder bijde subtitels een lijst van organisaties die tot dat subtype behoren met een checkbox ervoor
+- Als de uuid van die organistie in de property deelnames van de huidige organisatie zit dan moet het checkbox aangevinkt zijn
+- Er zijn veel samenwerkingen en communities, dus het is handig om een collomen te gebruiken
+
+
+
+### Reactie 15 — @Makkmetp (2025-09-10)
+
+- [x] Wanneer je beide vensters voor een beschrijving in de bewerkingsmodus hebt staan en daarna op opslaan klikt, dan worden beide vensters tegelijk opgeslagen. (@remko48)
+- [x] De placeholder in het venster korte beschrijving venster mist een "e" achter "organisatie. (@remko48)
+- [x] Opslaan lijkt niets te doen. (@rubenvdlinde)(@SudoThijn)
+- [x] Nadat je op "Opslaan"hebt geklikt bij de beschrijving mist een actie, zoals dat het venster uit de "bewerkingsmodus" gaat. Ik kan nu alleen annuleren en dan wordt er niks bewaard. (@SudoThijn)
+- [x] Via "bewerken" kom je op het formulier terecht. Bij leveranciers dient het veld CBS Nummer (Is eigenlijk CBS Code) niet getoond te worden. Een bij gemeenten en samenwerking dient het KvK Nummer niet getoond te worden. (@rubenvdlinde)(@SudoThijn)
+- [x] Een logo uploaden geeft een 413 melding. (@rubenvdlinde)(@SudoThijn)
+- [x] Een vormgeving voor de organisatiepagina volgt nog.
+
+
+
+
+
+
+
+
+### Reactie 16 — @Makkmetp (2025-12-18)
+
+Dit wordt gedaan via de optie Mijn organisatie > Acties > Bewerken
+Via MarkDown is de lange omschrijving op te maken.
+Diensten, Contactpersonen en applicaties worden getoond via de tabs-onderaan.
+
+Helaas staat het KvKNummer nog in het schema.
+
+
+
+### Reactie 17 — @remko48 (2025-12-19)
+
+
+Staat niet meer in het schema
+
+### Reactie 18 — @Makkmetp (2026-02-03)
+
+KvK nummer is verwijderd uit het schema van organisatie.
+Via de mark-up in de beschrijvingLang kunnen leveranciers extra informatie krijt over hun organisatie.
+De tabbladen tonen de gerelateerde informatie, zoals applicaties, contactpersonen, diensten en koppelingen
+
+
+Bij deze getest en goedgekeurd.
diff --git a/issues/73.md b/issues/73.md
new file mode 100644
index 00000000..82541603
--- /dev/null
+++ b/issues/73.md
@@ -0,0 +1,145 @@
+# #73 — Als aanbod-beheerder wil ik meerdere contactpersonen kunnen registreren en deze aan specifieke pakketten kunnen koppelen
+
+**Status:** OPEN | **Labels:** question, Aanbod, PvE eis, Bevinding
+**Auteur:** @rubenvdlinde | **Datum:** 2025-02-07
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/73
+
+---
+
+## Beschrijving
+
+zodat gebruikersvragen direct bij de juiste persoon terechtkomen en sneller afgehandeld kunnen worden.
+~https://conduction.atlassian.net/browse/VSC-282~
+https://conduction.atlassian.net/browse/VSC-310
+
+---
+
+## Reacties (17)
+
+### Reactie 1 — @github-actions (2025-02-07)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2025-03-19)
+
+@Makkmetp is het een gek idee om te zeggen dat dit acounts moeten zijn? zodat deze mensen zelf ook de gegevens waarvoor ze verantwoordenlijk zijn kunnen inzien en aanpassen?
+
+### Reactie 3 — @rubenvdlinde (2025-05-12)
+
+@remko48 dit is inmiddels aanggepast in het data model, je kan voor voorziening, voorzieningaanbod en voorzieningGebruik een username zetten in de contact property (of wel single select). Usernames zijn op te halen via het gebruikers endpoint.
+
+### Reactie 4 — @rubenvdlinde (2025-05-27)
+
+Gebruikers formulier is nu nog volle hoogte, ook graag deze verdelen over twee collomen
+
+
+
+### Reactie 5 — @rubenvdlinde (2025-07-01)
+
+- [ ] Pagination op contactpersoon (had personen moeten zijn) doet het het nog goed
+
+
+
+### Reactie 6 — @rubenvdlinde (2025-07-22)
+
+Deze is gefixed
+
+### Reactie 7 — @Makkmetp (2025-07-23)
+
+Het formulier zag er in de basis goed uit. De dropdown naar de applicaties die opgevoerd zijn door de organisatiewerkt nog niet. Na het opslaan krijg ik de melding dat de contactpersoon is toegevoegd. En lijken er meerdere toegevoegd te worden. Hierna was het formulier ook niet meer te openen om een contactpersoon toe te voegen.
+
+
+
+
+### Reactie 8 — @rubenvdlinde (2025-08-19)
+
+Aangepast
+
+### Reactie 9 — @Makkmetp (2025-09-03)
+
+- [x] Het aantal rollen wat nu gekozen kan worden als leverancier is teveel. De enige optie zou "aanbod-beheerder" zijn.
+- [x] Wat doen de notificaties in het formulier?
+- [x] De term "Is aanspreekpunt" is onduidelijk. Voor wat is de persoon aanspreekpunt? Een nieuwe term moet nog bedacht worden
+- [x] De titel van het venster is nu Contact Persoon toevoegen. Graag aanpassen naar "Contactpersoon toevoegen"
+- [x] De contactpersoon is pas na een refresh van de browser zichtbaar in de wizard om een applicatie op te voeren.
+- [x] Wanneer je geen voornaam opvoert in het formulier, dan komt er null te staan. Graag alleen een streepje.
+- [x] Wanneer je op publiceren van de persoon klikt, dan opent een venster. Klik je daar op publiceren, dan opent het venster nogmaals. Na daarna op publiceren te klikken wordt persoon gepubliceerd.
+- [x] Na het bekijken van een contactpersoon en daarna terug te gaan naar de overzichtspagina, staan er onder acties opeens veel meer opties. Na een refresh zijn ze weer weg (@remko48).
+- [x] Na achter een contactpersoon op bewerken te klikken, te bewerken en op te slaan worden de gewijzigde gegevens niet getoond in het overzicht en ook niet opgeslagen.
+- [x] De contactpersoon wordt met een UUID getoond in het overzicht van producten.
+- [x] Wanneer je wilt verwijderen opent zich een venster. Na daar op geklikt te hebben om te verwijderen is er geen interactie. Na op annuleren te klikken en te verversen van de overzichtspagina is de contactpersoon plots verdwenen.
+- [x] Een contactpersoon is te verwijderen terwijl deze gekoppeld is aan een applicatie. Een waarschuwing daarvoor is wel fijn.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+### Reactie 10 — @rubenvdlinde (2025-09-04)
+
+- Notificaties gaan over de berichten die je van eht system wil ontvangen
+- ook bij leveranciers kunnen de rollen gebruik-beheerder en functioneel-beheerder voorkomen
+- de temr is aanspreekpunt is voorgesteld door vng, iets anders mag uiterard maar wederom liever niet aan het datamodel rommelen (wat mij betreft kan die uberhuapt weg)
+- De contactpersoon wordt met een UUID getoond in het overzicht van producten. <- pakken we later op
+
+### Reactie 11 — @Makkmetp (2025-09-10)
+
+- [x] Nadat je een gebruiker hebt opgevoerd en deze via Acties > Bewerken de optie "Is aanspreekpunt" op "Ja" hebt gezet, wordt dit niet direct zichtbaar in het overzicht. Na op de knop `Vernieuwen` klikken ook niet. Wanneer je dan weer via bewerken de contactpersoon bekijkt, dan is de optie weer uitgevinkt in het formulier. (@rubenvdlinde)
+
+
+
+
+
+### Reactie 12 — @Makkmetp (2025-12-18)
+
+Nog te testen.
+Dit gaat over de wizard Applicatie publiceren in combinatie met Gebruikers aanmaken.
+In meerdere tests is naar voren gekomen dat de filtering op de lijst met contactpersonen niet goed functioneerde en je daar verschillende contactpersonen van andere organisaties tegen kwam.
+
+@rubenvdlinde wil je deze nog eerst zelf testen?
+
+### Reactie 13 — @rubenvdlinde (2026-01-28)
+
+Ik heb dit de afgelopen weken niet meer fout zien gaan
+
+### Reactie 14 — @markbacker (2026-01-29)
+
+Niet kunnen testen, omdat het niet lukte om een tweede contactpersoon/gebruiker aan te maken.
+
+
+
+### Reactie 15 — @rubenvdlinde (2026-02-12)
+
+Dubleur met #365
+
+---
+
+**Comment by @Makkmetp** — 2026-03-02
+
+#365 is een dubleur met #65 > Die issues gaan over het aanmaken van een contactpersoon.
+Dit issue gaat over het koppelen van contactpersonen aan applicaties.
+
+---
+
+**Comment by @Makkmetp** — 2026-03-02
+
+:white_check_mark:Het kunnen koppelen van een Contactpersoon/gebruiker aan een applicatie kan via de wizard Applicatie publiceren.
+
+
diff --git a/issues/85.md b/issues/85.md
new file mode 100644
index 00000000..c4fd741c
--- /dev/null
+++ b/issues/85.md
@@ -0,0 +1,62 @@
+# #85 — (VNGR) Als ontwikkelaar wil ik via een veilige, publieke API toegang hebben tot aanbodinformatie uit de Softwarecatalogus ID-104
+
+**Status:** OPEN | **Labels:** Aanbod, PvE eis, Bevinding, IGS review
+**Auteur:** @rubenvdlinde | **Datum:** 2025-02-07
+**Link:** https://github.com/VNG-Realisatie/Softwarecatalogus/issues/85
+
+---
+
+## Beschrijving
+
+zodat ik deze informatie in mijn eigen toepassingen kan integreren en benutten.
+
+zoals aanbiedende organisaties, aangeboden softwarepakketten, ondersteunde standaarden en andere relevante eigenschappen.
+
+---
+
+## Reacties (8)
+
+### Reactie 1 — @github-actions (2025-02-07)
+
+Bedankt voor het aanmaken van deze issue! 👋
+
+Voor meer informatie over onze spelregels voor het schrijven van issues, bekijk alsjeblieft onze [issues.md](../../issues/issues.md) documentatie.
+
+Belangrijke punten om te controleren:
+- ✍️ Is de beschrijving helder en compleet?
+- 📋 Zijn er duidelijke acceptatiecriteria toegevoegd?
+- 🎯 Is de context voldoende beschreven?
+
+### Reactie 2 — @rubenvdlinde (2025-02-19)
+
+Dit betreft dus het stuk software catalogus (voorzieningen etc) het stuk gemma wordt afgedekt door ander issues
+
+### Reactie 3 — @rubenvdlinde (2025-03-03)
+
+Architectuur, verbannden en inhoud staan beschreven op [de docusaurus](https://vng-realisatie.github.io/Softwarecatalogus/docs/GEMMA/api-introductie.) Er is een api test omgeving beschickbaar via https://conduction.stoplight.io/docs/vng-software-catalogue/or0zfjsiy36yp-get-all-views en de visuele test omgeving via https://vng.opencatalogi.nl/gemma
+
+### Reactie 4 — @matthiasoliveiro (2025-04-07)
+
+@Makkmetp kan je een status update geven?
+
+### Reactie 5 — @Makkmetp (2025-04-07)
+
+@terborg kan jij naar dit issue kijken? Wellicht dat @markbacker je verder kan ondersteunen.
+
+### Reactie 6 — @markbacker (2026-02-03)
+
+De verwijzingen naar documentatie en testpagina's die hierboven staan, werken niet meer. Zonder deze informatie kan ik de API niet vinden of beoordelen.
+
+### Reactie 7 — @rubenvdlinde (2026-02-20)
+
+Automatisch gengereerd documentatie is hersteld en de finden op https://redocly.github.io/redoc/?url=https%3A%2F%2Fbackend.accept.opencatalogi.nl%2Findex.php%2Fapps%2Fopenregister%2Fapi%2Fregisters%2F2%2Foas of het regjster actie menu
+
+
+
+### Reactie 8 — @markbacker (2026-02-24)
+
+De links hierboven werken nog steeds niet. Wel is er nu dus een ReDoc Interactive Demo pagina bij.
+
+Afspraak was dat documentatie in [docusaurus ](https://vng-realisatie.github.io/Softwarecatalogus/docs/intro) gemaakt gaat worden.
+
+Het is onduidelijk wat de status is van de docusaurus documentatie en hoe deze gegenereerde documentatie vanuit openregister hierin past.
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index 91621b27..5019d5cd 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -9,7 +9,7 @@
* @author Conduction b.v.
* @copyright 2024 Conduction B.V.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
@@ -49,6 +49,7 @@
use OCP\IGroupManager;
use OCP\IAppConfig;
use OCP\App\IAppManager;
+use OCP\ICacheFactory;
use Psr\Log\LoggerInterface;
use OCP\Security\ISecureRandom;
use Psr\Container\ContainerInterface;
@@ -63,7 +64,7 @@
* @package OCA\SoftwareCatalog\AppInfo
* @author Conduction b.v.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
class Application extends App implements IBootstrap
@@ -78,8 +79,8 @@ class Application extends App implements IBootstrap
*/
public function __construct()
{
- parent::__construct(self::APP_ID);
- }
+ parent::__construct(appName: self::APP_ID);
+ }//end __construct()
/**
* Register event listeners and services
@@ -90,58 +91,68 @@ public function __construct()
*/
public function register(IRegistrationContext $context): void
{
- include_once __DIR__ . '/../../vendor/autoload.php';
-
- // Register the handlers as services
- $context->registerService('OCA\SoftwareCatalog\Service\SoftwareCatalogue\OrganizationHandler', function (ContainerInterface $c) {
- return new \OCA\SoftwareCatalog\Service\SoftwareCatalogue\OrganizationHandler(
- $c->get(IGroupManager::class),
- $c->get(IUserManager::class),
- $c,
- $c->get(IAppManager::class),
- $c->get(\Psr\Log\LoggerInterface::class)
- );
- });
-
- $context->registerService('OCA\SoftwareCatalog\Service\SoftwareCatalogue\ContactPersonHandler', function (ContainerInterface $c) {
- return new \OCA\SoftwareCatalog\Service\SoftwareCatalogue\ContactPersonHandler(
- $c->get(IUserManager::class),
- $c->get(\OCP\Security\ISecureRandom::class),
- $c->get(IGroupManager::class),
- $c->get(IAppConfig::class),
- $c,
- $c->get(IAppManager::class),
- $c->get(\Psr\Log\LoggerInterface::class),
- $c->get(SymfonyEmailService::class),
- $c->get(IConfig::class)
- );
- });
-
- $context->registerService('OCA\SoftwareCatalog\Service\SoftwareCatalogue\GroupHandler', function (ContainerInterface $c) {
- return new \OCA\SoftwareCatalog\Service\SoftwareCatalogue\GroupHandler(
- $c->get(IGroupManager::class),
- $c->get(IUserManager::class),
- $c->get(IAppConfig::class),
- $c,
- $c->get(IAppManager::class),
- $c->get(\Psr\Log\LoggerInterface::class)
- );
- });
-
- $context->registerService('OCA\SoftwareCatalog\Service\SoftwareCatalogue\HierarchyHandler', function (ContainerInterface $c) {
- return new \OCA\SoftwareCatalog\Service\SoftwareCatalogue\HierarchyHandler(
- $c->get('OCA\SoftwareCatalog\Service\SoftwareCatalogue\OrganizationHandler'),
- $c->get('OCA\SoftwareCatalog\Service\SoftwareCatalogue\ContactPersonHandler'),
- $c->get(\Psr\Log\LoggerInterface::class)
- );
- });
-
-
-
- // Register TEST event listener for easily triggerable Nextcloud events
+ include_once __DIR__.'/../../vendor/autoload.php';
+
+ // Register the handlers as services.
+ $context->registerService(
+ 'OCA\SoftwareCatalog\Service\SoftwareCatalogue\OrganizationHandler',
+ function (ContainerInterface $c) {
+ return new \OCA\SoftwareCatalog\Service\SoftwareCatalogue\OrganizationHandler(
+ _groupManager: $c->get(IGroupManager::class),
+ _userManager: $c->get(IUserManager::class),
+ _container: $c,
+ _appManager: $c->get(IAppManager::class),
+ _logger: $c->get(\Psr\Log\LoggerInterface::class)
+ );
+ }
+ );
+
+ $context->registerService(
+ 'OCA\SoftwareCatalog\Service\SoftwareCatalogue\ContactPersonHandler',
+ function (ContainerInterface $c) {
+ return new \OCA\SoftwareCatalog\Service\SoftwareCatalogue\ContactPersonHandler(
+ _userManager: $c->get(IUserManager::class),
+ _secureRandom: $c->get(\OCP\Security\ISecureRandom::class),
+ _groupManager: $c->get(IGroupManager::class),
+ _config: $c->get(IAppConfig::class),
+ _container: $c,
+ _appManager: $c->get(IAppManager::class),
+ _logger: $c->get(\Psr\Log\LoggerInterface::class),
+ _emailService: $c->get(SymfonyEmailService::class),
+ config: $c->get(IConfig::class)
+ );
+ }
+ );
+
+ $context->registerService(
+ 'OCA\SoftwareCatalog\Service\SoftwareCatalogue\GroupHandler',
+ function (ContainerInterface $c) {
+ return new \OCA\SoftwareCatalog\Service\SoftwareCatalogue\GroupHandler(
+ _groupManager: $c->get(IGroupManager::class),
+ _userManager: $c->get(IUserManager::class),
+ _appConfig: $c->get(IAppConfig::class),
+ _container: $c,
+ _appManager: $c->get(IAppManager::class),
+ _logger: $c->get(\Psr\Log\LoggerInterface::class)
+ );
+ }
+ );
+
+ $context->registerService(
+ 'OCA\SoftwareCatalog\Service\SoftwareCatalogue\HierarchyHandler',
+ function (ContainerInterface $c) {
+ return new \OCA\SoftwareCatalog\Service\SoftwareCatalogue\HierarchyHandler(
+ _organizationHandler: $c->get('OCA\SoftwareCatalog\Service\SoftwareCatalogue\OrganizationHandler'),
+ _contactPersonHandler: $c->get('OCA\SoftwareCatalog\Service\SoftwareCatalogue\ContactPersonHandler'),
+ _logger: $c->get(\Psr\Log\LoggerInterface::class)
+ );
+ }
+ );
+
+ // Register TEST event listener for easily triggerable Nextcloud events.
$context->registerEventListener(UserLoggedInEvent::class, TestEventListener::class);
- // Register event listeners for OpenRegister events
+ // Register event listeners for OpenRegister events.
$context->registerEventListener(ObjectCreatedEvent::class, SoftwareCatalogEventListener::class);
$context->registerEventListener(ObjectUpdatedEvent::class, SoftwareCatalogEventListener::class);
$context->registerEventListener(ObjectDeletedEvent::class, SoftwareCatalogEventListener::class);
@@ -149,192 +160,256 @@ public function register(IRegistrationContext $context): void
$context->registerEventListener(ObjectUnlockedEvent::class, SoftwareCatalogEventListener::class);
$context->registerEventListener(ObjectRevertedEvent::class, SoftwareCatalogEventListener::class);
- // Register module compliance subscriber for module updates
+ // Register module compliance subscriber for module updates.
$context->registerEventListener(ObjectCreatedEvent::class, ModuleComplianceSubscriber::class);
$context->registerEventListener(ObjectUpdatedEvent::class, ModuleComplianceSubscriber::class);
- // Register module registration subscriber for auto-setting geregistreerdDoor
+ // Register module registration subscriber for auto-setting geregistreerdDoor.
$context->registerEventListener(ObjectCreatedEvent::class, ModuleRegistrationSubscriber::class);
$context->registerEventListener(ObjectUpdatedEvent::class, ModuleRegistrationSubscriber::class);
- // Register listener to sync user profile updates to contactpersoon objects
+ // Register listener to sync user profile updates to contactpersoon objects.
$context->registerEventListener(UserProfileUpdatedEvent::class, UserProfileUpdatedEventListener::class);
-
-
- // Organization event listeners removed - now using cron job for organization synchronization
- // Contact person event listeners are still active for real-time processing
-
- // Register new focused services
- $context->registerService(\OCA\SoftwareCatalog\Service\OrganisatieService::class, function ($container) {
- return new \OCA\SoftwareCatalog\Service\OrganisatieService(
- $container->get(\OCA\SoftwareCatalog\Service\SoftwareCatalogue\OrganizationHandler::class),
- $container->get('Psr\Log\LoggerInterface'),
- $container,
- $container->get('OCP\App\IAppManager'),
- $container->get(IAppConfig::class),
- $container->get(IUserManager::class),
- $container->get(SymfonyEmailService::class),
- );
- });
-
- $context->registerService(\OCA\SoftwareCatalog\Service\ContactpersoonService::class, function ($container) {
- return new \OCA\SoftwareCatalog\Service\ContactpersoonService(
- $container->get(\OCA\SoftwareCatalog\Service\SoftwareCatalogue\ContactPersonHandler::class),
- $container->get(\OCA\SoftwareCatalog\Service\SoftwareCatalogue\GroupHandler::class),
- $container->get(\OCA\SoftwareCatalog\Service\SoftwareCatalogue\HierarchyHandler::class),
- $container->get('Psr\Log\LoggerInterface'),
- $container,
- $container->get('OCP\App\IAppManager'),
- $container->get(IAppConfig::class),
- $container->get(SettingsService::class)
- );
- });
-
- // Register email service
- $context->registerService(SymfonyEmailService::class, function ($container) {
- return new SymfonyEmailService(
- $container->get(IAppConfig::class),
- $container->get('Psr\Log\LoggerInterface'),
- $container->get(SettingsService::class)
- );
- });
-
- // Register settings service
- $context->registerService(SettingsService::class, function ($container) {
- return new SettingsService(
- $container->get(IAppConfig::class),
- $container->get('OCP\IRequest'),
- $container,
- $container->get('OCP\App\IAppManager'),
- $container->get('Psr\Log\LoggerInterface')
- );
- });
-
- // Register organization sync service
- $context->registerService(\OCA\SoftwareCatalog\Service\OrganizationSyncService::class, function ($container) {
- return new \OCA\SoftwareCatalog\Service\OrganizationSyncService(
- $container->get(\OCA\SoftwareCatalog\Service\OrganisatieService::class),
- $container->get(\OCA\SoftwareCatalog\Service\ContactpersoonService::class),
- $container->get(SymfonyEmailService::class),
- $container->get(IAppConfig::class),
- $container->get('Psr\Log\LoggerInterface'),
- $container->get(SettingsService::class),
- $container->get(IDBConnection::class),
- $container->get(ContactPersonHandler::class),
- );
- });
-
- // Register gebruik sync service
- $context->registerService(\OCA\SoftwareCatalog\Service\GebruikSyncService::class, function ($container) {
- return new \OCA\SoftwareCatalog\Service\GebruikSyncService(
- $container->get('Psr\Log\LoggerInterface'),
- $container->get(SettingsService::class)
- );
- });
-
- // Event listener uses direct service access like OpenCatalogi - no service registration needed
-
- // Register module compliance service
- $context->registerService(\OCA\SoftwareCatalog\Service\ModuleComplianceService::class, function ($container) {
- return new \OCA\SoftwareCatalog\Service\ModuleComplianceService(
- $container,
- $container->get(SettingsService::class),
- $container->get('Psr\Log\LoggerInterface')
- );
- });
-
- // Register module registration service (auto-sets geregistreerdDoor)
- $context->registerService(\OCA\SoftwareCatalog\Service\ModuleRegistrationService::class, function ($container) {
- return new \OCA\SoftwareCatalog\Service\ModuleRegistrationService(
- $container,
- $container->get(SettingsService::class),
- $container->get('Psr\Log\LoggerInterface')
- );
- });
-
- // Register ArchiMate import service
- $context->registerService(\OCA\SoftwareCatalog\Service\ArchiMateImportService::class, function ($container) {
- return new \OCA\SoftwareCatalog\Service\ArchiMateImportService(
- $container->get(IAppConfig::class),
- $container->get('OCP\Files\IRootFolder'),
- $container->get('OCP\IUserSession'),
- $container->get('OCP\App\IAppManager'),
- $container,
- $container->get('Psr\Log\LoggerInterface'),
- $container->get(SettingsService::class),
- $container->get(\OCA\OpenRegister\Service\OrganisationService::class)
- );
- });
-
- // Register ArchiMate export service
- $context->registerService(\OCA\SoftwareCatalog\Service\ArchiMateExportService::class, function ($container) {
- return new \OCA\SoftwareCatalog\Service\ArchiMateExportService(
- $container->get('Psr\Log\LoggerInterface')
- );
- });
-
- // Register ArchiMate import/export service
- $context->registerService(\OCA\SoftwareCatalog\Service\ArchiMateService::class, function ($container) {
- return new \OCA\SoftwareCatalog\Service\ArchiMateService(
- $container->get(IAppConfig::class),
- $container->get('OCP\Files\IRootFolder'),
- $container->get('OCP\IUserSession'),
- $container->get('OCP\App\IAppManager'),
- $container,
- $container->get('Psr\Log\LoggerInterface'),
- $container->get(SettingsService::class),
- $container->get(\OCA\SoftwareCatalog\Service\ArchiMateImportService::class),
- $container->get(\OCA\SoftwareCatalog\Service\ArchiMateExportService::class)
- );
- });
-
- // Register View service for ArchiMate views with enrichment capabilities
- $context->registerService(\OCA\SoftwareCatalog\Service\ViewService::class, function ($container) {
- return new \OCA\SoftwareCatalog\Service\ViewService(
- $container->get(IAppConfig::class),
- $container->get('OCP\App\IAppManager'),
- $container,
- $container->get('Psr\Log\LoggerInterface'),
- $container->get(SettingsService::class),
- $container->get('OCP\IUserSession')
- );
- });
-
- // Register progress tracking service
- $context->registerService(\OCA\SoftwareCatalog\Service\ProgressTracker::class, function ($container) {
- return new \OCA\SoftwareCatalog\Service\ProgressTracker(
- $container->get('OCP\ISession'),
- $container->get('Psr\Log\LoggerInterface')
- );
- });
+ // Organization event listeners removed - now using cron job for organization synchronization.
+ // Contact person event listeners are still active for real-time processing.
+ // Register new focused services.
+ $context->registerService(
+ \OCA\SoftwareCatalog\Service\OrganisatieService::class,
+ function ($container) {
+ return new \OCA\SoftwareCatalog\Service\OrganisatieService(
+ organizationHandler: $container->get(
+ \OCA\SoftwareCatalog\Service\SoftwareCatalogue\OrganizationHandler::class
+ ),
+ logger: $container->get('Psr\Log\LoggerInterface'),
+ container: $container,
+ appManager: $container->get('OCP\App\IAppManager'),
+ config: $container->get(IAppConfig::class),
+ userManager: $container->get(IUserManager::class),
+ emailService: $container->get(SymfonyEmailService::class),
+ );
+ }
+ );
+
+ $context->registerService(
+ \OCA\SoftwareCatalog\Service\ContactpersoonService::class,
+ function ($container) {
+ return new \OCA\SoftwareCatalog\Service\ContactpersoonService(
+ contactPersonHandler: $container->get(
+ \OCA\SoftwareCatalog\Service\SoftwareCatalogue\ContactPersonHandler::class
+ ),
+ groupHandler: $container->get(
+ \OCA\SoftwareCatalog\Service\SoftwareCatalogue\GroupHandler::class
+ ),
+ hierarchyHandler: $container->get(
+ \OCA\SoftwareCatalog\Service\SoftwareCatalogue\HierarchyHandler::class
+ ),
+ logger: $container->get('Psr\Log\LoggerInterface'),
+ container: $container,
+ appManager: $container->get('OCP\App\IAppManager'),
+ config: $container->get(IAppConfig::class),
+ settingsService: $container->get(SettingsService::class)
+ );
+ }
+ );
+
+ // Register email service.
+ $context->registerService(
+ SymfonyEmailService::class,
+ function ($container) {
+ return new SymfonyEmailService(
+ config: $container->get(IAppConfig::class),
+ logger: $container->get('Psr\Log\LoggerInterface'),
+ settingsService: $container->get(SettingsService::class)
+ );
+ }
+ );
+
+ // Register settings service.
+ $context->registerService(
+ SettingsService::class,
+ function ($container) {
+ return new SettingsService(
+ config: $container->get(IAppConfig::class),
+ request: $container->get('OCP\IRequest'),
+ container: $container,
+ appManager: $container->get('OCP\App\IAppManager'),
+ logger: $container->get('Psr\Log\LoggerInterface')
+ );
+ }
+ );
+
+ // Register organization sync service.
+ $context->registerService(
+ \OCA\SoftwareCatalog\Service\OrganizationSyncService::class,
+ function ($container) {
+ return new \OCA\SoftwareCatalog\Service\OrganizationSyncService(
+ organisatieService: $container->get(\OCA\SoftwareCatalog\Service\OrganisatieService::class),
+ contactpersoonService: $container->get(\OCA\SoftwareCatalog\Service\ContactpersoonService::class),
+ emailService: $container->get(SymfonyEmailService::class),
+ config: $container->get(IAppConfig::class),
+ logger: $container->get('Psr\Log\LoggerInterface'),
+ settingsService: $container->get(SettingsService::class),
+ db: $container->get(IDBConnection::class),
+ contactpersonHandler: $container->get(ContactPersonHandler::class),
+ );
+ }
+ );
+
+ // Register gebruik sync service.
+ $context->registerService(
+ \OCA\SoftwareCatalog\Service\GebruikSyncService::class,
+ function ($container) {
+ return new \OCA\SoftwareCatalog\Service\GebruikSyncService(
+ logger: $container->get('Psr\Log\LoggerInterface'),
+ settingsService: $container->get(SettingsService::class)
+ );
+ }
+ );
+
+ // Event listener uses direct service access like OpenCatalogi - no service registration needed.
+ // Register module compliance service.
+ $context->registerService(
+ \OCA\SoftwareCatalog\Service\ModuleComplianceService::class,
+ function ($container) {
+ return new \OCA\SoftwareCatalog\Service\ModuleComplianceService(
+ container: $container,
+ settingsService: $container->get(SettingsService::class),
+ logger: $container->get('Psr\Log\LoggerInterface')
+ );
+ }
+ );
+
+ // Register module registration service (auto-sets geregistreerdDoor).
+ $context->registerService(
+ \OCA\SoftwareCatalog\Service\ModuleRegistrationService::class,
+ function ($container) {
+ return new \OCA\SoftwareCatalog\Service\ModuleRegistrationService(
+ container: $container,
+ settingsService: $container->get(SettingsService::class),
+ logger: $container->get('Psr\Log\LoggerInterface')
+ );
+ }
+ );
+
+ // Register module version service (creates default 1.0.0 version for new modules).
+ $context->registerService(
+ \OCA\SoftwareCatalog\Service\ModuleVersionService::class,
+ function ($container) {
+ return new \OCA\SoftwareCatalog\Service\ModuleVersionService(
+ container: $container,
+ settingsService: $container->get(SettingsService::class),
+ logger: $container->get('Psr\Log\LoggerInterface')
+ );
+ }
+ );
+
+ // Register ArchiMate import service.
+ $context->registerService(
+ \OCA\SoftwareCatalog\Service\ArchiMateImportService::class,
+ function ($container) {
+ return new \OCA\SoftwareCatalog\Service\ArchiMateImportService(
+ config: $container->get(IAppConfig::class),
+ rootFolder: $container->get('OCP\Files\IRootFolder'),
+ userSession: $container->get('OCP\IUserSession'),
+ appManager: $container->get('OCP\App\IAppManager'),
+ container: $container,
+ logger: $container->get('Psr\Log\LoggerInterface'),
+ settingsService: $container->get(SettingsService::class),
+ organisationService: $container->get(\OCA\OpenRegister\Service\OrganisationService::class)
+ );
+ }
+ );
+
+ // Register ArchiMate export service.
+ $context->registerService(
+ \OCA\SoftwareCatalog\Service\ArchiMateExportService::class,
+ function ($container) {
+ return new \OCA\SoftwareCatalog\Service\ArchiMateExportService(
+ logger: $container->get('Psr\Log\LoggerInterface')
+ );
+ }
+ );
+
+ // Register ArchiMate import/export service.
+ $context->registerService(
+ \OCA\SoftwareCatalog\Service\ArchiMateService::class,
+ function ($container) {
+ return new \OCA\SoftwareCatalog\Service\ArchiMateService(
+ config: $container->get(IAppConfig::class),
+ rootFolder: $container->get('OCP\Files\IRootFolder'),
+ userSession: $container->get('OCP\IUserSession'),
+ appManager: $container->get('OCP\App\IAppManager'),
+ container: $container,
+ logger: $container->get('Psr\Log\LoggerInterface'),
+ settingsService: $container->get(SettingsService::class),
+ importService: $container->get(\OCA\SoftwareCatalog\Service\ArchiMateImportService::class),
+ exportService: $container->get(\OCA\SoftwareCatalog\Service\ArchiMateExportService::class)
+ );
+ }
+ );
+
+ // Register View service for ArchiMate views with enrichment capabilities.
+ $context->registerService(
+ \OCA\SoftwareCatalog\Service\ViewService::class,
+ function ($container) {
+ return new \OCA\SoftwareCatalog\Service\ViewService(
+ config: $container->get(IAppConfig::class),
+ appManager: $container->get('OCP\App\IAppManager'),
+ container: $container,
+ logger: $container->get('Psr\Log\LoggerInterface'),
+ settingsService: $container->get(SettingsService::class),
+ userSession: $container->get('OCP\IUserSession'),
+ cacheFactory: $container->get(ICacheFactory::class)
+ );
+ }
+ );
+
+ // Register progress tracking service.
+ $context->registerService(
+ \OCA\SoftwareCatalog\Service\ProgressTracker::class,
+ function ($container) {
+ return new \OCA\SoftwareCatalog\Service\ProgressTracker(
+ session: $container->get('OCP\ISession'),
+ logger: $container->get('Psr\Log\LoggerInterface')
+ );
+ }
+ );
// Register background job for organization contact synchronization.
- $context->registerService(\OCA\SoftwareCatalog\BackgroundJob\OrganizationContactSyncJob::class, function ($container) {
- return new \OCA\SoftwareCatalog\BackgroundJob\OrganizationContactSyncJob(
- $container->get('OCP\AppFramework\Utility\ITimeFactory'),
- $container->get(\OCA\SoftwareCatalog\Service\OrganizationSyncService::class),
- $container->get('Psr\Log\LoggerInterface')
- );
- });
-
- // Register ContactpersonenController with explicit dependencies for /me endpoint
- $context->registerService(\OCA\SoftwareCatalog\Controller\ContactpersonenController::class, function ($container) {
- return new \OCA\SoftwareCatalog\Controller\ContactpersonenController(
- self::APP_ID,
- $container->get('OCP\IRequest'),
- $container->get(SettingsService::class),
- $container->get('OCA\SoftwareCatalog\Service\SoftwareCatalogue\ContactPersonHandler'),
- $container->get(\OCA\SoftwareCatalog\Service\ContactpersoonService::class),
- $container->get('OCP\IUserManager'),
- $container->get('OCP\IGroupManager'),
- $container->get('OCP\IUserSession'),
- $container,
- $container->get('OCP\Security\ISecureRandom'),
- $container->get('Psr\Log\LoggerInterface')
- );
- });
- }
+ $context->registerService(
+ \OCA\SoftwareCatalog\BackgroundJob\OrganizationContactSyncJob::class,
+ function ($container) {
+ return new \OCA\SoftwareCatalog\BackgroundJob\OrganizationContactSyncJob(
+ time: $container->get('OCP\AppFramework\Utility\ITimeFactory'),
+ syncService: $container->get(\OCA\SoftwareCatalog\Service\OrganizationSyncService::class),
+ logger: $container->get('Psr\Log\LoggerInterface')
+ );
+ }
+ );
+
+ // Register ContactpersonenController with explicit dependencies for /me endpoint.
+ $context->registerService(
+ \OCA\SoftwareCatalog\Controller\ContactpersonenController::class,
+ function ($container) {
+ return new \OCA\SoftwareCatalog\Controller\ContactpersonenController(
+ appName: self::APP_ID,
+ request: $container->get('OCP\IRequest'),
+ settingsService: $container->get(SettingsService::class),
+ contactPersonHandler: $container->get(
+ 'OCA\SoftwareCatalog\Service\SoftwareCatalogue\ContactPersonHandler'
+ ),
+ contactpersoonService: $container->get(\OCA\SoftwareCatalog\Service\ContactpersoonService::class),
+ userManager: $container->get('OCP\IUserManager'),
+ groupManager: $container->get('OCP\IGroupManager'),
+ userSession: $container->get('OCP\IUserSession'),
+ container: $container,
+ secureRandom: $container->get('OCP\Security\ISecureRandom'),
+ logger: $container->get('Psr\Log\LoggerInterface')
+ );
+ }
+ );
+ }//end register()
/**
* Boot the application
@@ -345,26 +420,8 @@ public function register(IRegistrationContext $context): void
*/
public function boot(IBootContext $context): void
{
- // Initialization is now handled by the Repair step (InitializeSettings)
- // which runs only during app install/upgrade, not on every request.
- // See lib/Repair/InitializeSettings.php
-
- $container = $context->getServerContainer();
- $logger = $container->get(LoggerInterface::class);
-
- // Register background job for organization contact synchronization
- try {
- $jobList = $container->get('OCP\BackgroundJob\IJobList');
- if (!$jobList->has(\OCA\SoftwareCatalog\BackgroundJob\OrganizationContactSyncJob::class, null)) {
- $jobList->add(\OCA\SoftwareCatalog\BackgroundJob\OrganizationContactSyncJob::class);
- $logger->debug('SoftwareCatalog boot: Background job registered');
- }
- } catch (\Exception $e) {
- $logger->error('SoftwareCatalog boot: Failed to register background job', [
- 'exception' => $e->getMessage()
- ]);
- }
- }
-
-
-}
+ // Background jobs are registered declaratively in appinfo/info.xml.
+ // Initialization is handled by the Repair step (InitializeSettings).
+ // No per-request work needed here.
+ }//end boot()
+}//end class
diff --git a/lib/BackgroundJob/CronjobContextTrait.php b/lib/BackgroundJob/CronjobContextTrait.php
index edd3689f..74b50b44 100644
--- a/lib/BackgroundJob/CronjobContextTrait.php
+++ b/lib/BackgroundJob/CronjobContextTrait.php
@@ -3,17 +3,19 @@
/**
* Cronjob Context Trait
*
- * This trait provides functionality for background jobs to set and clear
- * user and organisation context during execution. This allows cronjobs
- * to run with proper RBAC permissions based on configured settings.
+ * Since all sync operations now use _rbac: false and _multitenancy: false,
+ * this trait is no longer needed. Background jobs are system-level operations
+ * that do not require user context. Will be removed in a future version.
+ * See OrganizationContactSyncJob for the simplified approach.
*
- * @category Trait
- * @package OCA\SoftwareCatalog\BackgroundJob
- * @author Conduction b.v.
- * @copyright 2024 Conduction B.V.
- * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
- * @link https://github.com/ConductionNL/SoftwareCatalog
+ * @category Trait
+ * @package OCA\SoftwareCatalog\BackgroundJob
+ * @author Conduction b.v.
+ * @copyright 2024 Conduction B.V.
+ * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
+ * @deprecated Will be removed in a future version.
*/
declare(strict_types=1);
@@ -39,6 +41,7 @@
*/
trait CronjobContextTrait
{
+
/**
* The user that was set for this cronjob session
*
@@ -56,7 +59,7 @@ trait CronjobContextTrait
/**
* Whether the context was successfully set
*
- * @var bool
+ * @var boolean
*/
private bool $contextSet = false;
@@ -76,35 +79,44 @@ protected function setCronjobContext(string $jobId): bool
try {
// Get the settings service to retrieve configuration.
$settingsService = \OC::$server->get(\OCA\SoftwareCatalog\Service\SettingsService::class);
- $context = $settingsService->getCronjobContext($jobId);
+ $context = $settingsService->getCronjobContext($jobId);
if ($context === null) {
- $this->getLogger()->warning('[CRONJOB] No context configured for cronjob', [
- 'jobId' => $jobId
- ]);
+ $this->getLogger()->warning(
+ '[CRONJOB] No context configured for cronjob',
+ [
+ 'jobId' => $jobId,
+ ]
+ );
return false;
}
// Check if job is enabled.
- if (!($context['enabled'] ?? true)) {
- $this->getLogger()->info('[CRONJOB] Cronjob is disabled', [
- 'jobId' => $jobId
- ]);
+ if (($context['enabled'] ?? true) === false) {
+ $this->getLogger()->info(
+ '[CRONJOB] Cronjob is disabled',
+ [
+ 'jobId' => $jobId,
+ ]
+ );
return false;
}
- $userId = $context['userId'];
+ $userId = $context['userId'];
$organisationUuid = $context['organisationUuid'];
// Get the user.
$userManager = \OC::$server->get(IUserManager::class);
- $user = $userManager->get($userId);
+ $user = $userManager->get($userId);
if ($user === null) {
- $this->getLogger()->error('[CRONJOB] Configured user not found', [
- 'jobId' => $jobId,
- 'userId' => $userId
- ]);
+ $this->getLogger()->error(
+ '[CRONJOB] Configured user not found',
+ [
+ 'jobId' => $jobId,
+ 'userId' => $userId,
+ ]
+ );
return false;
}
@@ -116,7 +128,7 @@ protected function setCronjobContext(string $jobId): bool
$this->cronjobOrganisationUuid = $organisationUuid;
// Set the active organisation in OpenRegister if available.
- if (class_exists('\OCA\OpenRegister\Service\OrganisationService')) {
+ if (class_exists(classname: '\OCA\OpenRegister\Service\OrganisationService') === true) {
try {
$config = \OC::$server->get(IConfig::class);
@@ -128,32 +140,39 @@ protected function setCronjobContext(string $jobId): bool
$organisationUuid
);
- $this->getLogger()->info('[CRONJOB] Context set successfully', [
- 'jobId' => $jobId,
- 'userId' => $userId,
- 'organisationUuid' => $organisationUuid
- ]);
-
+ $this->getLogger()->info(
+ '[CRONJOB] Context set successfully',
+ [
+ 'jobId' => $jobId,
+ 'userId' => $userId,
+ 'organisationUuid' => $organisationUuid,
+ ]
+ );
} catch (\Exception $e) {
- $this->getLogger()->warning('[CRONJOB] Failed to set active organisation', [
- 'jobId' => $jobId,
- 'error' => $e->getMessage()
- ]);
- }
- }
+ $this->getLogger()->warning(
+ '[CRONJOB] Failed to set active organisation',
+ [
+ 'jobId' => $jobId,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end if
$this->contextSet = true;
return true;
-
} catch (\Exception $e) {
- $this->getLogger()->error('[CRONJOB] Failed to set cronjob context', [
- 'jobId' => $jobId,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->getLogger()->error(
+ '[CRONJOB] Failed to set cronjob context',
+ [
+ 'jobId' => $jobId,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return false;
- }
- }
+ }//end try
+ }//end setCronjobContext()
/**
* Clear the cronjob context after execution.
@@ -168,7 +187,7 @@ protected function setCronjobContext(string $jobId): bool
protected function clearCronjobContext(string $jobId): void
{
try {
- if (!$this->contextSet) {
+ if ($this->contextSet === false) {
return;
}
@@ -176,21 +195,26 @@ protected function clearCronjobContext(string $jobId): void
$userSession = \OC::$server->get(IUserSession::class);
$userSession->setUser(null);
- $this->getLogger()->debug('[CRONJOB] Context cleared', [
- 'jobId' => $jobId
- ]);
+ $this->getLogger()->debug(
+ '[CRONJOB] Context cleared',
+ [
+ 'jobId' => $jobId,
+ ]
+ );
$this->cronjobUser = null;
$this->cronjobOrganisationUuid = null;
$this->contextSet = false;
-
} catch (\Exception $e) {
- $this->getLogger()->warning('[CRONJOB] Failed to clear cronjob context', [
- 'jobId' => $jobId,
- 'error' => $e->getMessage()
- ]);
- }
- }
+ $this->getLogger()->warning(
+ '[CRONJOB] Failed to clear cronjob context',
+ [
+ 'jobId' => $jobId,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end clearCronjobContext()
/**
* Check if context is set for this cronjob session.
@@ -200,7 +224,7 @@ protected function clearCronjobContext(string $jobId): void
protected function hasContext(): bool
{
return $this->contextSet;
- }
+ }//end hasContext()
/**
* Get the current cronjob user.
@@ -210,7 +234,7 @@ protected function hasContext(): bool
protected function getCronjobUser(): ?\OCP\IUser
{
return $this->cronjobUser;
- }
+ }//end getCronjobUser()
/**
* Get the current cronjob organisation UUID.
@@ -220,7 +244,7 @@ protected function getCronjobUser(): ?\OCP\IUser
protected function getCronjobOrganisationUuid(): ?string
{
return $this->cronjobOrganisationUuid;
- }
+ }//end getCronjobOrganisationUuid()
/**
* Get the logger instance.
@@ -230,7 +254,4 @@ protected function getCronjobOrganisationUuid(): ?string
* @return LoggerInterface
*/
abstract protected function getLogger(): LoggerInterface;
-}
-
-
-
+}//end trait
diff --git a/lib/BackgroundJob/OrganizationContactSyncJob.php b/lib/BackgroundJob/OrganizationContactSyncJob.php
index 19cc1b23..4186b8c6 100644
--- a/lib/BackgroundJob/OrganizationContactSyncJob.php
+++ b/lib/BackgroundJob/OrganizationContactSyncJob.php
@@ -6,13 +6,13 @@
* This file contains the background job class for synchronizing organizations and contact persons
* between SoftwareCatalog objects and OpenRegister entities.
*
- * @category BackgroundJob
- * @package OCA\SoftwareCatalog\BackgroundJob
- * @author Conduction b.v.
+ * @category BackgroundJob
+ * @package OCA\SoftwareCatalog\BackgroundJob
+ * @author Conduction b.v.
* @copyright 2024 Conduction B.V.
- * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
- * @link https://github.com/ConductionNL/SoftwareCatalog
+ * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
+ * @version GIT: 1.0.0
+ * @link https://github.com/ConductionNL/SoftwareCatalog
*/
declare(strict_types=1);
@@ -31,24 +31,18 @@
* and OpenRegister entities using full sync (all organizations). All business logic is
* delegated to the OrganizationSyncService.
*
- * The job uses CronjobContextTrait to set user and organisation context based on
- * administrator configuration, enabling proper RBAC authorization during execution.
+ * All sync operations use _rbac: false and _multitenancy: false since this is a
+ * system-level background job that needs unrestricted access to all objects.
*
* @category BackgroundJob
* @package OCA\SoftwareCatalog\BackgroundJob
* @author Conduction b.v.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT: 1.0.0
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
class OrganizationContactSyncJob extends TimedJob
{
- use CronjobContextTrait;
-
- /**
- * The cronjob identifier for configuration lookup
- */
- private const JOB_ID = 'organization_contact_sync';
/**
* Organization synchronization service
@@ -76,28 +70,19 @@ public function __construct(
OrganizationSyncService $organizationSyncService,
LoggerInterface $logger
) {
- parent::__construct($timeFactory);
- $this->setInterval(300); // 5 minutes.
+ parent::__construct(time: $timeFactory);
+ $this->setInterval(interval: 300);
+ // 5 minutes.
$this->organizationSyncService = $organizationSyncService;
$this->logger = $logger;
- }
-
- /**
- * Get the logger instance for the trait.
- *
- * @return LoggerInterface
- */
- protected function getLogger(): LoggerInterface
- {
- return $this->logger;
- }
+ }//end __construct()
/**
* Runs the background job
*
- * This method sets the user and organisation context based on configuration,
- * then delegates all synchronization logic to the OrganizationSyncService.
+ * Delegates all synchronization logic to the OrganizationSyncService.
* The service handles all business logic, logging, and error handling.
+ * No user context is needed since all ObjectService calls use _rbac: false.
*
* @param mixed $argument Job arguments (not used)
*
@@ -105,23 +90,6 @@ protected function getLogger(): LoggerInterface
*/
protected function run($argument): void
{
- try {
- // Set the cronjob context (user and organisation) from configuration.
- $contextSet = $this->setCronjobContext(self::JOB_ID);
-
- if (!$contextSet) {
- $this->logger->warning('[CRONJOB] OrganizationContactSyncJob: Running without context - RBAC checks may fail', [
- 'jobId' => self::JOB_ID,
- 'hint' => 'Configure user and organisation in Settings > Cronjobs to enable proper authorization'
- ]);
- }
-
- // Delegate all synchronization logic to the service.
- $this->organizationSyncService->performScheduledSync();
-
- } finally {
- // Always clear the context when done.
- $this->clearCronjobContext(self::JOB_ID);
- }
- }
-}
+ $this->organizationSyncService->performScheduledSync();
+ }//end run()
+}//end class
diff --git a/lib/Controller/AanbodController.php b/lib/Controller/AanbodController.php
index 2b37211c..c9cfc4b2 100644
--- a/lib/Controller/AanbodController.php
+++ b/lib/Controller/AanbodController.php
@@ -1,22 +1,22 @@
+ *
+ * @category Controller
+ * @package OCA\SoftwareCatalog\Controller
+ * @author Conduction b.v.
* @copyright 2024 Conduction B.V.
- * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- * @version 1.0.0
- * @link https://github.com/ConductionNL/SoftwareCatalog
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
*/
+declare(strict_types=1);
+
namespace OCA\SoftwareCatalog\Controller;
use OCP\AppFramework\Controller;
@@ -27,30 +27,30 @@
use Psr\Log\LoggerInterface;
/**
- * Controller for handling aanbod (offers) API operations
- *
+ * Controller for handling aanbod (offers) API operations.
+ *
* This controller provides REST API endpoints for managing aanbod objects where
* the active organization is involved either as afnemer (consumer) or aanbieder
* (provider), and for accepting or denying these offers.
- *
- * @category Controller
- * @package OCA\SoftwareCatalog\Controller
- * @author Conduction b.v.
+ *
+ * @category Controller
+ * @package OCA\SoftwareCatalog\Controller
+ * @author Conduction b.v.
* @copyright 2024 Conduction B.V.
- * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- * @version 1.0.0
- * @link https://github.com/ConductionNL/SoftwareCatalog
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
*/
class AanbodController extends Controller
{
/**
- * Constructor for AanbodController
- *
- * @param string $appName The name of the app
- * @param IRequest $request The HTTP request object
- * @param IUserSession $userSession The user session service for getting the current user
- * @param AanbodService $aanbodService The business logic service
- * @param LoggerInterface $logger The logger service for debugging and error reporting
+ * Constructor for AanbodController.
+ *
+ * @param string $appName The name of the app
+ * @param IRequest $request The HTTP request object
+ * @param IUserSession $userSession The user session service for getting the current user
+ * @param AanbodService $aanbodService The business logic service
+ * @param LoggerInterface $logger The logger service for debugging and error reporting
*/
public function __construct(
string $appName,
@@ -59,260 +59,334 @@ public function __construct(
private readonly AanbodService $aanbodService,
private readonly LoggerInterface $logger
) {
- parent::__construct($appName, $request);
- }
+ parent::__construct(appName: $appName, request: $request);
+ }//end __construct()
/**
- * Get all aanbod objects (modules, diensten, koppelingen, gebruiks)
- *
+ * Get all aanbod objects (modules, diensten, koppelingen, gebruiks).
+ *
* API Endpoint: GET /api/aanbod
- *
+ *
* Returns modules, diensten, and koppelingen where the current organisation
* is in the aanbieder property, or gebruiks where the current organisation
* is in the afnemer property. Excludes objects where @self.organisation
* equals the current organisation.
- *
+ *
* Query Parameters:
* - limit (int): Maximum number of results to return
* - offset (int): Number of results to skip for pagination
* - page (int): Page number for pagination
- *
+ *
+ * @return JSONResponse JSON response with aanbod objects array
+ *
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
- *
- * @return JSONResponse JSON response with aanbod objects array
*/
public function getAanbod(): JSONResponse
{
- $this->logger->info('API: Getting aanbod objects', [
- 'endpoint' => '/api/aanbod',
- 'method' => 'GET',
- 'query_params' => $this->request->getParams()
- ]);
+ $this->logger->info(
+ 'API: Getting aanbod objects',
+ [
+ 'endpoint' => '/api/aanbod',
+ 'method' => 'GET',
+ 'query_params' => $this->request->getParams(),
+ ]
+ );
try {
- // Parse query parameters for filtering options
+ // Parse query parameters for filtering options.
$options = $this->parseQueryOptions();
-
- // Get aanbod objects from service
+
+ // Get aanbod objects from service.
$result = $this->aanbodService->getAanbod($options);
-
- // Determine HTTP status code based on whether there's an error
- $statusCode = isset($result['error']) ? 500 : 200;
-
- $this->logger->info('API: Aanbod request completed', [
- 'total' => $result['total'] ?? 0,
- 'results_count' => count($result['results'] ?? []),
- 'has_error' => isset($result['error'])
- ]);
-
+
+ // Determine HTTP status code based on whether there's an error.
+ if (isset($result['error']) === true) {
+ $statusCode = 500;
+ } else {
+ $statusCode = 200;
+ }
+
+ $this->logger->info(
+ 'API: Aanbod request completed',
+ [
+ 'total' => $result['total'] ?? 0,
+ 'results_count' => count($result['results'] ?? []),
+ 'has_error' => isset($result['error']) === true,
+ ]
+ );
+
return new JSONResponse($result, $statusCode);
-
} catch (\Exception $e) {
- $this->logger->error('API: Failed to get aanbod objects', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
-
- return new JSONResponse([
- 'results' => [],
- 'total' => 0,
- 'page' => 1,
- 'pages' => 0,
- 'limit' => 20,
- 'offset' => 0,
- 'error' => 'Internal server error: ' . $e->getMessage()
- ], 500);
- }
- }
+ $this->logger->error(
+ 'API: Failed to get aanbod objects',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'results' => [],
+ 'total' => 0,
+ 'page' => 1,
+ 'pages' => 0,
+ 'limit' => 20,
+ 'offset' => 0,
+ 'error' => 'Internal server error: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getAanbod()
/**
- * Accept an aanbod object (set @self.organisation to current organisation)
- *
+ * Accept an aanbod object (set @self.organisation to current organisation).
+ *
* API Endpoint: PUT /api/aanbod/{uuid}/accept
- *
+ *
* Sets the '@self.organisation' property of an aanbod object to the active
* organization. This operation is only allowed if the active organization
* is the afnemer (for gebruiks) or aanbieder (for modules, diensten, koppelingen).
- *
+ *
+ * @param string $uuid The UUID of the aanbod object to accept
+ *
+ * @return JSONResponse JSON response with success status and updated object
+ *
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
- *
- * @param string $uuid The UUID of the aanbod object to accept
- * @return JSONResponse JSON response with success status and updated object
*/
public function acceptAanbod(string $uuid): JSONResponse
{
- $this->logger->info('API: Accepting aanbod object', [
- 'endpoint' => "/api/aanbod/{$uuid}/accept",
- 'method' => 'PUT',
- 'aanbod_id' => $uuid
- ]);
+ $this->logger->info(
+ 'API: Accepting aanbod object',
+ [
+ 'endpoint' => "/api/aanbod/{$uuid}/accept",
+ 'method' => 'PUT',
+ 'aanbod_id' => $uuid,
+ ]
+ );
try {
- // Validate input
- if (empty($uuid)) {
- return new JSONResponse([
- 'success' => false,
- 'error' => 'Aanbod UUID is required',
- 'aanbod' => null
- ], 400);
+ // Validate input.
+ if (empty($uuid) === true) {
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'error' => 'Aanbod UUID is required',
+ 'aanbod' => null,
+ ],
+ 400
+ );
}
- // Parse any additional options from request body
- $options = [];
+ // Parse any additional options from request body.
+ $options = [];
$requestBody = $this->request->getParams();
- if (!empty($requestBody)) {
- $options = array_filter($requestBody, function($key) {
- return !in_array($key, ['uuid']); // Exclude path parameters
- }, ARRAY_FILTER_USE_KEY);
+ if (empty($requestBody) === false) {
+ $options = array_filter(
+ $requestBody,
+ function ($key) {
+ // Exclude path parameters.
+ return in_array(needle: $key, haystack: ['uuid']) === false;
+ },
+ ARRAY_FILTER_USE_KEY
+ );
}
-
- // Accept aanbod object via service
- $result = $this->aanbodService->acceptAanbod($uuid, $options);
-
- // Determine appropriate HTTP status code
- $statusCode = $result['success'] ? 200 : ($result['error'] === 'Aanbod object not found' ? 404 :
- (strpos($result['error'] ?? '', 'Operation not allowed') !== false ? 403 : 500));
-
- $this->logger->info('API: Accept aanbod request completed', [
- 'aanbod_id' => $uuid,
- 'success' => $result['success'],
- 'status_code' => $statusCode
- ]);
-
+
+ // Accept aanbod object via service.
+ $result = $this->aanbodService->acceptAanbod(uuid: $uuid, options: $options);
+
+ // Determine appropriate HTTP status code.
+ if ($result['success'] === true) {
+ $statusCode = 200;
+ } else if ($result['error'] === 'Aanbod object not found') {
+ $statusCode = 404;
+ } else if (strpos(haystack: ($result['error'] ?? ''), needle: 'Operation not allowed') !== false) {
+ $statusCode = 403;
+ } else {
+ $statusCode = 500;
+ }
+
+ $this->logger->info(
+ 'API: Accept aanbod request completed',
+ [
+ 'aanbod_id' => $uuid,
+ 'success' => $result['success'],
+ 'status_code' => $statusCode,
+ ]
+ );
+
return new JSONResponse($result, $statusCode);
-
} catch (\Exception $e) {
- $this->logger->error('API: Failed to accept aanbod object', [
- 'aanbod_id' => $uuid,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
-
- return new JSONResponse([
- 'success' => false,
- 'error' => 'Internal server error: ' . $e->getMessage(),
- 'aanbod' => null
- ], 500);
- }
- }
+ $this->logger->error(
+ 'API: Failed to accept aanbod object',
+ [
+ 'aanbod_id' => $uuid,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'error' => 'Internal server error: '.$e->getMessage(),
+ 'aanbod' => null,
+ ],
+ 500
+ );
+ }//end try
+ }//end acceptAanbod()
/**
- * Deny an aanbod object (delete it)
- *
+ * Deny an aanbod object (delete it).
+ *
* API Endpoint: DELETE /api/aanbod/{uuid}/deny
- *
+ *
* Deletes an aanbod object. This operation is only allowed if the active
* organization is the afnemer (for gebruiks) or aanbieder (for modules,
* diensten, koppelingen).
- *
+ *
+ * @param string $uuid The UUID of the aanbod object to deny
+ *
+ * @return JSONResponse JSON response with success status and deletion details
+ *
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
- *
- * @param string $uuid The UUID of the aanbod object to deny
- * @return JSONResponse JSON response with success status and deletion details
*/
public function denyAanbod(string $uuid): JSONResponse
{
- $this->logger->info('API: Denying aanbod object', [
- 'endpoint' => "/api/aanbod/{$uuid}/deny",
- 'method' => 'DELETE',
- 'aanbod_id' => $uuid
- ]);
+ $this->logger->info(
+ 'API: Denying aanbod object',
+ [
+ 'endpoint' => "/api/aanbod/{$uuid}/deny",
+ 'method' => 'DELETE',
+ 'aanbod_id' => $uuid,
+ ]
+ );
try {
- // Validate input
- if (empty($uuid)) {
- return new JSONResponse([
- 'success' => false,
- 'error' => 'Aanbod UUID is required',
- 'deleted' => false
- ], 400);
+ // Validate input.
+ if (empty($uuid) === true) {
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'error' => 'Aanbod UUID is required',
+ 'deleted' => false,
+ ],
+ 400
+ );
}
- // Parse any additional options from request body
- $options = [];
+ // Parse any additional options from request body.
+ $options = [];
$requestBody = $this->request->getParams();
- if (!empty($requestBody)) {
- $options = array_filter($requestBody, function($key) {
- return !in_array($key, ['uuid']); // Exclude path parameters
- }, ARRAY_FILTER_USE_KEY);
+ if (empty($requestBody) === false) {
+ $options = array_filter(
+ $requestBody,
+ function ($key) {
+ // Exclude path parameters.
+ return in_array(needle: $key, haystack: ['uuid']) === false;
+ },
+ ARRAY_FILTER_USE_KEY
+ );
+ }
+
+ // Deny aanbod object via service.
+ $result = $this->aanbodService->denyAanbod(uuid: $uuid, options: $options);
+
+ // Determine appropriate HTTP status code.
+ if ($result['success'] === true) {
+ $statusCode = 200;
+ } else if ($result['error'] === 'Aanbod object not found') {
+ $statusCode = 404;
+ } else if (strpos(haystack: ($result['error'] ?? ''), needle: 'Operation not allowed') !== false) {
+ $statusCode = 403;
+ } else {
+ $statusCode = 500;
}
-
- // Deny aanbod object via service
- $result = $this->aanbodService->denyAanbod($uuid, $options);
-
- // Determine appropriate HTTP status code
- $statusCode = $result['success'] ? 200 : ($result['error'] === 'Aanbod object not found' ? 404 :
- (strpos($result['error'] ?? '', 'Operation not allowed') !== false ? 403 : 500));
-
- $this->logger->info('API: Deny aanbod request completed', [
- 'aanbod_id' => $uuid,
- 'success' => $result['success'],
- 'deleted' => $result['deleted'] ?? false,
- 'status_code' => $statusCode
- ]);
-
+
+ $this->logger->info(
+ 'API: Deny aanbod request completed',
+ [
+ 'aanbod_id' => $uuid,
+ 'success' => $result['success'],
+ 'deleted' => $result['deleted'] ?? false,
+ 'status_code' => $statusCode,
+ ]
+ );
+
return new JSONResponse($result, $statusCode);
-
} catch (\Exception $e) {
- $this->logger->error('API: Failed to deny aanbod object', [
- 'aanbod_id' => $uuid,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
-
- return new JSONResponse([
- 'success' => false,
- 'error' => 'Internal server error: ' . $e->getMessage(),
- 'deleted' => false
- ], 500);
- }
- }
+ $this->logger->error(
+ 'API: Failed to deny aanbod object',
+ [
+ 'aanbod_id' => $uuid,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'error' => 'Internal server error: '.$e->getMessage(),
+ 'deleted' => false,
+ ],
+ 500
+ );
+ }//end try
+ }//end denyAanbod()
/**
- * Parse query parameters into options array
- *
+ * Parse query parameters into options array.
+ *
* @return array Parsed options array
*/
private function parseQueryOptions(): array
{
$options = [];
-
- // Parse pagination parameters
+
+ // Parse pagination parameters.
$limit = $this->request->getParam('_limit') ?? $this->request->getParam('limit');
- if ($limit !== null && is_numeric($limit)) {
- $options['_limit'] = (int)$limit;
- $options['limit'] = (int)$limit; // Keep both for compatibility
+ if ($limit !== null && is_numeric($limit) === true) {
+ $options['_limit'] = (int) $limit;
+ // Keep both for compatibility.
+ $options['limit'] = (int) $limit;
}
-
+
$offset = $this->request->getParam('_offset') ?? $this->request->getParam('offset');
- if ($offset !== null && is_numeric($offset)) {
- $options['_offset'] = (int)$offset;
- $options['offset'] = (int)$offset; // Keep both for compatibility
+ if ($offset !== null && is_numeric($offset) === true) {
+ $options['_offset'] = (int) $offset;
+ // Keep both for compatibility.
+ $options['offset'] = (int) $offset;
}
-
+
$page = $this->request->getParam('_page') ?? $this->request->getParam('page');
- if ($page !== null && is_numeric($page)) {
- $options['_page'] = (int)$page;
+ if ($page !== null && is_numeric($page) === true) {
+ $options['_page'] = (int) $page;
}
-
- // Force database source for real-time data
+
+ // Force database source for real-time data.
$options['_source'] = 'database';
-
- $this->logger->debug('Parsed query options for Aanbod', [
- 'raw_params' => [
- 'limit' => $limit,
- 'offset' => $offset,
- 'page' => $page
- ],
- 'parsed_options' => $options
- ]);
-
+
+ $this->logger->debug(
+ 'Parsed query options for Aanbod',
+ [
+ 'raw_params' => [
+ 'limit' => $limit,
+ 'offset' => $offset,
+ 'page' => $page,
+ ],
+ 'parsed_options' => $options,
+ ]
+ );
+
return $options;
- }
-}
+ }//end parseQueryOptions()
+}//end class
diff --git a/lib/Controller/AangebodenGebruikController.php b/lib/Controller/AangebodenGebruikController.php
index 712967c7..16beb6aa 100644
--- a/lib/Controller/AangebodenGebruikController.php
+++ b/lib/Controller/AangebodenGebruikController.php
@@ -1,23 +1,23 @@
+ *
+ * @category Controller
+ * @package OCA\SoftwareCatalog\Controller
+ * @author Conduction b.v.
* @copyright 2024 Conduction B.V.
- * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- * @version 1.0.0
- * @link https://github.com/ConductionNL/SoftwareCatalog
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
*/
+declare(strict_types=1);
+
namespace OCA\SoftwareCatalog\Controller;
use OCP\AppFramework\Controller;
@@ -28,30 +28,30 @@
use Psr\Log\LoggerInterface;
/**
- * Controller for handling offered usage (aangeboden gebruik) API operations
- *
+ * Controller for handling offered usage (aangeboden gebruik) API operations.
+ *
* This controller provides REST API endpoints for managing gebruiks objects where
* the active organization is involved either as afnemer (consumer) or in deelnemers
* (participants), and for updating the @self property of gebruiks objects.
- *
- * @category Controller
- * @package OCA\SoftwareCatalog\Controller
- * @author Conduction b.v.
+ *
+ * @category Controller
+ * @package OCA\SoftwareCatalog\Controller
+ * @author Conduction b.v.
* @copyright 2024 Conduction B.V.
- * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- * @version 1.0.0
- * @link https://github.com/ConductionNL/SoftwareCatalog
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
*/
class AangebodenGebruikController extends Controller
{
/**
- * Constructor for AangebodenGebruikController
- *
- * @param string $appName The name of the app
- * @param IRequest $request The HTTP request object
- * @param IUserSession $userSession The user session service for getting the current user
+ * Constructor for AangebodenGebruikController.
+ *
+ * @param string $appName The name of the app
+ * @param IRequest $request The HTTP request object
+ * @param IUserSession $userSession The user session service for getting the current user
* @param AangebodenGebruikService $aangebodenGebruikService The business logic service
- * @param LoggerInterface $logger The logger service for debugging and error reporting
+ * @param LoggerInterface $logger The logger service for debugging and error reporting
*/
public function __construct(
string $appName,
@@ -60,14 +60,14 @@ public function __construct(
private readonly AangebodenGebruikService $aangebodenGebruikService,
private readonly LoggerInterface $logger
) {
- parent::__construct($appName, $request);
- }
+ parent::__construct(appName: $appName, request: $request);
+ }//end __construct()
/**
- * Get all gebruiks objects where the active organization is the afnemer (consumer)
- *
+ * Get all gebruiks objects where the active organization is the afnemer (consumer).
+ *
* API Endpoint: GET /api/aangeboden-gebruik/afnemer
- *
+ *
* Query Parameters:
* - limit (int): Maximum number of results to return
* - offset (int): Number of results to skip for pagination
@@ -75,61 +75,76 @@ public function __construct(
* - product (string): Filter by product ID
* - startDate (string): Filter by start date (ISO 8601 format)
* - endDate (string): Filter by end date (ISO 8601 format)
- *
+ *
+ * @return JSONResponse JSON response with gebruiks array where org is afnemer
+ *
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
* @PublicPage
- *
- * @return JSONResponse JSON response with gebruiks array where org is afnemer
*/
public function getGebruiksWhereAfnemer(): JSONResponse
{
- $this->logger->info('API: Getting gebruiks where active org is afnemer', [
- 'endpoint' => '/api/aangeboden-gebruik/afnemer',
- 'method' => 'GET',
- 'query_params' => $this->request->getParams()
- ]);
+ $this->logger->info(
+ 'API: Getting gebruiks where active org is afnemer',
+ [
+ 'endpoint' => '/api/aangeboden-gebruik/afnemer',
+ 'method' => 'GET',
+ 'query_params' => $this->request->getParams(),
+ ]
+ );
try {
- // Parse query parameters for filtering options (force database source)
- // Don't include product filter for "get all" endpoint
+ // Parse query parameters for filtering options (force database source).
+ // Don't include product filter for "get all" endpoint.
$options = $this->parseQueryOptions();
-
- // Get gebruiks from service where org is afnemer
+
+ // Get gebruiks from service where org is afnemer.
$result = $this->aangebodenGebruikService->getGebruiksWhereAfnemer($options);
-
- // Determine HTTP status code based on whether there's an error
- $statusCode = isset($result['error']) ? 500 : 200;
-
- $this->logger->info('API: Afnemer gebruiks request completed', [
- 'total' => $result['total'] ?? 0,
- 'results_count' => count($result['results'] ?? []),
- 'has_error' => isset($result['error'])
- ]);
-
+
+ // Determine HTTP status code based on whether there's an error.
+ if (isset($result['error']) === true) {
+ $statusCode = 500;
+ } else {
+ $statusCode = 200;
+ }
+
+ $this->logger->info(
+ 'API: Afnemer gebruiks request completed',
+ [
+ 'total' => $result['total'] ?? 0,
+ 'results_count' => count($result['results'] ?? []),
+ 'has_error' => isset($result['error']) === true,
+ ]
+ );
+
return new JSONResponse($result, $statusCode);
-
} catch (\Exception $e) {
- $this->logger->error('API: Failed to get afnemer gebruiks', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
-
- return new JSONResponse([
- 'results' => [],
- 'total' => 0,
- 'page' => 1,
- 'pages' => 0,
- 'limit' => 20,
- 'offset' => 0,
- 'error' => 'Internal server error: ' . $e->getMessage()
- ], 500);
- }
- }
+ $this->logger->error(
+ 'API: Failed to get afnemer gebruiks',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'results' => [],
+ 'total' => 0,
+ 'page' => 1,
+ 'pages' => 0,
+ 'limit' => 20,
+ 'offset' => 0,
+ 'error' => 'Internal server error: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getGebruiksWhereAfnemer()
/**
- * Get koppelingen and gebruiks for a specific application/module UUID
+ * Get koppelingen and gebruiks for a specific application/module UUID.
*
* This endpoint returns both koppelingen and gebruiks related to a specific application/module.
* Access rules:
@@ -137,288 +152,376 @@ public function getGebruiksWhereAfnemer(): JSONResponse
* - Users whose organization owns the application/module can see all related usage
* - Supports filtering by organization UUID via query parameter for ambtenaar users
*
+ * @param string $uuid The UUID of the application/module
+ *
+ * @return JSONResponse Koppelingen and gebruiks objects for the specified UUID
+ *
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
- *
- * @param string $uuid The UUID of the application/module
- * @return JSONResponse Koppelingen and gebruiks objects for the specified UUID
*/
public function getKoppelingenGebruikByUuid(string $uuid): JSONResponse
{
- $this->logger->info('API: Getting koppelingen and gebruiks for specific UUID', [
- 'endpoint' => '/api/koppelingen-gebruik/{uuid}',
- 'method' => 'GET',
- 'uuid' => $uuid,
- 'query_params' => $this->request->getParams()
- ]);
+ $this->logger->info(
+ 'API: Getting koppelingen and gebruiks for specific UUID',
+ [
+ 'endpoint' => '/api/koppelingen-gebruik/{uuid}',
+ 'method' => 'GET',
+ 'uuid' => $uuid,
+ 'query_params' => $this->request->getParams(),
+ ]
+ );
try {
- // Check if user is in admin or ambtenaar group
- $isAmbtenaar = $this->isUserInGroup('admin') || $this->isUserInGroup('ambtenaar');
-
- // Get organization filter if provided (only for ambtenaar users)
+ // Check if user is in admin or ambtenaar group.
+ $isAmbtenaar = $this->isUserInGroup(groupName: 'admin') || $this->isUserInGroup(groupName: 'ambtenaar');
+
+ // Get organization filter if provided (only for ambtenaar users).
$organisationFilter = $this->request->getParam('organisation');
-
- // Parse query parameters for filtering options
+
+ // Parse query parameters for filtering options.
$options = $this->parseQueryOptions();
-
- // Add organization filter if provided and user is ambtenaar
- if ($isAmbtenaar && $organisationFilter) {
+
+ // Add organization filter if provided and user is ambtenaar.
+ if ($isAmbtenaar === true && $organisationFilter !== null) {
$options['organisation'] = $organisationFilter;
}
-
- // Get koppelingen and gebruiks for UUID from service
- $result = $this->aangebodenGebruikService->getKoppelingenGebruikByUuid($uuid, $options, $isAmbtenaar);
-
- // Determine HTTP status code based on whether there's an error
- $statusCode = isset($result['error']) ? 500 : 200;
-
- $this->logger->info('API: Koppelingen-gebruik by UUID request completed', [
- 'uuid' => $uuid,
- 'total' => $result['total'] ?? 0,
- 'results_count' => count($result['results'] ?? []),
- 'has_error' => isset($result['error'])
- ]);
-
+
+ // Get koppelingen and gebruiks for UUID from service.
+ $result = $this->aangebodenGebruikService->getKoppelingenGebruikByUuid(
+ uuid: $uuid,
+ options: $options,
+ isAmbtenaar: $isAmbtenaar
+ );
+
+ // Determine HTTP status code based on whether there's an error.
+ if (isset($result['error']) === true) {
+ $statusCode = 500;
+ } else {
+ $statusCode = 200;
+ }
+
+ $this->logger->info(
+ 'API: Koppelingen-gebruik by UUID request completed',
+ [
+ 'uuid' => $uuid,
+ 'total' => $result['total'] ?? 0,
+ 'results_count' => count($result['results'] ?? []),
+ 'has_error' => isset($result['error']) === true,
+ ]
+ );
+
return new JSONResponse($result, $statusCode);
-
} catch (\Exception $e) {
- $this->logger->error('API: Failed to get koppelingen-gebruik by UUID', [
- 'uuid' => $uuid,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
-
- return new JSONResponse([
- 'results' => [],
- 'total' => 0,
- 'page' => 1,
- 'pages' => 0,
- 'limit' => 20,
- 'offset' => 0,
- 'error' => 'Internal server error: ' . $e->getMessage()
- ], 500);
- }
- }
+ $this->logger->error(
+ 'API: Failed to get koppelingen-gebruik by UUID',
+ [
+ 'uuid' => $uuid,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'results' => [],
+ 'total' => 0,
+ 'page' => 1,
+ 'pages' => 0,
+ 'limit' => 20,
+ 'offset' => 0,
+ 'error' => 'Internal server error: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getKoppelingenGebruikByUuid()
/**
- * Get all gebruiks objects (ignoring RBAC and multitenancy) - restricted to ambtenaar group
+ * Get all gebruiks objects (ignoring RBAC and multitenancy) - restricted to ambtenaar group.
*
* This endpoint returns all gebruiks objects regardless of ownership or organization,
* bypassing normal RBAC and multitenancy restrictions. Access is restricted to users
* with the "ambtenaar" group.
*
* @deprecated Use getKoppelingenGebruik() instead
+ *
+ * @return JSONResponse All gebruiks objects in standard searchObjectsPaginated format
+ *
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
- *
- * @return JSONResponse All gebruiks objects in standard searchObjectsPaginated format
*/
public function getAllGebruiksForAmbtenaar(): JSONResponse
{
- $this->logger->info('API: Getting all gebruiks for ambtenaar (ignoring RBAC/multitenancy)', [
- 'endpoint' => '/api/aangeboden-gebruik/ambtenaar',
- 'method' => 'GET',
- 'query_params' => $this->request->getParams()
- ]);
+ $this->logger->info(
+ 'API: Getting all gebruiks for ambtenaar (ignoring RBAC/multitenancy)',
+ [
+ 'endpoint' => '/api/aangeboden-gebruik/ambtenaar',
+ 'method' => 'GET',
+ 'query_params' => $this->request->getParams(),
+ ]
+ );
try {
- // Check if user is in admin or ambtenaar group
- if (!$this->isUserInGroup('admin') && !$this->isUserInGroup('ambtenaar')) {
- // Get user ID for logging (may be null if not authenticated)
+ // Check if user is in admin or ambtenaar group.
+ $isAdmin = $this->isUserInGroup(groupName: 'admin');
+ $isAmbtenaar = $this->isUserInGroup(groupName: 'ambtenaar');
+ if ($isAdmin === false && $isAmbtenaar === false) {
+ // Get user ID for logging (may be null if not authenticated).
$user = $this->userSession->getUser();
- $userId = $user ? $user->getUID() : 'null';
-
- $this->logger->info('API: Returning empty results - user not in admin or ambtenaar group', [
- 'endpoint' => '/api/aangeboden-gebruik/ambtenaar',
- 'user' => $userId
- ]);
-
- // Return empty results with 200 status (not an error)
- return new JSONResponse([
- 'results' => [],
- 'total' => 0,
- 'page' => 1,
- 'pages' => 1,
- 'limit' => 20,
- 'offset' => 0
- ], 200);
- }
+ if ($user !== null) {
+ $userId = $user->getUID();
+ } else {
+ $userId = 'null';
+ }
+
+ $this->logger->info(
+ 'API: Returning empty results - user not in admin or ambtenaar group',
+ [
+ 'endpoint' => '/api/aangeboden-gebruik/ambtenaar',
+ 'user' => $userId,
+ ]
+ );
- // Parse query parameters for filtering options (force database source)
- // Don't include product filter for "get all" endpoint
+ // Return empty results with 200 status (not an error).
+ return new JSONResponse(
+ [
+ 'results' => [],
+ 'total' => 0,
+ 'page' => 1,
+ 'pages' => 1,
+ 'limit' => 20,
+ 'offset' => 0,
+ ],
+ 200
+ );
+ }//end if
+
+ // Parse query parameters for filtering options (force database source).
+ // Don't include product filter for "get all" endpoint.
$options = $this->parseQueryOptions();
-
- // Get all gebruiks from service (ignoring RBAC/multitenancy)
+
+ // Get all gebruiks from service (ignoring RBAC/multitenancy).
$result = $this->aangebodenGebruikService->getAllGebruiksForAmbtenaar($options);
-
- // Determine HTTP status code based on whether there's an error
- $statusCode = isset($result['error']) ? 500 : 200;
-
- $this->logger->info('API: Ambtenaar all gebruiks request completed', [
- 'total' => $result['total'] ?? 0,
- 'results_count' => count($result['results'] ?? []),
- 'has_error' => isset($result['error'])
- ]);
-
+
+ // Determine HTTP status code based on whether there's an error.
+ if (isset($result['error']) === true) {
+ $statusCode = 500;
+ } else {
+ $statusCode = 200;
+ }
+
+ $this->logger->info(
+ 'API: Ambtenaar all gebruiks request completed',
+ [
+ 'total' => $result['total'] ?? 0,
+ 'results_count' => count($result['results'] ?? []),
+ 'has_error' => isset($result['error']) === true,
+ ]
+ );
+
return new JSONResponse($result, $statusCode);
-
} catch (\Exception $e) {
- $this->logger->error('API: Failed to get all gebruiks for ambtenaar', [
- 'error' => $e->getMessage(),
- 'endpoint' => '/api/aangeboden-gebruik/ambtenaar'
- ]);
-
- return new JSONResponse([
- 'results' => [],
- 'total' => 0,
- 'page' => 1,
- 'pages' => 0,
- 'limit' => 20,
- 'offset' => 0,
- 'error' => 'Internal server error: ' . $e->getMessage()
- ], 500);
- }
- }
+ $this->logger->error(
+ 'API: Failed to get all gebruiks for ambtenaar',
+ [
+ 'error' => $e->getMessage(),
+ 'endpoint' => '/api/aangeboden-gebruik/ambtenaar',
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'results' => [],
+ 'total' => 0,
+ 'page' => 1,
+ 'pages' => 0,
+ 'limit' => 20,
+ 'offset' => 0,
+ 'error' => 'Internal server error: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getAllGebruiksForAmbtenaar()
/**
- * Get a single gebruiks object by ID (ignoring RBAC and multitenancy) - restricted to ambtenaar group
+ * Get a single gebruiks object by ID (ignoring RBAC and multitenancy) - restricted to ambtenaar group.
*
- * This endpoint returns a specific gebruiks object by its ID, bypassing normal RBAC
+ * This endpoint returns a specific gebruiks object by its ID, bypassing normal RBAC
* and multitenancy restrictions. Access is restricted to users with the "ambtenaar" group.
*
+ * @param string $gebruikId The ID of the gebruik object to retrieve
+ *
+ * @return JSONResponse Single gebruik object in standard searchObjectsPaginated format
+ *
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
* @PublicPage
- *
- * @param string $gebruikId The ID of the gebruik object to retrieve
- * @return JSONResponse Single gebruik object in standard searchObjectsPaginated format
*/
public function getSingleGebruikForAmbtenaar(string $gebruikId): JSONResponse
{
- $this->logger->info('API: Getting single gebruik for ambtenaar (ignoring RBAC/multitenancy)', [
- 'endpoint' => '/api/aangeboden-gebruik/ambtenaar/{gebruikId}',
- 'method' => 'GET',
- 'gebruik_id' => $gebruikId,
- 'query_params' => $this->request->getParams()
- ]);
+ $this->logger->info(
+ 'API: Getting single gebruik for ambtenaar (ignoring RBAC/multitenancy)',
+ [
+ 'endpoint' => '/api/aangeboden-gebruik/ambtenaar/{gebruikId}',
+ 'method' => 'GET',
+ 'gebruik_id' => $gebruikId,
+ 'query_params' => $this->request->getParams(),
+ ]
+ );
try {
- // Check if user is in admin or ambtenaar group
- if (!$this->isUserInGroup('admin') && !$this->isUserInGroup('ambtenaar')) {
- // Get user ID for logging (may be null if not authenticated)
+ // Check if user is in admin or ambtenaar group.
+ $isAdmin = $this->isUserInGroup(groupName: 'admin');
+ $isAmbtenaar = $this->isUserInGroup(groupName: 'ambtenaar');
+ if ($isAdmin === false && $isAmbtenaar === false) {
+ // Get user ID for logging (may be null if not authenticated).
$user = $this->userSession->getUser();
- $userId = $user ? $user->getUID() : 'null';
-
- $this->logger->info('API: Returning empty results - user not in admin or ambtenaar group', [
- 'endpoint' => '/api/aangeboden-gebruik/ambtenaar/{gebruikId}',
- 'gebruik_id' => $gebruikId,
- 'user' => $userId
- ]);
-
- // Return empty results with 200 status (not an error)
- return new JSONResponse([
- 'results' => [],
- 'total' => 0,
- 'page' => 1,
- 'pages' => 1,
- 'limit' => 20,
- 'offset' => 0
- ], 200);
- }
+ if ($user !== null) {
+ $userId = $user->getUID();
+ } else {
+ $userId = 'null';
+ }
- // Parse query parameters for filtering options (force database source)
- // Don't include product filter - we use uses parameter instead
+ $this->logger->info(
+ 'API: Returning empty results - user not in admin or ambtenaar group',
+ [
+ 'endpoint' => '/api/aangeboden-gebruik/ambtenaar/{gebruikId}',
+ 'gebruik_id' => $gebruikId,
+ 'user' => $userId,
+ ]
+ );
+
+ // Return empty results with 200 status (not an error).
+ return new JSONResponse(
+ [
+ 'results' => [],
+ 'total' => 0,
+ 'page' => 1,
+ 'pages' => 1,
+ 'limit' => 20,
+ 'offset' => 0,
+ ],
+ 200
+ );
+ }//end if
+
+ // Parse query parameters for filtering options (force database source).
+ // Don't include product filter - we use uses parameter instead.
$options = $this->parseQueryOptions();
-
- // Get single gebruik from service (ignoring RBAC/multitenancy)
- $result = $this->aangebodenGebruikService->getSingleGebruikForAmbtenaar($gebruikId, $options);
-
- // Determine HTTP status code based on whether there's an error
- $statusCode = isset($result['error']) ? 500 : 200;
-
- $this->logger->info('API: Ambtenaar single gebruik request completed', [
- 'gebruik_id' => $gebruikId,
- 'total' => $result['total'] ?? 0,
- 'results_count' => count($result['results'] ?? []),
- 'has_error' => isset($result['error'])
- ]);
-
+
+ // Get single gebruik from service (ignoring RBAC/multitenancy).
+ $result = $this->aangebodenGebruikService->getSingleGebruikForAmbtenaar(
+ gebruikId: $gebruikId,
+ options: $options
+ );
+
+ // Determine HTTP status code based on whether there's an error.
+ if (isset($result['error']) === true) {
+ $statusCode = 500;
+ } else {
+ $statusCode = 200;
+ }
+
+ $this->logger->info(
+ 'API: Ambtenaar single gebruik request completed',
+ [
+ 'gebruik_id' => $gebruikId,
+ 'total' => $result['total'] ?? 0,
+ 'results_count' => count($result['results'] ?? []),
+ 'has_error' => isset($result['error']) === true,
+ ]
+ );
+
return new JSONResponse($result, $statusCode);
-
} catch (\Exception $e) {
- $this->logger->error('API: Failed to get single gebruik for ambtenaar', [
- 'gebruik_id' => $gebruikId,
- 'error' => $e->getMessage(),
- 'endpoint' => '/api/aangeboden-gebruik/ambtenaar/{gebruikId}'
- ]);
-
- return new JSONResponse([
- 'results' => [],
- 'total' => 0,
- 'page' => 1,
- 'pages' => 0,
- 'limit' => 20,
- 'offset' => 0,
- 'error' => 'Internal server error: ' . $e->getMessage()
- ], 500);
- }
- }
+ $this->logger->error(
+ 'API: Failed to get single gebruik for ambtenaar',
+ [
+ 'gebruik_id' => $gebruikId,
+ 'error' => $e->getMessage(),
+ 'endpoint' => '/api/aangeboden-gebruik/ambtenaar/{gebruikId}',
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'results' => [],
+ 'total' => 0,
+ 'page' => 1,
+ 'pages' => 0,
+ 'limit' => 20,
+ 'offset' => 0,
+ 'error' => 'Internal server error: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getSingleGebruikForAmbtenaar()
/**
- * Check if the current user is in a specific group
+ * Check if the current user is in a specific group.
*
* @param string $groupName The name of the group to check
+ *
* @return bool True if user is in the group, false otherwise
*/
private function isUserInGroup(string $groupName): bool
{
try {
- // Get the current user from the session
+ // Get the current user from the session.
$user = $this->userSession->getUser();
- if (!$user) {
- $this->logger->debug('No user in session for group check', [
- 'group' => $groupName
- ]);
+ if ($user === null) {
+ $this->logger->debug(
+ 'No user in session for group check',
+ [
+ 'group' => $groupName,
+ ]
+ );
return false;
}
- $userId = $user->getUID();
+ $userId = $user->getUID();
$groupManager = \OC::$server->getGroupManager();
-
+
$group = $groupManager->get($groupName);
- if (!$group) {
+ if ($group === null) {
$this->logger->warning('Group does not exist', ['group' => $groupName]);
return false;
}
$isInGroup = $group->inGroup($user);
-
- $this->logger->debug('User group membership check', [
- 'user' => $userId,
- 'group' => $groupName,
- 'isMember' => $isInGroup
- ]);
+
+ $this->logger->debug(
+ 'User group membership check',
+ [
+ 'user' => $userId,
+ 'group' => $groupName,
+ 'isMember' => $isInGroup,
+ ]
+ );
return $isInGroup;
-
} catch (\Exception $e) {
- $this->logger->error('Failed to check user group membership', [
- 'group' => $groupName,
- 'error' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to check user group membership',
+ [
+ 'group' => $groupName,
+ 'error' => $e->getMessage(),
+ ]
+ );
return false;
- }
- }
+ }//end try
+ }//end isUserInGroup()
/**
- * Get all gebruiks objects where the active organization is in deelnemers (participants)
- *
+ * Get all gebruiks objects where the active organization is in deelnemers (participants).
+ *
* API Endpoint: GET /api/aangeboden-gebruik/deelnemers
- *
+ *
* Query Parameters:
* - limit (int): Maximum number of results to return
* - offset (int): Number of results to skip for pagination
@@ -426,563 +529,516 @@ private function isUserInGroup(string $groupName): bool
* - product (string): Filter by product ID
* - startDate (string): Filter by start date (ISO 8601 format)
* - endDate (string): Filter by end date (ISO 8601 format)
- *
+ *
+ * @return JSONResponse JSON response with gebruiks array where org is in deelnemers
+ *
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
* @PublicPage
- *
- * @return JSONResponse JSON response with gebruiks array where org is in deelnemers
*/
public function getGebruiksWhereDeelnemers(): JSONResponse
{
- $this->logger->info('API: Getting gebruiks where active org is in deelnemers', [
- 'endpoint' => '/api/aangeboden-gebruik/deelnemers',
- 'method' => 'GET',
- 'query_params' => $this->request->getParams()
- ]);
+ $this->logger->info(
+ 'API: Getting gebruiks where active org is in deelnemers',
+ [
+ 'endpoint' => '/api/aangeboden-gebruik/deelnemers',
+ 'method' => 'GET',
+ 'query_params' => $this->request->getParams(),
+ ]
+ );
try {
- // Parse query parameters for filtering options
- // Don't include product filter for deelnemers endpoint
- $options = $this->parseQueryOptions(false);
-
- // Get gebruiks from service where org is in deelnemers
+ // Parse query parameters for filtering options.
+ // Don't include product filter for deelnemers endpoint.
+ $options = $this->parseQueryOptions();
+
+ // Get gebruiks from service where org is in deelnemers.
$result = $this->aangebodenGebruikService->getGebruiksWhereDeelnemers($options);
-
- // Determine appropriate HTTP status code
- $statusCode = $result['success'] ? 200 : 500;
-
- $this->logger->info('API: Deelnemers gebruiks request completed', [
- 'success' => $result['success'],
- 'gebruiks_count' => $result['count'] ?? 0,
- 'organisation' => $result['organisation'] ?? 'unknown'
- ]);
-
+
+ // Determine appropriate HTTP status code.
+ if ($result['success'] === true) {
+ $statusCode = 200;
+ } else {
+ $statusCode = 500;
+ }
+
+ $this->logger->info(
+ 'API: Deelnemers gebruiks request completed',
+ [
+ 'success' => $result['success'],
+ 'gebruiks_count' => $result['count'] ?? 0,
+ 'organisation' => $result['organisation'] ?? 'unknown',
+ ]
+ );
+
return new JSONResponse($result, $statusCode);
-
} catch (\Exception $e) {
- $this->logger->error('API: Failed to get deelnemers gebruiks', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
-
- return new JSONResponse([
- 'success' => false,
- 'error' => 'Internal server error: ' . $e->getMessage(),
- 'gebruiks' => [],
- 'count' => 0
- ], 500);
- }
- }
+ $this->logger->error(
+ 'API: Failed to get deelnemers gebruiks',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'error' => 'Internal server error: '.$e->getMessage(),
+ 'gebruiks' => [],
+ 'count' => 0,
+ ],
+ 500
+ );
+ }//end try
+ }//end getGebruiksWhereDeelnemers()
/**
- * Set the @self property of a gebruik or koppeling to the active organization
- *
+ * Set the @self property of a gebruik or koppeling to the active organization.
+ *
* API Endpoint: PUT /api/aangeboden-gebruik/{gebruikId}/set-self
- *
+ *
* This endpoint allows setting the @self.organisation property of a specific gebruik
* or koppeling object to the active organization, but only if the active organization
* is the afnemer (consumer) or aanbieder (provider) for that object.
- *
+ *
+ * @param string $gebruikId The UUID of the gebruik or koppeling object to update
+ *
+ * @return JSONResponse JSON response with success status and updated object
+ *
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
* @PublicPage
- *
- * @param string $gebruikId The UUID of the gebruik or koppeling object to update
- * @return JSONResponse JSON response with success status and updated object
*/
public function setGebruikSelfToActiveOrg(string $gebruikId): JSONResponse
{
- $this->logger->info('API: Setting gebruik @self property to active org', [
- 'endpoint' => "/api/aangeboden-gebruik/{$gebruikId}/set-self",
- 'method' => 'PUT',
- 'gebruik_id' => $gebruikId
- ]);
+ $this->logger->info(
+ 'API: Setting gebruik @self property to active org',
+ [
+ 'endpoint' => "/api/aangeboden-gebruik/{$gebruikId}/set-self",
+ 'method' => 'PUT',
+ 'gebruik_id' => $gebruikId,
+ ]
+ );
try {
- // Validate input
- if (empty($gebruikId)) {
- return new JSONResponse([
- 'success' => false,
- 'error' => 'Gebruik ID is required',
- 'gebruik' => null
- ], 400);
+ // Validate input.
+ if (empty($gebruikId) === true) {
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'error' => 'Gebruik ID is required',
+ 'gebruik' => null,
+ ],
+ 400
+ );
}
- // Parse any additional options from request body
- $options = [];
+ // Parse any additional options from request body.
+ $options = [];
$requestBody = $this->request->getParams();
- if (!empty($requestBody)) {
- $options = array_filter($requestBody, function($key) {
- return !in_array($key, ['gebruikId']); // Exclude path parameters
- }, ARRAY_FILTER_USE_KEY);
+ if (empty($requestBody) === false) {
+ $options = array_filter(
+ $requestBody,
+ function ($key) {
+ // Exclude path parameters.
+ return in_array(needle: $key, haystack: ['gebruikId']) === false;
+ },
+ ARRAY_FILTER_USE_KEY
+ );
+ }
+
+ // Update gebruik @self property via service.
+ $result = $this->aangebodenGebruikService->setGebruikSelfToActiveOrg(
+ gebruikId: $gebruikId,
+ options: $options
+ );
+
+ // Determine appropriate HTTP status code.
+ if ($result['success'] === true) {
+ $statusCode = 200;
+ } else if ($result['error'] === 'Gebruik object not found') {
+ $statusCode = 404;
+ } else if (strpos(haystack: ($result['error'] ?? ''), needle: 'Operation not allowed') !== false) {
+ $statusCode = 403;
+ } else {
+ $statusCode = 500;
}
-
- // Update gebruik @self property via service
- $result = $this->aangebodenGebruikService->setGebruikSelfToActiveOrg($gebruikId, $options);
-
- // Determine appropriate HTTP status code
- $statusCode = $result['success'] ? 200 : ($result['error'] === 'Gebruik object not found' ? 404 :
- (strpos($result['error'] ?? '', 'Operation not allowed') !== false ? 403 : 500));
-
- $this->logger->info('API: Set gebruik @self property request completed', [
- 'gebruik_id' => $gebruikId,
- 'success' => $result['success'],
- 'status_code' => $statusCode
- ]);
-
+
+ $this->logger->info(
+ 'API: Set gebruik @self property request completed',
+ [
+ 'gebruik_id' => $gebruikId,
+ 'success' => $result['success'],
+ 'status_code' => $statusCode,
+ ]
+ );
+
return new JSONResponse($result, $statusCode);
-
} catch (\Exception $e) {
- $this->logger->error('API: Failed to set gebruik @self property', [
- 'gebruik_id' => $gebruikId,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
-
- return new JSONResponse([
- 'success' => false,
- 'error' => 'Internal server error: ' . $e->getMessage(),
- 'gebruik' => null
- ], 500);
- }
- }
+ $this->logger->error(
+ 'API: Failed to set gebruik @self property',
+ [
+ 'gebruik_id' => $gebruikId,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'error' => 'Internal server error: '.$e->getMessage(),
+ 'gebruik' => null,
+ ],
+ 500
+ );
+ }//end try
+ }//end setGebruikSelfToActiveOrg()
/**
- * Delete (deny) a gebruik or koppeling object as afnemer or aanbieder
- *
+ * Delete (deny) a gebruik or koppeling object as afnemer or aanbieder.
+ *
* API Endpoint: DELETE /api/aangeboden-gebruik/{gebruikId}/deny
- *
+ *
* This endpoint allows deleting a specific gebruik or koppeling object, but only
* if the active organization is the afnemer (consumer) or aanbieder (provider) for
* that object. This implements the "deny" workflow where a gemeente can reject a
* suggestion from a leverancier, or a leverancier can reject/delete their own koppelingen.
- *
+ *
* Security: Implements custom security checks since RBAC is disabled to access
* cross-organisation objects. Only the afnemer or aanbieder can delete the object.
- *
+ *
+ * @param string $gebruikId The UUID of the gebruik or koppeling object to delete
+ *
+ * @return JSONResponse JSON response with success status and deletion details
+ *
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
* @PublicPage
- *
- * @param string $gebruikId The UUID of the gebruik or koppeling object to delete
- * @return JSONResponse JSON response with success status and deletion details
*/
public function deleteGebruikAsAfnemer(string $gebruikId): JSONResponse
{
- $this->logger->info('API: Deleting gebruik object as afnemer', [
- 'endpoint' => "/api/aangeboden-gebruik/{$gebruikId}/deny",
- 'method' => 'DELETE',
- 'gebruik_id' => $gebruikId
- ]);
+ $this->logger->info(
+ 'API: Deleting gebruik object as afnemer',
+ [
+ 'endpoint' => "/api/aangeboden-gebruik/{$gebruikId}/deny",
+ 'method' => 'DELETE',
+ 'gebruik_id' => $gebruikId,
+ ]
+ );
try {
- // Validate input
- if (empty($gebruikId)) {
- return new JSONResponse([
- 'success' => false,
- 'error' => 'Gebruik ID is required',
- 'deleted' => false
- ], 400);
+ // Validate input.
+ if (empty($gebruikId) === true) {
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'error' => 'Gebruik ID is required',
+ 'deleted' => false,
+ ],
+ 400
+ );
}
- // Parse any additional options from request body
- $options = [];
+ // Parse any additional options from request body.
+ $options = [];
$requestBody = $this->request->getParams();
- if (!empty($requestBody)) {
- $options = array_filter($requestBody, function($key) {
- return !in_array($key, ['gebruikId']); // Exclude path parameters
- }, ARRAY_FILTER_USE_KEY);
+ if (empty($requestBody) === false) {
+ $options = array_filter(
+ $requestBody,
+ function ($key) {
+ // Exclude path parameters.
+ return in_array(needle: $key, haystack: ['gebruikId']) === false;
+ },
+ ARRAY_FILTER_USE_KEY
+ );
}
-
- // Delete gebruik object via service
- $result = $this->aangebodenGebruikService->deleteGebruikAsAfnemer($gebruikId, $options);
-
- // Determine appropriate HTTP status code
- $statusCode = $result['success'] ? 200 : ($result['error'] === 'Gebruik object not found' ? 404 :
- (strpos($result['error'] ?? '', 'Operation not allowed') !== false ? 403 : 500));
-
- $this->logger->info('API: Delete gebruik as afnemer request completed', [
- 'gebruik_id' => $gebruikId,
- 'success' => $result['success'],
- 'deleted' => $result['deleted'] ?? false,
- 'status_code' => $statusCode
- ]);
-
+
+ // Delete gebruik object via service.
+ $result = $this->aangebodenGebruikService->deleteGebruikAsAfnemer(
+ gebruikId: $gebruikId,
+ options: $options
+ );
+
+ // Determine appropriate HTTP status code.
+ if ($result['success'] === true) {
+ $statusCode = 200;
+ } else if ($result['error'] === 'Gebruik object not found') {
+ $statusCode = 404;
+ } else if (strpos(haystack: ($result['error'] ?? ''), needle: 'Operation not allowed') !== false) {
+ $statusCode = 403;
+ } else {
+ $statusCode = 500;
+ }
+
+ $this->logger->info(
+ 'API: Delete gebruik as afnemer request completed',
+ [
+ 'gebruik_id' => $gebruikId,
+ 'success' => $result['success'],
+ 'deleted' => $result['deleted'] ?? false,
+ 'status_code' => $statusCode,
+ ]
+ );
+
return new JSONResponse($result, $statusCode);
-
} catch (\Exception $e) {
- $this->logger->error('API: Failed to delete gebruik as afnemer', [
- 'gebruik_id' => $gebruikId,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
-
- return new JSONResponse([
- 'success' => false,
- 'error' => 'Internal server error: ' . $e->getMessage(),
- 'deleted' => false
- ], 500);
- }
- }
+ $this->logger->error(
+ 'API: Failed to delete gebruik as afnemer',
+ [
+ 'gebruik_id' => $gebruikId,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'error' => 'Internal server error: '.$e->getMessage(),
+ 'deleted' => false,
+ ],
+ 500
+ );
+ }//end try
+ }//end deleteGebruikAsAfnemer()
/**
- * Get API documentation for AangebodenGebruik endpoints
- *
+ * Get API documentation for AangebodenGebruik endpoints.
+ *
* API Endpoint: GET /api/aangeboden-gebruik/docs
- *
+ *
+ * @return JSONResponse JSON response with API documentation
+ *
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
* @PublicPage
- *
- * @return JSONResponse JSON response with API documentation
*/
public function getApiDocumentation(): JSONResponse
{
$documentation = [
'api_version' => '2.0.0',
- 'description' => 'SoftwareCatalog AangebodenGebruik API - Manage gebruiks and koppelingen objects with extended access control',
- 'base_url' => '/api/aangeboden-gebruik',
- 'endpoints' => [
+ 'description' => 'SoftwareCatalog AangebodenGebruik API',
+ 'base_url' => '/api/aangeboden-gebruik',
+ 'endpoints' => [
[
- 'method' => 'GET',
- 'path' => '/api/aangeboden-gebruik/afnemer',
+ 'method' => 'GET',
+ 'path' => '/api/aangeboden-gebruik/afnemer',
'description' => 'Get all gebruiks objects where the active organization is the afnemer (consumer)',
- 'parameters' => [
+ 'parameters' => [
[
- 'name' => 'limit',
- 'type' => 'integer',
- 'required' => false,
- 'description' => 'Maximum number of results to return'
+ 'name' => 'limit',
+ 'type' => 'integer',
+ 'required' => false,
+ 'description' => 'Maximum number of results to return',
],
[
- 'name' => 'offset',
- 'type' => 'integer',
- 'required' => false,
- 'description' => 'Number of results to skip for pagination'
+ 'name' => 'offset',
+ 'type' => 'integer',
+ 'required' => false,
+ 'description' => 'Number of results to skip for pagination',
],
[
- 'name' => 'status',
- 'type' => 'string',
- 'required' => false,
- 'description' => 'Filter by usage status'
+ 'name' => 'status',
+ 'type' => 'string',
+ 'required' => false,
+ 'description' => 'Filter by usage status',
],
[
- 'name' => 'product',
- 'type' => 'string',
- 'required' => false,
- 'description' => 'Filter by product ID'
+ 'name' => 'product',
+ 'type' => 'string',
+ 'required' => false,
+ 'description' => 'Filter by product ID',
],
[
- 'name' => 'startDate',
- 'type' => 'string',
- 'required' => false,
- 'description' => 'Filter by start date (ISO 8601 format)'
+ 'name' => 'startDate',
+ 'type' => 'string',
+ 'required' => false,
+ 'description' => 'Filter by start date (ISO 8601 format)',
],
[
- 'name' => 'endDate',
- 'type' => 'string',
- 'required' => false,
- 'description' => 'Filter by end date (ISO 8601 format)'
- ]
- ],
- 'response_example' => [
- 'success' => true,
- 'gebruiks' => [
- [
- 'id' => 'usage-uuid-123',
- 'afnemer' => 'org-uuid',
- 'product' => 'product-uuid',
- 'status' => 'actief',
- '_filter_type' => 'afnemer',
- '_schema_id' => 'schema-id'
- ]
+ 'name' => 'endDate',
+ 'type' => 'string',
+ 'required' => false,
+ 'description' => 'Filter by end date (ISO 8601 format)',
],
- 'count' => 1,
- 'filter_type' => 'afnemer',
- 'organisation' => 'org-uuid'
- ]
+ ],
],
[
- 'method' => 'GET',
- 'path' => '/api/aangeboden-gebruik/deelnemers',
- 'description' => 'Get all gebruiks objects where the active organization is in deelnemers (participants)',
- 'parameters' => [
+ 'method' => 'GET',
+ 'path' => '/api/aangeboden-gebruik/deelnemers',
+ 'description' => 'Get gebruiks with active org in deelnemers',
+ 'parameters' => [
[
- 'name' => 'limit',
- 'type' => 'integer',
- 'required' => false,
- 'description' => 'Maximum number of results to return'
+ 'name' => 'limit',
+ 'type' => 'integer',
+ 'required' => false,
+ 'description' => 'Maximum number of results to return',
],
[
- 'name' => 'offset',
- 'type' => 'integer',
- 'required' => false,
- 'description' => 'Number of results to skip for pagination'
+ 'name' => 'offset',
+ 'type' => 'integer',
+ 'required' => false,
+ 'description' => 'Number of results to skip for pagination',
],
[
- 'name' => 'status',
- 'type' => 'string',
- 'required' => false,
- 'description' => 'Filter by usage status'
+ 'name' => 'status',
+ 'type' => 'string',
+ 'required' => false,
+ 'description' => 'Filter by usage status',
],
- [
- 'name' => 'product',
- 'type' => 'string',
- 'required' => false,
- 'description' => 'Filter by product ID'
- ],
- [
- 'name' => 'startDate',
- 'type' => 'string',
- 'required' => false,
- 'description' => 'Filter by start date (ISO 8601 format)'
- ],
- [
- 'name' => 'endDate',
- 'type' => 'string',
- 'required' => false,
- 'description' => 'Filter by end date (ISO 8601 format)'
- ]
],
- 'response_example' => [
- 'success' => true,
- 'gebruiks' => [
- [
- 'id' => 'usage-uuid-456',
- 'afnemer' => 'other-org-uuid',
- 'deelnemers' => ['org-uuid', 'another-org-uuid'],
- 'product' => 'product-uuid',
- 'status' => 'actief',
- '_filter_type' => 'deelnemers',
- '_schema_id' => 'schema-id'
- ]
- ],
- 'count' => 1,
- 'filter_type' => 'deelnemers',
- 'organisation' => 'org-uuid'
- ]
],
[
- 'method' => 'PUT',
- 'path' => '/api/aangeboden-gebruik/{gebruikId}/set-self',
- 'description' => 'Set the @self property of a gebruik or koppeling to the active organization (only allowed if active org is afnemer or aanbieder)',
- 'parameters' => [
+ 'method' => 'PUT',
+ 'path' => '/api/aangeboden-gebruik/{gebruikId}/set-self',
+ 'description' => 'Set the @self property of a gebruik or koppeling to the active organization',
+ 'parameters' => [
[
- 'name' => 'gebruikId',
- 'type' => 'string',
- 'required' => true,
- 'description' => 'The UUID of the gebruik object to update (in URL path)'
- ]
- ],
- 'response_example' => [
- 'success' => true,
- 'message' => 'Gebruik @self property updated successfully',
- 'gebruik' => [
- 'id' => 'usage-uuid-123',
- 'afnemer' => 'org-uuid',
- '@self' => [
- 'organisation' => 'org-uuid',
- 'register' => 'register-id',
- 'schema' => 'schema-id'
- ]
+ 'name' => 'gebruikId',
+ 'type' => 'string',
+ 'required' => true,
+ 'description' => 'The UUID of the gebruik object to update (in URL path)',
],
- 'updated_fields' => ['@self.organisation']
- ]
+ ],
],
[
- 'method' => 'GET',
- 'path' => '/api/aangeboden-gebruik/docs',
+ 'method' => 'GET',
+ 'path' => '/api/aangeboden-gebruik/docs',
'description' => 'Get this API documentation',
- 'parameters' => [],
- 'response_example' => '(this response)'
+ 'parameters' => [],
],
[
- 'method' => 'GET',
- 'path' => '/api/koppelingen-gebruik',
- 'description' => 'Get all koppelingen and gebruiks objects with extended access control. Access rules: Users with ambtenaar group can see all objects (optionally filtered by organization). Users whose organization owns an application/module can see all related usage.',
- 'parameters' => [
+ 'method' => 'GET',
+ 'path' => '/api/koppelingen-gebruik',
+ 'description' => 'Get all koppelingen and gebruiks objects with extended access control.',
+ 'parameters' => [
[
- 'name' => 'organisation',
- 'type' => 'string',
- 'required' => false,
- 'description' => 'Filter by organization UUID (only for ambtenaar users)'
+ 'name' => 'organisation',
+ 'type' => 'string',
+ 'required' => false,
+ 'description' => 'Filter by organization UUID (only for ambtenaar users)',
],
[
- 'name' => 'limit',
- 'type' => 'integer',
- 'required' => false,
- 'description' => 'Maximum number of results to return'
+ 'name' => 'limit',
+ 'type' => 'integer',
+ 'required' => false,
+ 'description' => 'Maximum number of results to return',
],
[
- 'name' => 'offset',
- 'type' => 'integer',
- 'required' => false,
- 'description' => 'Number of results to skip for pagination'
- ]
- ],
- 'response_example' => [
- 'results' => [
- [
- 'id' => 'uuid-123',
- 'type' => 'gebruik',
- '@self' => [
- 'organisation' => 'org-uuid',
- 'register' => 'register-id',
- 'schema' => 'schema-id'
- ]
- ],
- [
- 'id' => 'uuid-456',
- 'type' => 'koppeling',
- '@self' => [
- 'organisation' => 'org-uuid',
- 'register' => 'register-id',
- 'schema' => 'schema-id'
- ]
- ]
+ 'name' => 'offset',
+ 'type' => 'integer',
+ 'required' => false,
+ 'description' => 'Number of results to skip for pagination',
],
- 'total' => 2,
- 'page' => 1,
- 'pages' => 1,
- 'limit' => 20,
- 'offset' => 0
- ]
+ ],
],
[
- 'method' => 'GET',
- 'path' => '/api/koppelingen-gebruik/{uuid}',
- 'description' => 'Get koppelingen and gebruiks for a specific application/module UUID. Access rules: Users with ambtenaar group can see all related objects. Users whose organization owns the application/module can see all related usage.',
- 'parameters' => [
- [
- 'name' => 'uuid',
- 'type' => 'string',
- 'required' => true,
- 'description' => 'The UUID of the application/module (in URL path)'
- ],
+ 'method' => 'GET',
+ 'path' => '/api/koppelingen-gebruik/{uuid}',
+ 'description' => 'Get koppelingen and gebruiks for a specific application/module UUID.',
+ 'parameters' => [
[
- 'name' => 'organisation',
- 'type' => 'string',
- 'required' => false,
- 'description' => 'Filter by organization UUID (only for ambtenaar users)'
+ 'name' => 'uuid',
+ 'type' => 'string',
+ 'required' => true,
+ 'description' => 'The UUID of the application/module (in URL path)',
],
[
- 'name' => 'limit',
- 'type' => 'integer',
- 'required' => false,
- 'description' => 'Maximum number of results to return'
+ 'name' => 'organisation',
+ 'type' => 'string',
+ 'required' => false,
+ 'description' => 'Filter by organization UUID (only for ambtenaar users)',
],
- [
- 'name' => 'offset',
- 'type' => 'integer',
- 'required' => false,
- 'description' => 'Number of results to skip for pagination'
- ]
],
- 'response_example' => [
- 'results' => [
- [
- 'id' => 'uuid-123',
- 'type' => 'gebruik',
- '@self' => [
- 'organisation' => 'org-uuid',
- 'register' => 'register-id',
- 'schema' => 'schema-id'
- ]
- ]
- ],
- 'total' => 1,
- 'page' => 1,
- 'pages' => 1,
- 'limit' => 20,
- 'offset' => 0
- ]
- ]
+ ],
],
- 'security' => [
- 'afnemer_filtering' => 'Uses standard RBAC filtering based on organization association',
- 'deelnemers_filtering' => 'Uses RBAC-disabled search to find participation records',
- 'self_update_permission' => 'Only allowed if active organization is the afnemer or aanbieder for the specific gebruik or koppeling',
- 'koppelingen_gebruik_access' => 'Extended access: ambtenaar users can see all objects (optionally filtered by organization), organization owners can see usage of their applications/modules'
+ 'security' => [
+ 'afnemer_filtering' => 'Uses standard RBAC filtering based on organization association',
+ 'deelnemers_filtering' => 'Uses RBAC-disabled search to find participation records',
+ 'self_update_permission' => 'Only allowed if active organization is the afnemer or aanbieder',
],
'error_codes' => [
400 => 'Bad Request - Invalid parameters or missing required fields',
- 403 => 'Forbidden - Operation not allowed (e.g., org is not afnemer or aanbieder for @self update or delete)',
+ 403 => 'Forbidden - Operation not allowed',
404 => 'Not Found - Gebruik object not found',
- 500 => 'Internal Server Error - Server-side error occurred'
- ]
+ 500 => 'Internal Server Error - Server-side error occurred',
+ ],
];
return new JSONResponse($documentation, 200);
- }
-
+ }//end getApiDocumentation()
/**
- * Parse query parameters into options array
- *
+ * Parse query parameters into options array.
+ *
* This method extracts and validates query parameters for filtering,
* pagination, and other options. Always forces database source for real-time data.
- *
+ *
* @return array Parsed options array with database source
*/
private function parseQueryOptions(): array
{
$options = [];
-
- // Parse pagination parameters (with and without underscore for compatibility)
+
+ // Parse pagination parameters (with and without underscore for compatibility).
$limit = $this->request->getParam('_limit') ?? $this->request->getParam('limit');
- if ($limit !== null && is_numeric($limit)) {
- $options['_limit'] = (int)$limit;
- $options['limit'] = (int)$limit; // Keep both for compatibility
+ if ($limit !== null && is_numeric($limit) === true) {
+ $options['_limit'] = (int) $limit;
+ // Keep both for compatibility.
+ $options['limit'] = (int) $limit;
}
-
+
$offset = $this->request->getParam('_offset') ?? $this->request->getParam('offset');
- if ($offset !== null && is_numeric($offset)) {
- $options['_offset'] = (int)$offset;
- $options['offset'] = (int)$offset; // Keep both for compatibility
+ if ($offset !== null && is_numeric($offset) === true) {
+ $options['_offset'] = (int) $offset;
+ // Keep both for compatibility.
+ $options['offset'] = (int) $offset;
}
-
+
$page = $this->request->getParam('_page') ?? $this->request->getParam('page');
- if ($page !== null && is_numeric($page)) {
- $options['_page'] = (int)$page;
+ if ($page !== null && is_numeric($page) === true) {
+ $options['_page'] = (int) $page;
}
-
- // Parse filter parameters
+
+ // Parse filter parameters.
$status = $this->request->getParam('status');
- if ($status !== null && !empty(trim($status))) {
+ if ($status !== null && empty(trim($status) === true) === false) {
$options['status'] = trim($status);
}
-
+
$startDate = $this->request->getParam('startDate');
- if ($startDate !== null && !empty(trim($startDate))) {
+ if ($startDate !== null && empty(trim($startDate) === true) === false) {
$options['startDate'] = trim($startDate);
}
-
+
$endDate = $this->request->getParam('endDate');
- if ($endDate !== null && !empty(trim($endDate))) {
+ if ($endDate !== null && empty(trim($endDate) === true) === false) {
$options['endDate'] = trim($endDate);
}
-
- // Force database source for all custom endpoints to ensure real-time data
+
+ // Force database source for all custom endpoints to ensure real-time data.
$options['_source'] = 'database';
-
- $this->logger->debug('Parsed query options for AangebodenGebruik', [
- 'raw_params' => [
- 'limit' => $limit,
- 'offset' => $offset,
- 'status' => $status,
- 'startDate' => $startDate,
- 'endDate' => $endDate
- ],
- 'parsed_options' => $options
- ]);
-
- return $options;
- }
-}
+ $this->logger->debug(
+ 'Parsed query options for AangebodenGebruik',
+ [
+ 'raw_params' => [
+ 'limit' => $limit,
+ 'offset' => $offset,
+ 'status' => $status,
+ 'startDate' => $startDate,
+ 'endDate' => $endDate,
+ ],
+ 'parsed_options' => $options,
+ ]
+ );
+ return $options;
+ }//end parseQueryOptions()
+}//end class
diff --git a/lib/Controller/ContactpersonenController.php b/lib/Controller/ContactpersonenController.php
index b73de062..5f093b2d 100644
--- a/lib/Controller/ContactpersonenController.php
+++ b/lib/Controller/ContactpersonenController.php
@@ -1,5 +1,20 @@
+ * @copyright 2024 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
+ */
+
declare(strict_types=1);
namespace OCA\SoftwareCatalog\Controller;
@@ -18,97 +33,101 @@
use Psr\Log\LoggerInterface;
/**
- * Controller for managing contactpersonen and their user accounts
+ * Controller for managing contactpersonen and their user accounts.
*
* This controller handles operations related to contactpersonen including:
* - Converting contactpersonen to users
* - Managing user passwords
* - Managing user group memberships
*
- * @category Controller
- * @package OCA\SoftwareCatalog\Controller
- * @author Conduction b.v.
- * @license AGPL-3.0-or-later
- * @link https://github.com/ConductionNL/SoftwareCatalog
- * @version 1.0.0
+ * @category Controller
+ * @package OCA\SoftwareCatalog\Controller
+ * @author Conduction b.v.
+ * @copyright 2024 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
*/
class ContactpersonenController extends Controller
{
+
/**
- * Settings service for configuration access
+ * Settings service for configuration access.
*
* @var SettingsService
*/
private SettingsService $settingsService;
/**
- * Contact person handler for user operations
+ * Contact person handler for user operations.
*
* @var ContactPersonHandler
*/
private ContactPersonHandler $contactPersonHandler;
/**
- * User manager for user operations
+ * User manager for user operations.
*
* @var IUserManager
*/
private IUserManager $userManager;
/**
- * Group manager for group operations
+ * Group manager for group operations.
*
* @var IGroupManager
*/
private IGroupManager $groupManager;
/**
- * Secure random generator for passwords
+ * Secure random generator for passwords.
*
* @var ISecureRandom
*/
private ISecureRandom $secureRandom;
/**
- * Logger instance
+ * Logger instance.
*
* @var LoggerInterface
*/
private LoggerInterface $logger;
/**
- * User session for getting current user
+ * User session for getting current user.
*
* @var IUserSession
*/
private IUserSession $userSession;
/**
- * Container for dependency injection
+ * Container for dependency injection.
*
* @var ContainerInterface
*/
private ContainerInterface $container;
/**
- * Contactpersoon service for business logic
+ * Contactpersoon service for business logic.
+ *
+ * @var ContactpersoonService
*/
private ContactpersoonService $contactpersoonService;
/**
- * Constructor
+ * Constructor.
*
- * @param string $appName The app name
- * @param IRequest $request The request object
- * @param SettingsService $settingsService Settings service
- * @param ContactPersonHandler $contactPersonHandler Contact person handler
+ * @param string $appName The app name
+ * @param IRequest $request The request object
+ * @param SettingsService $settingsService Settings service
+ * @param ContactPersonHandler $contactPersonHandler Contact person handler
* @param ContactpersoonService $contactpersoonService Contactpersoon service
- * @param IUserManager $userManager User manager
- * @param IGroupManager $groupManager Group manager
- * @param IUserSession $userSession User session
- * @param ContainerInterface $container Container for DI
- * @param ISecureRandom $secureRandom Secure random generator
- * @param LoggerInterface $logger Logger instance
+ * @param IUserManager $userManager User manager
+ * @param IGroupManager $groupManager Group manager
+ * @param IUserSession $userSession User session
+ * @param ContainerInterface $container Container for DI
+ * @param ISecureRandom $secureRandom Secure random generator
+ * @param LoggerInterface $logger Logger instance
*/
public function __construct(
string $appName,
@@ -123,24 +142,24 @@ public function __construct(
ISecureRandom $secureRandom,
LoggerInterface $logger
) {
- parent::__construct($appName, $request);
- $this->settingsService = $settingsService;
- $this->contactPersonHandler = $contactPersonHandler;
+ parent::__construct(appName: $appName, request: $request);
+ $this->settingsService = $settingsService;
+ $this->contactPersonHandler = $contactPersonHandler;
$this->contactpersoonService = $contactpersoonService;
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
- $this->userSession = $userSession;
- $this->container = $container;
+ $this->userManager = $userManager;
+ $this->groupManager = $groupManager;
+ $this->userSession = $userSession;
+ $this->container = $container;
$this->secureRandom = $secureRandom;
- $this->logger = $logger;
- }
+ $this->logger = $logger;
+ }//end __construct()
/**
- * Get contactpersonen for an organisation with user status
+ * Get contactpersonen for an organisation with user status.
*
- * @param string $organisationId The organisation ID
+ * @param string $organisationId The organisation ID.
*
- * @return JSONResponse List of contactpersonen with user information
+ * @return JSONResponse List of contactpersonen with user information.
*
* @NoAdminRequired
* @NoCSRFRequired
@@ -148,78 +167,90 @@ public function __construct(
public function getContactpersonen(string $organisationId): JSONResponse
{
try {
- // Get object service
+ // Get object service.
$objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
- // Search for contactpersonen belonging to this organisation
- // Use a more generic search that doesn't require specific register/schema
+ // Search for contactpersonen belonging to this organisation.
+ // Use a more generic search that doesn't require specific register/schema.
$searchParams = [
'organisation' => $organisationId,
- '_limit' => 100,
- '_schema' => 'contactpersoon' // Let ObjectService resolve the schema
+ '_limit' => 100,
+ '_schema' => 'contactpersoon',
+ // Let ObjectService resolve the schema.
];
$contactpersonen = $objectService->searchObjectsPaginated($searchParams);
- // Enhance with user information
+ // Enhance with user information.
$enhancedContactpersonen = [];
foreach ($contactpersonen['results'] as $contactpersoon) {
$contactData = $contactpersoon->getObject();
- $username = $contactData['username'] ?? null;
+ $username = $contactData['username'] ?? null;
+ $hasUser = empty($username) === false;
$userInfo = [
- 'hasUser' => !empty($username),
+ 'hasUser' => $hasUser,
'username' => $username,
- 'groups' => [],
- 'disabled' => false
+ 'groups' => [],
+ 'disabled' => false,
];
- if (!empty($username)) {
+ if (empty($username) === false) {
$user = $this->userManager->get($username);
- if ($user) {
- $userGroups = $this->groupManager->getUserGroups($user);
- $userInfo['groups'] = array_map(function($group) {
- return $group->getGID();
- }, $userGroups);
-
- // Get the disabled status from Nextcloud
- $userInfo['disabled'] = !$user->isEnabled();
+ if ($user !== null) {
+ $userGroups = $this->groupManager->getUserGroups($user);
+ $userInfo['groups'] = array_map(
+ function ($group) {
+ return $group->getGID();
+ },
+ $userGroups
+ );
+
+ // Get the disabled status from Nextcloud.
+ $userInfo['disabled'] = ($user->isEnabled() === false);
}
}
$enhancedContactpersonen[] = [
- 'id' => $contactpersoon->getId(),
+ 'id' => $contactpersoon->getId(),
'uuid' => $contactpersoon->getUuid(),
'data' => $contactData,
- 'user' => $userInfo
+ 'user' => $userInfo,
];
- }
-
- return new JSONResponse([
- 'success' => true,
- 'contactpersonen' => $enhancedContactpersonen,
- 'total' => $contactpersonen['total'] ?? count($enhancedContactpersonen)
- ]);
-
+ }//end foreach
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'contactpersonen' => $enhancedContactpersonen,
+ 'total' => $contactpersonen['total'] ?? count($enhancedContactpersonen),
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('Failed to get contactpersonen: ' . $e->getMessage(), [
- 'organisationId' => $organisationId,
- 'exception' => $e
- ]);
-
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to retrieve contactpersonen: ' . $e->getMessage()
- ], 500);
- }
- }
+ $this->logger->error(
+ 'Failed to get contactpersonen: '.$e->getMessage(),
+ [
+ 'organisationId' => $organisationId,
+ 'exception' => $e,
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to retrieve contactpersonen: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getContactpersonen()
/**
- * Convert a contactpersoon to a user account
+ * Convert a contactpersoon to a user account.
*
- * @param string $contactpersoonId The contactpersoon ID
+ * @param string $contactpersoonId The contactpersoon ID.
*
- * @return JSONResponse Result of user creation
+ * @return JSONResponse Result of user creation.
*
* @NoAdminRequired
* @NoCSRFRequired
@@ -227,10 +258,10 @@ public function getContactpersonen(string $organisationId): JSONResponse
public function convertToUser(string $contactpersoonId): JSONResponse
{
try {
- // Get object service
+ // Get object service.
$objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
- // Find the contactpersoon object
+ // Find the contactpersoon object.
$contactpersoonObject = $objectService->find(
id: $contactpersoonId,
register: 'voorzieningen',
@@ -239,162 +270,214 @@ public function convertToUser(string $contactpersoonId): JSONResponse
_multitenancy: false
);
- if (!$contactpersoonObject) {
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Contactpersoon not found'
- ], 404);
+ if ($contactpersoonObject === null) {
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Contactpersoon not found',
+ ],
+ 404
+ );
}
- // Get register and schema from the found object
+ // Get register and schema from the found object.
$registerId = $contactpersoonObject->getRegister();
- $schemaId = $contactpersoonObject->getSchema();
+ $schemaId = $contactpersoonObject->getSchema();
- $this->logger->info('ContactpersonenController: Found contactpersoon object', [
- 'contactpersoonId' => $contactpersoonId,
- 'registerId' => $registerId,
- 'schemaId' => $schemaId
- ]);
+ $this->logger->info(
+ 'ContactpersonenController: Found contactpersoon object',
+ [
+ 'contactpersoonId' => $contactpersoonId,
+ 'registerId' => $registerId,
+ 'schemaId' => $schemaId,
+ ]
+ );
$contactData = $contactpersoonObject->getObject();
- // Check if user already exists
- if (!empty($contactData['username'])) {
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Contactpersoon already has a user account'
- ], 400);
+ // Check if user already exists.
+ if (empty($contactData['username']) === false) {
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Contactpersoon already has a user account',
+ ],
+ 400
+ );
}
- // Validate email address before attempting user creation
- $email = $contactData['email'] ?? $contactData['e-mailadres'] ?? '';
+ // Validate email address before attempting user creation.
+ $email = $contactData['email'] ?? $contactData['e-mailadres'] ?? '';
$emailError = $this->contactPersonHandler->validateEmailForUsername($email);
if ($emailError !== null) {
- return new JSONResponse([
- 'success' => false,
- 'message' => $emailError
- ], 400);
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => $emailError,
+ ],
+ 400
+ );
}
- // Create user account using ContactPersonHandler
+ // Create user account using ContactPersonHandler.
$user = $this->contactPersonHandler->createUserAccount($contactpersoonObject);
- if (!$user) {
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to create user account'
- ], 500);
+ if ($user === null) {
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to create user account',
+ ],
+ 500
+ );
}
- // Ensure groups are assigned based on organization type
- // This is a safety check in case the createUserAccount didn't assign groups properly
- $contactData = $contactpersoonObject->getObject();
+ // Ensure groups are assigned based on organization type.
+ // This is a safety check in case the createUserAccount didn't assign groups properly.
+ $contactData = $contactpersoonObject->getObject();
$organizationId = $contactData['organisatie'] ?? $contactData['organisation'] ?? '';
- if (!empty($organizationId)) {
- $this->logger->info('ContactpersonenController: Ensuring groups are assigned based on organization type', [
- 'contactpersoonId' => $contactpersoonId,
- 'username' => $user->getUID(),
- 'organizationId' => $organizationId
- ]);
-
- // Call the ContactPersonHandler to update groups based on contact data
- $this->contactPersonHandler->updateUserGroupsFromContactData($user, $contactData);
+ if (empty($organizationId) === false) {
+ $this->logger->info(
+ 'ContactpersonenController: Ensuring groups are assigned based on organization type',
+ [
+ 'contactpersoonId' => $contactpersoonId,
+ 'username' => $user->getUID(),
+ 'organizationId' => $organizationId,
+ ]
+ );
+
+ // Call the ContactPersonHandler to update groups based on contact data.
+ $this->contactPersonHandler->updateUserGroupsFromContactData(
+ user: $user,
+ objectData: $contactData
+ );
}
- // Link user to organization entity
- $this->contactPersonHandler->addUserToOrganizationEntity($contactpersoonObject, $user->getUID(), $organizationId);
+ // Link user to organization entity.
+ $this->contactPersonHandler->addUserToOrganizationEntity(
+ contactpersoonObject: $contactpersoonObject,
+ username: $user->getUID(),
+ organizationId: $organizationId
+ );
- // Update the contactpersoon object with the username
+ // Update the contactpersoon object with the username.
$contactData['username'] = $user->getUID();
- // Ensure string fields are properly typed (fixes data stored with incorrect types)
+ // Ensure string fields are properly typed (fixes data stored with incorrect types).
$stringFields = ['voornaam', 'tussenvoegsel', 'achternaam', 'functie', 'telefoonnummer', 'email', 'e-mailadres'];
foreach ($stringFields as $field) {
- if (isset($contactData[$field]) && !is_string($contactData[$field])) {
- $contactData[$field] = (string)$contactData[$field];
+ if (isset($contactData[$field]) === true && is_string($contactData[$field]) === false) {
+ $contactData[$field] = (string) $contactData[$field];
}
}
- // Handle organisatie field - if it's a string UUID, convert to null to avoid validation errors
- // The relationship is maintained through the organisation entity's users array
- if (isset($contactData['organisatie']) && is_string($contactData['organisatie'])) {
- $this->logger->info('ContactpersonenController: Converting organisatie string to null for validation', [
- 'originalValue' => $contactData['organisatie']
- ]);
+ // Handle organisatie field — if it's a string UUID, convert to null to avoid validation errors.
+ // The relationship is maintained through the organisation entity's users array.
+ if (isset($contactData['organisatie']) === true && is_string($contactData['organisatie']) === true) {
+ $this->logger->info(
+ 'ContactpersonenController: Converting organisatie string to null for validation',
+ [
+ 'originalValue' => $contactData['organisatie'],
+ ]
+ );
$contactData['organisatie'] = null;
}
- if (isset($contactData['organisation']) && is_string($contactData['organisation'])) {
+
+ if (isset($contactData['organisation']) === true && is_string($contactData['organisation']) === true) {
$contactData['organisation'] = null;
}
$contactpersoonObject->setObject($contactData);
- // Debug logging to understand data types before save
- $this->logger->info('ContactpersonenController: About to save contactpersoon object', [
- 'contactpersoonId' => $contactpersoonId,
- 'achternaamValue' => $contactData['achternaam'] ?? 'not set',
- 'achternaamType' => isset($contactData['achternaam']) ? gettype($contactData['achternaam']) : 'not set',
- 'registerId' => $registerId,
- 'schemaId' => $schemaId
- ]);
-
- // Save using ObjectEntityMapper directly to bypass schema validation
- // This avoids "Unresolved reference" errors when schema references can't be resolved
+ // Debug logging to understand data types before save.
+ $achternaamValue = $contactData['achternaam'] ?? 'not set';
+ if (isset($contactData['achternaam']) === true) {
+ $achternaamType = gettype($contactData['achternaam']);
+ } else {
+ $achternaamType = 'not set';
+ }
+
+ $this->logger->info(
+ 'ContactpersonenController: About to save contactpersoon object',
+ [
+ 'contactpersoonId' => $contactpersoonId,
+ 'achternaamValue' => $achternaamValue,
+ 'achternaamType' => $achternaamType,
+ 'registerId' => $registerId,
+ 'schemaId' => $schemaId,
+ ]
+ );
+
+ // Save using ObjectEntityMapper directly to bypass schema validation.
+ // This avoids "Unresolved reference" errors when schema references can't be resolved.
$objectMapper = $this->container->get('OCA\OpenRegister\Db\ObjectEntityMapper');
$objectMapper->update($contactpersoonObject);
- $this->logger->info('ContactpersonenController: Updated contactpersoon with username', [
- 'contactpersoonId' => $contactpersoonId,
- 'username' => $user->getUID()
- ]);
+ $this->logger->info(
+ 'ContactpersonenController: Updated contactpersoon with username',
+ [
+ 'contactpersoonId' => $contactpersoonId,
+ 'username' => $user->getUID(),
+ ]
+ );
- // Get user groups to include in response
- $userGroups = $this->groupManager->getUserGroups($user);
+ // Get user groups to include in response.
+ $userGroups = $this->groupManager->getUserGroups($user);
$softwareCatalogGroups = ['gebruik-beheerder', 'aanbod-beheerder', 'gebruik-raadpleger'];
- $userGroupNames = [];
+ $userGroupNames = [];
foreach ($userGroups as $group) {
$groupId = $group->getGID();
- if (in_array($groupId, $softwareCatalogGroups)) {
+ if (in_array(needle: $groupId, haystack: $softwareCatalogGroups) === true) {
$userGroupNames[] = $groupId;
}
}
- // Add groups to the contactpersoon data for frontend
- $updatedContactData = $contactpersoonObject->getObject();
+ // Add groups to the contactpersoon data for frontend.
+ $updatedContactData = $contactpersoonObject->getObject();
$updatedContactData['groups'] = $userGroupNames;
- // Return the updated contactpersoon object with groups
- return new JSONResponse([
- 'success' => true,
- 'message' => 'User account created successfully',
- 'username' => $user->getUID(),
- 'contactpersoon' => array_merge($contactpersoonObject->jsonSerialize(), [
- 'groups' => $userGroupNames
- ])
- ]);
-
+ // Return the updated contactpersoon object with groups.
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'message' => 'User account created successfully',
+ 'username' => $user->getUID(),
+ 'contactpersoon' => array_merge(
+ $contactpersoonObject->jsonSerialize(),
+ [
+ 'groups' => $userGroupNames,
+ ]
+ ),
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('Failed to convert contactpersoon to user: ' . $e->getMessage(), [
- 'contactpersoonId' => $contactpersoonId,
- 'exception' => $e
- ]);
-
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to create user account: ' . $e->getMessage()
- ], 500);
- }
- }
+ $this->logger->error(
+ 'Failed to convert contactpersoon to user: '.$e->getMessage(),
+ [
+ 'contactpersoonId' => $contactpersoonId,
+ 'exception' => $e,
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to create user account: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end convertToUser()
/**
- * Change user password
+ * Change user password.
*
- * @param string $username The username
- * @param string $newPassword The new password
+ * @param string $username The username.
+ * @param string $newPassword The new password.
*
- * @return JSONResponse Result of password change
+ * @return JSONResponse Result of password change.
*
* @NoAdminRequired
* @NoCSRFRequired
@@ -404,160 +487,204 @@ public function changePassword(string $username, string $newPassword): JSONRespo
try {
$user = $this->userManager->get($username);
- if (!$user) {
- return new JSONResponse([
- 'success' => false,
- 'message' => 'User not found'
- ], 404);
+ if ($user === null) {
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'User not found',
+ ],
+ 404
+ );
}
- // Validate password (basic validation)
+ // Validate password (basic validation).
if (strlen($newPassword) < 10) {
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Password must be at least 10 characters long'
- ], 400);
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Password must be at least 10 characters long',
+ ],
+ 400
+ );
}
- // Set new password — setPassword() returns false if the password
- // is rejected (e.g., compromised password list, policy violation).
+ // Set new password — setPassword() returns false if the password.
+ // Is rejected (e.g., compromised password list, policy violation).
$result = $user->setPassword($newPassword);
if ($result === false) {
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Password was rejected. It may be too common or violate the password policy. Please choose a different password.'
- ], 400);
+ // Password rejected — too common or violates the configured policy.
+ $msg = 'Password was rejected: may be too common or violate the policy. Please choose another.';
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => $msg,
+ ],
+ 400
+ );
}
- $this->logger->info('Password changed for user', [
- 'username' => $username
- ]);
-
- return new JSONResponse([
- 'success' => true,
- 'message' => 'Password changed successfully'
- ]);
-
+ $this->logger->info(
+ 'Password changed for user',
+ [
+ 'username' => $username,
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'message' => 'Password changed successfully',
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('Failed to change password: ' . $e->getMessage(), [
- 'username' => $username,
- 'exception' => $e
- ]);
-
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to change password: ' . $e->getMessage()
- ], 500);
- }
- }
+ $this->logger->error(
+ 'Failed to change password: '.$e->getMessage(),
+ [
+ 'username' => $username,
+ 'exception' => $e,
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to change password: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end changePassword()
/**
- * Update user groups
+ * Update user groups.
*
- * @param string $username The username
- * @param array $groups Array of group names to assign
+ * @param string $username The username.
+ * @param array $groups Array of group names to assign.
*
- * @return JSONResponse Result of group update
+ * @return JSONResponse Result of group update.
*
* @NoAdminRequired
* @NoCSRFRequired
*/
- public function updateUserGroups(string $username, array $groups = []): JSONResponse
+ public function updateUserGroups(string $username, array $groups=[]): JSONResponse
{
try {
$user = $this->userManager->get($username);
- if (!$user) {
- return new JSONResponse([
- 'success' => false,
- 'message' => 'User not found'
- ], 404);
+ if ($user === null) {
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'User not found',
+ ],
+ 404
+ );
}
- // Get allowed software catalog groups
+ // Get allowed software catalog groups.
$allowedGroups = ['gebruik-beheerder', 'aanbod-beheerder', 'gebruik-raadpleger'];
- // Filter to only allowed groups
+ // Filter to only allowed groups.
$validGroups = array_intersect($groups, $allowedGroups);
- // Get current user groups (only software catalog groups)
+ // Get current user groups (only software catalog groups).
$currentGroups = $this->groupManager->getUserGroups($user);
$currentSoftwareCatalogGroups = [];
foreach ($currentGroups as $group) {
- if (in_array($group->getGID(), $allowedGroups)) {
+ if (in_array(needle: $group->getGID() === true, haystack: $allowedGroups) === true) {
$currentSoftwareCatalogGroups[] = $group->getGID();
}
}
- // Remove user from groups they should no longer be in
+ // Remove user from groups they should no longer be in.
$groupsToRemove = array_diff($currentSoftwareCatalogGroups, $validGroups);
foreach ($groupsToRemove as $groupName) {
$group = $this->groupManager->get($groupName);
- if ($group && $group->inGroup($user)) {
+ if ($group !== null && $group->inGroup($user) === true) {
$group->removeUser($user);
- $this->logger->info('Removed user from group', [
- 'username' => $username,
- 'group' => $groupName
- ]);
+ $this->logger->info(
+ 'Removed user from group',
+ [
+ 'username' => $username,
+ 'group' => $groupName,
+ ]
+ );
}
}
- // Add user to new groups (only if they exist)
+ // Add user to new groups (only if they exist).
$groupsToAdd = array_diff($validGroups, $currentSoftwareCatalogGroups);
foreach ($groupsToAdd as $groupName) {
$group = $this->groupManager->get($groupName);
- if ($group) {
- if (!$group->inGroup($user)) {
+ if ($group !== null) {
+ if ($group->inGroup($user) === false) {
$group->addUser($user);
- $this->logger->info('Added user to group', [
- 'username' => $username,
- 'group' => $groupName
- ]);
+ $this->logger->info(
+ 'Added user to group',
+ [
+ 'username' => $username,
+ 'group' => $groupName,
+ ]
+ );
}
} else {
- $this->logger->warning('Group does not exist, skipping', [
- 'username' => $username,
- 'group' => $groupName
- ]);
+ $this->logger->warning(
+ 'Group does not exist, skipping',
+ [
+ 'username' => $username,
+ 'group' => $groupName,
+ ]
+ );
}
- }
-
- // Get updated groups
- $updatedGroups = $this->groupManager->getUserGroups($user);
- $updatedGroupNames = array_map(function($group) {
- return $group->getGID();
- }, $updatedGroups);
-
- return new JSONResponse([
- 'success' => true,
- 'message' => 'User groups updated successfully',
- 'groups' => $updatedGroupNames
- ]);
-
+ }//end foreach
+
+ // Get updated groups.
+ $updatedGroups = $this->groupManager->getUserGroups($user);
+ $updatedGroupNames = array_map(
+ function ($group) {
+ return $group->getGID();
+ },
+ $updatedGroups
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'message' => 'User groups updated successfully',
+ 'groups' => $updatedGroupNames,
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('Failed to update user groups: ' . $e->getMessage(), [
- 'username' => $username,
- 'groups' => $groups,
- 'exception' => $e
- ]);
-
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to update user groups: ' . $e->getMessage()
- ], 500);
- }
- }
+ $this->logger->error(
+ 'Failed to update user groups: '.$e->getMessage(),
+ [
+ 'username' => $username,
+ 'groups' => $groups,
+ 'exception' => $e,
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to update user groups: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end updateUserGroups()
/**
- * Get contact persons for an organization with user details
+ * Get contact persons for an organization with user details.
*
* Returns all contact persons linked to a specific organization,
* with their corresponding Nextcloud user details spliced in.
*
- * @param string $organizationUuid The organization UUID
- * @return JSONResponse JSON response containing contact persons with user details
+ * @param string $organizationUuid The organization UUID.
+ *
+ * @return JSONResponse JSON response containing contact persons with user details.
*
* @NoAdminRequired
* @NoCSRFRequired
@@ -565,69 +692,88 @@ public function updateUserGroups(string $username, array $groups = []): JSONResp
public function getContactPersonsWithUserDetailsForOrganization(string $organizationUuid): JSONResponse
{
try {
- $this->logger->info('ContactpersonenController: Getting contact persons with user details for organization', [
- 'organizationUuid' => $organizationUuid
- ]);
-
- // Validate organization UUID
- if (empty($organizationUuid)) {
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Organization UUID is required'
- ], 400);
+ $this->logger->info(
+ 'ContactpersonenController: Getting contact persons with user details for organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
+
+ // Validate organization UUID.
+ if (empty($organizationUuid) === true) {
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Organization UUID is required',
+ ],
+ 400
+ );
}
- // Get contact persons with user details using the service
- $contactPersons = $this->contactpersoonService->getContactPersonsWithUserDetailsForOrganization($organizationUuid);
+ // Get contact persons with user details using the service.
+ $contactPersons = $this->contactpersoonService->getContactPersonsWithUserDetailsForOrganization(
+ $organizationUuid
+ );
- // Convert objects to arrays for JSON response
+ // Convert objects to arrays for JSON response.
$contactPersonsData = [];
foreach ($contactPersons as $contactPerson) {
$contactPersonsData[] = [
- 'id' => $contactPerson->getId(),
- 'uuid' => $contactPerson->getUuid(),
- 'object' => $contactPerson->getObject(),
+ 'id' => $contactPerson->getId(),
+ 'uuid' => $contactPerson->getUuid(),
+ 'object' => $contactPerson->getObject(),
'register' => $contactPerson->getRegister(),
- 'schema' => $contactPerson->getSchema(),
- 'created' => $contactPerson->getCreated(),
- 'modified' => $contactPerson->getModified()
+ 'schema' => $contactPerson->getSchema(),
+ 'created' => $contactPerson->getCreated(),
+ 'modified' => $contactPerson->getModified(),
];
}
- $this->logger->info('ContactpersonenController: Successfully retrieved contact persons with user details', [
- 'organizationUuid' => $organizationUuid,
- 'contactPersonCount' => count($contactPersonsData)
- ]);
-
- return new JSONResponse([
- 'success' => true,
- 'data' => $contactPersonsData,
- 'count' => count($contactPersonsData),
- 'organizationUuid' => $organizationUuid
- ]);
-
+ $this->logger->info(
+ 'ContactpersonenController: Successfully retrieved contact persons with user details',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'contactPersonCount' => count($contactPersonsData),
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'data' => $contactPersonsData,
+ 'count' => count($contactPersonsData),
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('ContactpersonenController: Failed to get contact persons with user details for organization', [
- 'organizationUuid' => $organizationUuid,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
-
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get contact persons with user details: ' . $e->getMessage()
- ], 500);
- }
- }
+ $this->logger->error(
+ 'ContactpersonenController: Failed to get contact persons with user details for organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get contact persons with user details: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getContactPersonsWithUserDetailsForOrganization()
/**
- * Get user information and available groups for a specific contactpersoon
+ * Get user information and available groups for a specific contactpersoon.
*
* Returns user information including current groups and available groups
* for a specific contactpersoon identified by UUID.
*
- * @param string $contactpersoonId The contactpersoon UUID
- * @return JSONResponse JSON response containing user info and available groups
+ * @param string $contactpersoonId The contactpersoon UUID.
+ *
+ * @return JSONResponse JSON response containing user info and available groups.
*
* @NoAdminRequired
* @NoCSRFRequired
@@ -635,14 +781,17 @@ public function getContactPersonsWithUserDetailsForOrganization(string $organiza
public function getUserInfo(string $contactpersoonId): JSONResponse
{
try {
- $this->logger->info('ContactpersonenController: Getting user info for contactpersoon', [
- 'contactpersoonId' => $contactpersoonId
- ]);
-
- // Get contactpersoon from OpenRegister
+ $this->logger->info(
+ 'ContactpersonenController: Getting user info for contactpersoon',
+ [
+ 'contactpersoonId' => $contactpersoonId,
+ ]
+ );
+
+ // Get contactpersoon from OpenRegister.
$objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
- // First try to find the object by UUID
+ // First try to find the object by UUID.
$contactObject = $objectService->find(
id: $contactpersoonId,
register: 'voorzieningen',
@@ -651,95 +800,106 @@ public function getUserInfo(string $contactpersoonId): JSONResponse
_multitenancy: false
);
- if (!$contactObject) {
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Contactpersoon not found'
- ], 404);
+ if ($contactObject === null) {
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Contactpersoon not found',
+ ],
+ 404
+ );
}
$contactData = $contactObject->getObject();
- $username = $contactData['username'] ?? null;
+ $username = $contactData['username'] ?? null;
+ $hasUser = empty($username) === false;
$userInfo = [
- 'hasUser' => !empty($username),
+ 'hasUser' => $hasUser,
'username' => $username,
- 'groups' => [],
- 'disabled' => false
+ 'groups' => [],
+ 'disabled' => false,
];
- // If user exists, get their current groups and disabled status
- if (!empty($username)) {
+ // If user exists, get their current groups and disabled status.
+ if (empty($username) === false) {
$user = $this->userManager->get($username);
- if ($user) {
- $userGroups = $this->groupManager->getUserGroups($user);
+ if ($user !== null) {
+ $userGroups = $this->groupManager->getUserGroups($user);
$softwareCatalogGroups = ['gebruik-beheerder', 'aanbod-beheerder', 'gebruik-raadpleger'];
foreach ($userGroups as $group) {
$groupId = $group->getGID();
- if (in_array($groupId, $softwareCatalogGroups)) {
+ if (in_array(needle: $groupId, haystack: $softwareCatalogGroups) === true) {
$userInfo['groups'][] = $groupId;
}
}
- // Get the disabled status from Nextcloud
- $userInfo['disabled'] = !$user->isEnabled();
+ // Get the disabled status from Nextcloud.
+ $userInfo['disabled'] = ($user->isEnabled() === false);
}
}
- // Available groups (same as getAvailableGroups but inline for consistency)
+ // Available groups (same as getAvailableGroups but inline for consistency).
$availableGroups = [
[
- 'id' => 'gebruik-beheerder',
- 'name' => 'Gebruik Beheerder',
- 'description' => 'Manages software usage and procurement'
+ 'id' => 'gebruik-beheerder',
+ 'name' => 'Gebruik Beheerder',
+ 'description' => 'Manages software usage and procurement',
],
[
- 'id' => 'aanbod-beheerder',
- 'name' => 'Aanbod Beheerder',
- 'description' => 'Manages software offerings and catalog content'
+ 'id' => 'aanbod-beheerder',
+ 'name' => 'Aanbod Beheerder',
+ 'description' => 'Manages software offerings and catalog content',
],
[
- 'id' => 'gebruik-raadpleger',
- 'name' => 'Gebruik Raadpleger',
- 'description' => 'Views software usage and procurement data'
- ]
+ 'id' => 'gebruik-raadpleger',
+ 'name' => 'Gebruik Raadpleger',
+ 'description' => 'Views software usage and procurement data',
+ ],
];
- // Check which groups actually exist
+ // Check which groups actually exist.
$existingGroups = [];
foreach ($availableGroups as $groupInfo) {
$group = $this->groupManager->get($groupInfo['id']);
- if ($group) {
+ if ($group !== null) {
$existingGroups[] = $groupInfo;
}
}
- return new JSONResponse([
- 'success' => true,
- 'userInfo' => $userInfo,
- 'availableGroups' => $existingGroups
- ]);
-
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'userInfo' => $userInfo,
+ 'availableGroups' => $existingGroups,
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('ContactpersonenController: Failed to get user info', [
- 'contactpersoonId' => $contactpersoonId,
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine()
- ]);
-
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get user info: ' . $e->getMessage()
- ], 500);
- }
- }
+ $this->logger->error(
+ 'ContactpersonenController: Failed to get user info',
+ [
+ 'contactpersoonId' => $contactpersoonId,
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get user info: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getUserInfo()
/**
- * Get available software catalog groups
+ * Get available software catalog groups.
*
- * @return JSONResponse List of available groups
+ * @return JSONResponse List of available groups.
*
* @NoAdminRequired
* @NoCSRFRequired
@@ -749,179 +909,282 @@ public function getAvailableGroups(): JSONResponse
try {
$availableGroups = [
[
- 'id' => 'gebruik-beheerder',
- 'name' => 'Gebruik Beheerder',
- 'description' => 'Manages software usage and procurement'
+ 'id' => 'gebruik-beheerder',
+ 'name' => 'Gebruik Beheerder',
+ 'description' => 'Manages software usage and procurement',
],
[
- 'id' => 'aanbod-beheerder',
- 'name' => 'Aanbod Beheerder',
- 'description' => 'Manages software offerings and catalog content'
+ 'id' => 'aanbod-beheerder',
+ 'name' => 'Aanbod Beheerder',
+ 'description' => 'Manages software offerings and catalog content',
],
[
- 'id' => 'gebruik-raadpleger',
- 'name' => 'Gebruik Raadpleger',
- 'description' => 'Views software usage and procurement data'
- ]
+ 'id' => 'gebruik-raadpleger',
+ 'name' => 'Gebruik Raadpleger',
+ 'description' => 'Views software usage and procurement data',
+ ],
];
- // Check which groups actually exist
+ // Check which groups actually exist.
$existingGroups = [];
foreach ($availableGroups as $groupInfo) {
$group = $this->groupManager->get($groupInfo['id']);
- if ($group) {
+ if ($group !== null) {
$existingGroups[] = $groupInfo;
}
}
- return new JSONResponse([
- 'success' => true,
- 'groups' => $existingGroups
- ]);
-
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'groups' => $existingGroups,
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('Failed to get available groups: ' . $e->getMessage(), [
- 'exception' => $e
- ]);
-
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to retrieve available groups: ' . $e->getMessage()
- ], 500);
- }
- }
+ $this->logger->error(
+ 'Failed to get available groups: '.$e->getMessage(),
+ [
+ 'exception' => $e,
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to retrieve available groups: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getAvailableGroups()
/**
- * Disable a user account
+ * Disable a user account.
+ *
+ * @param string $contactpersoonId The contactpersoon ID.
+ *
+ * @return JSONResponse Result of the disable operation.
+ *
* @NoAdminRequired
* @NoCSRFRequired
- * @param string $contactpersoonId
- * @return JSONResponse
*/
- public function disableUser(string $contactpersoonId): JSONResponse {
+ public function disableUser(string $contactpersoonId): JSONResponse
+ {
try {
- // Delegate to service
+ // Delegate to service.
$this->contactpersoonService->disableUserForContactpersoon($contactpersoonId);
- $this->logger->info('User account disabled', [
- 'contactpersoonId' => $contactpersoonId,
- 'disabled_by' => $this->userId
- ]);
- return new JSONResponse(['success' => true, 'message' => 'User account disabled successfully']);
-
+ $this->logger->info(
+ 'User account disabled',
+ [
+ 'contactpersoonId' => $contactpersoonId,
+ 'disabled_by' => $this->userId,
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'message' => 'User account disabled successfully',
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('Failed to disable user account', [
- 'contactpersoonId' => $contactpersoonId,
- 'error' => $e->getMessage()
- ]);
- return new JSONResponse(['success' => false, 'message' => 'Failed to disable user account: ' . $e->getMessage()], 500);
- }
- }
+ $this->logger->error(
+ 'Failed to disable user account',
+ [
+ 'contactpersoonId' => $contactpersoonId,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to disable user account: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end disableUser()
/**
- * Enable a user account
+ * Enable a user account.
+ *
+ * @param string $contactpersoonId The contactpersoon ID.
+ *
+ * @return JSONResponse Result of the enable operation.
+ *
* @NoAdminRequired
* @NoCSRFRequired
- * @param string $contactpersoonId
- * @return JSONResponse
*/
- public function enableUser(string $contactpersoonId): JSONResponse {
+ public function enableUser(string $contactpersoonId): JSONResponse
+ {
try {
- // Delegate to service
+ // Delegate to service.
$this->contactpersoonService->enableUserForContactpersoon($contactpersoonId);
- $this->logger->info('User account enabled', [
- 'contactpersoonId' => $contactpersoonId,
- 'enabled_by' => $this->userId
- ]);
- return new JSONResponse(['success' => true, 'message' => 'User account enabled successfully']);
-
+ $this->logger->info(
+ 'User account enabled',
+ [
+ 'contactpersoonId' => $contactpersoonId,
+ 'enabled_by' => $this->userId,
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'message' => 'User account enabled successfully',
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('Failed to enable user account', [
- 'contactpersoonId' => $contactpersoonId,
- 'error' => $e->getMessage()
- ]);
- return new JSONResponse(['success' => false, 'message' => 'Failed to enable user account: ' . $e->getMessage()], 500);
- }
- }
+ $this->logger->error(
+ 'Failed to enable user account',
+ [
+ 'contactpersoonId' => $contactpersoonId,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to enable user account: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end enableUser()
/**
- * Test endpoint to debug bulk user info
+ * Test endpoint to debug bulk user info.
+ *
+ * @return JSONResponse Debug information about available services.
+ *
* @NoAdminRequired
* @NoCSRFRequired
- * @return JSONResponse
*/
- public function testBulkUserInfo(): JSONResponse {
+ public function testBulkUserInfo(): JSONResponse
+ {
try {
- $this->logger->info('testBulkUserInfo called', [
- 'objectService' => $this->objectService ? 'available' : 'null',
- 'userManager' => $this->userManager ? 'available' : 'null',
- 'groupManager' => $this->groupManager ? 'available' : 'null'
- ]);
-
- return new JSONResponse([
- 'success' => true,
- 'message' => 'Test endpoint working',
- 'services' => [
- 'objectService' => $this->objectService ? 'available' : 'null',
- 'userManager' => $this->userManager ? 'available' : 'null',
- 'groupManager' => $this->groupManager ? 'available' : 'null'
- ]
- ]);
+ if ($this->objectService !== null) {
+ $objectServiceAvail = 'available';
+ } else {
+ $objectServiceAvail = 'null';
+ }
+
+ if ($this->userManager !== null) {
+ $userManagerAvail = 'available';
+ } else {
+ $userManagerAvail = 'null';
+ }
+
+ if ($this->groupManager !== null) {
+ $groupManagerAvail = 'available';
+ } else {
+ $groupManagerAvail = 'null';
+ }
+
+ $this->logger->info(
+ 'testBulkUserInfo called',
+ [
+ 'objectService' => $objectServiceAvail,
+ 'userManager' => $userManagerAvail,
+ 'groupManager' => $groupManagerAvail,
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'message' => 'Test endpoint working',
+ 'services' => [
+ 'objectService' => $objectServiceAvail,
+ 'userManager' => $userManagerAvail,
+ 'groupManager' => $groupManagerAvail,
+ ],
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('Test endpoint error', ['error' => $e->getMessage()]);
- return new JSONResponse(['success' => false, 'message' => $e->getMessage()], 500);
- }
- }
+ $this->logger->error(
+ 'Test endpoint error',
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => $e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end testBulkUserInfo()
/**
- * Get user info for multiple contactpersonen in one request
+ * Get user info for multiple contactpersonen in one request.
+ *
+ * @return JSONResponse Bulk user info keyed by contactpersoon ID.
+ *
* @NoAdminRequired
* @NoCSRFRequired
- * @return JSONResponse
*/
- public function getBulkUserInfo(): JSONResponse {
+ public function getBulkUserInfo(): JSONResponse
+ {
try {
$input = json_decode(file_get_contents('php://input'), true);
$contactpersoonIds = $input['contactpersoonIds'] ?? [];
- $this->logger->info('Controller: getBulkUserInfo called', [
- 'input' => $input,
- 'contactpersoonIds' => $contactpersoonIds
- ]);
-
- if (empty($contactpersoonIds) || !is_array($contactpersoonIds)) {
- return new JSONResponse(['success' => false, 'message' => 'No contactpersoon IDs provided'], 400);
+ $this->logger->info(
+ 'Controller: getBulkUserInfo called',
+ [
+ 'input' => $input,
+ 'contactpersoonIds' => $contactpersoonIds,
+ ]
+ );
+
+ if (empty($contactpersoonIds) === true || is_array($contactpersoonIds) === false) {
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'No contactpersoon IDs provided',
+ ],
+ 400
+ );
}
- // Delegate to service
+ // Delegate to service.
$bulkUserInfo = $this->contactpersoonService->getBulkUserInfo($contactpersoonIds);
- return new JSONResponse([
- 'success' => true,
- 'userInfo' => $bulkUserInfo
- ]);
-
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'userInfo' => $bulkUserInfo,
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('Controller: Failed to get bulk user info', [
- 'error' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get bulk user info: ' . $e->getMessage()
- ], 500);
- }
- }
+ $this->logger->error(
+ 'Controller: Failed to get bulk user info',
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get bulk user info: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getBulkUserInfo()
/**
- * Get current user profile information
+ * Get current user profile information.
*
* Returns the current logged-in user's profile including:
* - email, firstName, middleName, lastName, functie
* - organisations.active (the currently active organisation)
* - organisations.all (all organisations the user belongs to)
*
- * @return JSONResponse The user profile data
+ * @return JSONResponse The user profile data.
*
* @NoAdminRequired
* @NoCSRFRequired
@@ -929,133 +1192,150 @@ public function getBulkUserInfo(): JSONResponse {
public function getMe(): JSONResponse
{
try {
- // Get current user from session
+ // Get current user from session.
$user = $this->userSession->getUser();
if ($user === null) {
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Not authenticated'
- ], 401);
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Not authenticated',
+ ],
+ 401
+ );
}
- $userId = $user->getUID();
+ $userId = $user->getUID();
$userEmail = $user->getEMailAddress() ?? $userId;
- $this->logger->info('ContactpersonenController: Getting /me data for user', [
- 'userId' => $userId,
- 'userEmail' => $userEmail
- ]);
+ $this->logger->info(
+ 'ContactpersonenController: Getting /me data for user',
+ [
+ 'userId' => $userId,
+ 'userEmail' => $userEmail,
+ ]
+ );
- // Initialize response with user data from Nextcloud
+ // Initialize response with user data from Nextcloud.
$response = [
- 'email' => $userEmail,
- 'firstName' => '',
- 'middleName' => '',
- 'lastName' => '',
- 'functie' => '',
+ 'email' => $userEmail,
+ 'firstName' => '',
+ 'middleName' => '',
+ 'lastName' => '',
+ 'functie' => '',
'organisations' => [
'active' => null,
- 'all' => []
- ]
+ 'all' => [],
+ ],
];
- // Try to get contactpersoon data for additional profile info
+ // Try to get contactpersoon data for additional profile info.
try {
$objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
- // Search for contactpersoon by username (which is the email)
+ // Search for contactpersoon by username (which is the email).
$searchParams = [
'username' => $userId,
- '_limit' => 1,
- '_schema' => 'contactpersoon'
+ '_limit' => 1,
+ '_schema' => 'contactpersoon',
];
$contactpersonen = $objectService->searchObjectsPaginated($searchParams);
- if (!empty($contactpersonen['results'])) {
+ if (empty($contactpersonen['results']) === false) {
$contactpersoon = $contactpersonen['results'][0];
- $contactData = $contactpersoon->getObject();
+ $contactData = $contactpersoon->getObject();
- // Extract name parts
- $response['firstName'] = $contactData['voornaam'] ?? $contactData['firstName'] ?? '';
+ // Extract name parts.
+ $response['firstName'] = $contactData['voornaam'] ?? $contactData['firstName'] ?? '';
$response['middleName'] = $contactData['tussenvoegsel'] ?? $contactData['middleName'] ?? '';
- $response['lastName'] = $contactData['achternaam'] ?? $contactData['lastName'] ?? '';
- $response['functie'] = $contactData['functie'] ?? '';
+ $response['lastName'] = $contactData['achternaam'] ?? $contactData['lastName'] ?? '';
+ $response['functie'] = $contactData['functie'] ?? '';
- // If email not set, try from contact data
- if (empty($response['email'])) {
+ // If email not set, try from contact data.
+ if (empty($response['email']) === true) {
$response['email'] = $contactData['e-mailadres'] ?? $contactData['email'] ?? $userEmail;
}
}
} catch (\Exception $e) {
- $this->logger->debug('ContactpersonenController: Could not find contactpersoon for user', [
- 'userId' => $userId,
- 'error' => $e->getMessage()
- ]);
- }
-
- // Get organisation data from OpenRegister
+ $this->logger->debug(
+ 'ContactpersonenController: Could not find contactpersoon for user',
+ [
+ 'userId' => $userId,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+
+ // Get organisation data from OpenRegister.
try {
$organisationService = $this->container->get('OCA\OpenRegister\Service\OrganisationService');
- // Get active organisation
+ // Get active organisation.
$activeOrg = $organisationService->getActiveOrganisation();
if ($activeOrg !== null) {
$response['organisations']['active'] = [
'uuid' => $activeOrg->getUuid(),
'naam' => $activeOrg->getName(),
- 'id' => (string)$activeOrg->getId(),
- 'slug' => $activeOrg->getSlug() ?? $this->createSlug($activeOrg->getName())
+ 'id' => (string) $activeOrg->getId(),
+ 'slug' => $activeOrg->getSlug() ?? $this->createSlug(name: $activeOrg->getName()),
];
}
- // Get all user organisations
+ // Get all user organisations.
$userOrgs = $organisationService->getUserOrganisations();
foreach ($userOrgs as $org) {
$response['organisations']['all'][] = [
'uuid' => $org->getUuid(),
'naam' => $org->getName(),
- 'id' => (string)$org->getId(),
- 'slug' => $org->getSlug() ?? $this->createSlug($org->getName())
+ 'id' => (string) $org->getId(),
+ 'slug' => $org->getSlug() ?? $this->createSlug(name: $org->getName()),
];
}
} catch (\Exception $e) {
- $this->logger->warning('ContactpersonenController: Could not get organisation data', [
- 'userId' => $userId,
- 'error' => $e->getMessage()
- ]);
- }
+ $this->logger->warning(
+ 'ContactpersonenController: Could not get organisation data',
+ [
+ 'userId' => $userId,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
return new JSONResponse($response);
-
} catch (\Exception $e) {
- $this->logger->error('ContactpersonenController: Failed to get /me data', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
-
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get user profile: ' . $e->getMessage()
- ], 500);
- }
- }
+ $this->logger->error(
+ 'ContactpersonenController: Failed to get /me data',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get user profile: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getMe()
/**
- * Create a URL-friendly slug from a name
+ * Create a URL-friendly slug from a name.
*
- * @param string $name The name to convert
+ * @param string $name The name to convert.
*
- * @return string The slug
+ * @return string The slug.
*/
private function createSlug(string $name): string
{
- // Convert to lowercase
+ // Convert to lowercase.
$slug = strtolower($name);
- // Replace spaces and special chars with hyphens
+ // Replace spaces and special chars with hyphens.
$slug = preg_replace('/[^a-z0-9]+/', '-', $slug);
- // Remove leading/trailing hyphens
+ // Remove leading/trailing hyphens.
$slug = trim($slug, '-');
return $slug;
- }
-}
+ }//end createSlug()
+}//end class
diff --git a/lib/Controller/DashboardController.php b/lib/Controller/DashboardController.php
index f349f759..a600b663 100644
--- a/lib/Controller/DashboardController.php
+++ b/lib/Controller/DashboardController.php
@@ -1,4 +1,15 @@
+ * @copyright 2024 Conduction B.V.
+ * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
+ */
namespace OCA\SoftwareCatalog\Controller;
@@ -10,25 +21,36 @@
class DashboardController extends Controller
{
-
+ /**
+ * Constructor for DashboardController.
+ *
+ * @param string $appName The app name
+ * @param IRequest $request The request object
+ */
public function __construct($appName, IRequest $request)
{
- parent::__construct($appName, $request);
- }
+ parent::__construct(appName: $appName, request: $request);
+ }//end __construct()
/**
+ * Renders the main application page.
+ *
+ * @param string|null $getParameter Optional query parameter
+ *
+ * @return TemplateResponse The rendered page template
+ *
* @NoAdminRequired
* @NoCSRFRequired
*/
- public function page(?string $getParameter)
+ public function page(?string $getParameter): TemplateResponse
{
try {
- $response =new TemplateResponse(
+ $response = new TemplateResponse(
$this->appName,
'index',
[]
);
-
+
$csp = new ContentSecurityPolicy();
$csp->addAllowedConnectDomain('*');
$response->setContentSecurityPolicy($csp);
@@ -42,19 +64,23 @@ public function page(?string $getParameter)
'500'
);
}
- }
+ }//end page()
/**
+ * Returns an empty JSON result.
+ *
* @NoAdminRequired
* @NoCSRFRequired
+ *
+ * @return JSONResponse The JSON response with empty results
*/
public function index(): JSONResponse
{
try {
- $results = ["results" => self::TEST_ARRAY];
+ $results = ['results' => []];
return new JSONResponse($results);
} catch (\Exception $e) {
return new JSONResponse(['error' => $e->getMessage()], 500);
}
- }
-}
+ }//end index()
+}//end class
diff --git a/lib/Controller/GebruikController.php b/lib/Controller/GebruikController.php
index 70cf18f8..251e0615 100644
--- a/lib/Controller/GebruikController.php
+++ b/lib/Controller/GebruikController.php
@@ -8,9 +8,9 @@
*
* @category Controller
* @package OCA\SoftwareCatalog\Controller
- * @author SoftwareCatalog Team
+ * @author Conduction b.v.
* @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/nextcloud/softwarecatalog
*/
@@ -27,25 +27,29 @@
use OCP\IUserSession;
/**
- * Controller for handling view-related API operations
+ * Controller for handling gebruik-related API operations.
*
- * This controller provides REST API endpoints for querying and managing ArchiMate views
- * with optional enrichment capabilities for products, usage data (gebruik), and related information.
+ * This controller provides REST API endpoints for querying and managing gebruik objects
+ * with role-based access for gebruik-beheerder and aanbod-beheerder users.
*
* @category Controller
* @package OCA\SoftwareCatalog\Controller
- * @author SoftwareCatalog Team
+ * @author Conduction b.v.
* @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/nextcloud/softwarecatalog
*/
class GebruikController extends Controller
{
/**
- * Constructor for ViewController
+ * Constructor for GebruikController.
*
- * @param string $appName The app name
- * @param IRequest $request The request object
+ * @param string $appName The app name
+ * @param IRequest $request The request object
+ * @param IUserSession $userSession The user session service
+ * @param IGroupManager $groupManager The group manager service
+ * @param IConfig $config The configuration service
+ * @param GebruikService $gebruikService The gebruik service
*/
public function __construct(
string $appName,
@@ -55,17 +59,20 @@ public function __construct(
private readonly IConfig $config,
private readonly GebruikService $gebruikService,
) {
- parent::__construct($appName, $request);
- }
+ parent::__construct(appName: $appName, request: $request);
+ }//end __construct()
/**
- * Fetch gebruiken, for a gebruik-beheerder, get all gebruiken, for an aanbod-beheerder, fetch gebruiken of applications of the organization of the user.
+ * Fetch gebruiken based on user role.
+ *
+ * For a gebruik-beheerder, returns all gebruiken.
+ * For an aanbod-beheerder, returns gebruiken of applications of the user's organization.
*
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
*
- * @return JSONResponse
+ * @return JSONResponse The JSON response with gebruiken results
*/
public function getGebruiken(): JSONResponse
{
@@ -76,14 +83,19 @@ public function getGebruiken(): JSONResponse
return new JSONResponse($this->getEmptyResult());
}
- $groups = $this->groupManager->getUserGroups(user: $user);
- $groupNames = array_map(function (IGroup $group) {
- return $group->getGID();
- }, $groups);
+ $groups = $this->groupManager->getUserGroups(user: $user);
+ $groupNames = array_map(
+ function (IGroup $group) {
+ return $group->getGID();
+ },
+ $groups
+ );
$orgUuid = $this->config->getUserValue(userId: $user->getUID(), appName: 'core', key: 'organisation');
- if (in_array(needle: 'admin', haystack: $groupNames) === true || in_array(needle: 'gebruik-beheerder', haystack: $groupNames) === true) {
+ $isAdmin = in_array(needle: 'admin', haystack: $groupNames);
+ $isBeheerder = in_array(needle: 'gebruik-beheerder', haystack: $groupNames);
+ if ($isAdmin === true || $isBeheerder === true) {
$options = $this->request->getParams();
} else if (in_array(needle: 'aanbod-beheerder', haystack: $groupNames) === true) {
$options = $this->request->getParams();
@@ -99,7 +111,6 @@ public function getGebruiken(): JSONResponse
} else if (isset($options['module']) === false) {
$options['module'] = $applicatieIds;
}
-
} else {
return new JSONResponse($this->getEmptyResult());
}
@@ -109,7 +120,7 @@ public function getGebruiken(): JSONResponse
} catch (Exception $e) {
return new JSONResponse(['error' => $e->getMessage()], statusCode: 500);
}
- }
+ }//end getGebruiken()
/**
* Fetch gebruiken for a deelnemer.
@@ -138,7 +149,7 @@ public function getGebruikenForDeelnemer(): JSONResponse
} catch (Exception $e) {
return new JSONResponse(['error' => $e->getMessage()], statusCode: 500);
}
- }
+ }//end getGebruikenForDeelnemer()
/**
* Returns an empty result set with the standard paginated response structure.
@@ -164,8 +175,5 @@ private function getEmptyResult(): array
'deleted' => false,
],
];
- }
-
-
-
-}
+ }//end getEmptyResult()
+}//end class
diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php
index 44cb3e26..a2d9a553 100644
--- a/lib/Controller/SettingsController.php
+++ b/lib/Controller/SettingsController.php
@@ -46,20 +46,19 @@ class SettingsController extends Controller
*/
private $objectService;
-
/**
* SettingsController constructor.
*
- * @param string $appName The name of the app
- * @param IRequest $request The request object
- * @param IAppConfig $config The app configuration
- * @param ContainerInterface $container The container
- * @param IAppManager $appManager The app manager
- * @param SettingsService $settingsService The settings service
- * @param OrganizationSyncService $organizationSyncService The organization sync service
- * @param ArchiMateService $archiMateService The ArchiMate import/export service
- * @param ProgressTracker $progressTracker The progress tracking service
- * @param LoggerInterface $logger The logger instance
+ * @param string $appName The name of the app.
+ * @param IRequest $request The request object.
+ * @param IAppConfig $config The app configuration.
+ * @param ContainerInterface $container The container.
+ * @param IAppManager $appManager The app manager.
+ * @param SettingsService $settingsService The settings service.
+ * @param OrganizationSyncService $organizationSyncService The organization sync service.
+ * @param ArchiMateService $archiMateService The ArchiMate import/export service.
+ * @param ProgressTracker $progressTracker The progress tracking service.
+ * @param LoggerInterface $logger The logger instance.
*/
public function __construct(
$appName,
@@ -70,14 +69,13 @@ public function __construct(
private readonly SettingsService $settingsService,
private readonly OrganizationSyncService $organizationSyncService,
private readonly ArchiMateService $archiMateService,
+ private readonly ProgressTracker $progressTracker,
private readonly LoggerInterface $logger,
) {
- parent::__construct($appName, $request);
- $this->_appName = $appName;
+ parent::__construct(appName: $appName, request: $request);
}//end __construct()
-
/**
* Attempts to retrieve the OpenRegister service from the container.
*
@@ -86,7 +84,7 @@ public function __construct(
*/
public function getObjectService(): ?\OCA\OpenRegister\Service\ObjectService
{
- if (in_array('openregister', $this->appManager->getInstalledApps())) {
+ if (in_array(needle: 'openregister', haystack: $this->appManager->getInstalledApps()) === true) {
$this->objectService = $this->container->get('OCA\OpenRegister\Service\ObjectService');
return $this->objectService;
}
@@ -95,7 +93,6 @@ public function getObjectService(): ?\OCA\OpenRegister\Service\ObjectService
}//end getObjectService()
-
/**
* Attempts to retrieve the Configuration service from the container.
*
@@ -105,7 +102,7 @@ public function getObjectService(): ?\OCA\OpenRegister\Service\ObjectService
public function getConfigurationService(): ?\OCA\OpenRegister\Service\ConfigurationService
{
// Check if the 'openregister' app is installed.
- if (in_array('openregister', $this->appManager->getInstalledApps())) {
+ if (in_array(needle: 'openregister', haystack: $this->appManager->getInstalledApps()) === true) {
// Retrieve the ConfigurationService from the container.
$configurationService = $this->container->get('OCA\OpenRegister\Service\ConfigurationService');
return $configurationService;
@@ -116,7 +113,6 @@ public function getConfigurationService(): ?\OCA\OpenRegister\Service\Configurat
}//end getConfigurationService()
-
/**
* Retrieve the current settings.
*
@@ -128,19 +124,21 @@ public function getConfigurationService(): ?\OCA\OpenRegister\Service\Configurat
public function index(): JSONResponse
{
try {
- // Delegate all business logic to service
+ // Delegate all business logic to service.
$data = $this->settingsService->getAllSettings();
return new JSONResponse($data);
} catch (\Exception $e) {
- $this->logger->error('Failed to retrieve settings', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to retrieve settings',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return new JSONResponse(['error' => $e->getMessage()], 500);
}
}//end index()
-
/**
* Handle the post request to update settings.
*
@@ -152,80 +150,100 @@ public function create(): JSONResponse
{
try {
$data = $this->request->getParams();
-
- // Handle different types of settings updates
+
+ // Handle different types of settings updates.
$result = [];
-
- // Update schema/register configuration
- if (isset($data['configuration']) || isset($data['selectedRegister'])) {
- $configData = array_filter($data, function($key) {
- return !in_array($key, ['userGroups', 'emailSettings']);
- }, ARRAY_FILTER_USE_KEY);
-
- if (!empty($configData)) {
+
+ // Update schema/register configuration.
+ if (isset($data['configuration']) === true || isset($data['selectedRegister']) === true) {
+ $configData = array_filter(
+ $data,
+ function ($key) {
+ return in_array(needle: $key, haystack: ['userGroups', 'emailSettings']) === false;
+ },
+ ARRAY_FILTER_USE_KEY
+ );
+
+ if (empty($configData) === false) {
$result['configuration'] = $this->settingsService->updateSettings($configData);
}
}
-
- // Update user groups
- if (isset($data['userGroups'])) {
+
+ // Update user groups.
+ if (isset($data['userGroups']) === true) {
$userGroups = $data['userGroups'];
-
- if (isset($userGroups['generic'])) {
+
+ if (isset($userGroups['generic']) === true) {
$validation = $this->settingsService->validateGroups($userGroups['generic']);
- if (!empty($validation['invalid'])) {
- return new JSONResponse([
- 'error' => 'Invalid generic group names provided',
- 'validation' => $validation
- ], 400);
+ if (empty($validation['invalid']) === false) {
+ return new JSONResponse(
+ [
+ 'error' => 'Invalid generic group names provided',
+ 'validation' => $validation,
+ ],
+ 400
+ );
}
+
$this->settingsService->setGenericUserGroups($validation['valid']);
$result['userGroups']['generic'] = $validation['valid'];
}
-
- if (isset($userGroups['organizationAdmin'])) {
+
+ if (isset($userGroups['organizationAdmin']) === true) {
$validation = $this->settingsService->validateGroups($userGroups['organizationAdmin']);
- if (!empty($validation['invalid'])) {
- return new JSONResponse([
- 'error' => 'Invalid organization admin group names provided',
- 'validation' => $validation
- ], 400);
+ if (empty($validation['invalid']) === false) {
+ return new JSONResponse(
+ [
+ 'error' => 'Invalid organization admin group names provided',
+ 'validation' => $validation,
+ ],
+ 400
+ );
}
+
$this->settingsService->setOrganizationAdminGroups($validation['valid']);
$result['userGroups']['organizationAdmin'] = $validation['valid'];
}
-
- if (isset($userGroups['superUser'])) {
+
+ if (isset($userGroups['superUser']) === true) {
$validation = $this->settingsService->validateGroups($userGroups['superUser']);
- if (!empty($validation['invalid'])) {
- return new JSONResponse([
- 'error' => 'Invalid super user group names provided',
- 'validation' => $validation
- ], 400);
+ if (empty($validation['invalid']) === false) {
+ return new JSONResponse(
+ [
+ 'error' => 'Invalid super user group names provided',
+ 'validation' => $validation,
+ ],
+ 400
+ );
}
+
$this->settingsService->setSuperUserGroups($validation['valid']);
$result['userGroups']['superUser'] = $validation['valid'];
}
- }
-
- // Update email settings
- if (isset($data['emailSettings'])) {
+ }//end if
+
+ // Update email settings.
+ if (isset($data['emailSettings']) === true) {
$result['emailSettings'] = $this->settingsService->updateEmailSettings($data['emailSettings']);
}
-
- return new JSONResponse([
- 'success' => true,
- 'data' => $result,
- 'message' => 'Settings updated successfully'
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to update settings', [
- 'exception' => $e->getMessage(),
- 'requestData' => $this->request->getParams()
- ]);
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'data' => $result,
+ 'message' => 'Settings updated successfully',
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to update settings',
+ [
+ 'exception' => $e->getMessage(),
+ 'requestData' => $this->request->getParams(),
+ ]
+ );
return new JSONResponse(['error' => $e->getMessage()], 500);
- }
+ }//end try
}//end create()
@@ -233,7 +251,7 @@ public function create(): JSONResponse
* Get general configuration settings
*
* @NoCSRFRequired
- *
+ *
* @return JSONResponse General configuration
*/
public function getGeneralConfig(): JSONResponse
@@ -242,64 +260,78 @@ public function getGeneralConfig(): JSONResponse
$config = [
'catalogLocation' => $this->settingsService->getCatalogLocation(),
];
-
- return new JSONResponse([
- 'success' => true,
- 'config' => $config
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to get general config', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get general config: ' . $e->getMessage()
- ], 500);
- }
- }
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'config' => $config,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to get general config',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get general config: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getGeneralConfig()
/**
* Update general configuration settings
*
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Update result
*/
public function updateGeneralConfig(): JSONResponse
{
try {
$data = $this->request->getParams();
-
- if (isset($data['catalogLocation'])) {
+
+ if (isset($data['catalogLocation']) === true) {
$this->settingsService->setCatalogLocation($data['catalogLocation']);
}
-
- return new JSONResponse([
- 'success' => true,
- 'message' => 'General configuration updated successfully',
- 'config' => [
- 'catalogLocation' => $this->settingsService->getCatalogLocation(),
- ]
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to update general config', [
- 'exception' => $e->getMessage(),
- 'requestData' => $this->request->getParams()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to update general config: ' . $e->getMessage()
- ], 500);
- }
- }
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'message' => 'General configuration updated successfully',
+ 'config' => [
+ 'catalogLocation' => $this->settingsService->getCatalogLocation(),
+ ],
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to update general config',
+ [
+ 'exception' => $e->getMessage(),
+ 'requestData' => $this->request->getParams(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to update general config: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end updateGeneralConfig()
/**
* Get organization synchronization configuration
*
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Sync configuration
*/
public function getSyncConfig(): JSONResponse
@@ -308,58 +340,72 @@ public function getSyncConfig(): JSONResponse
$config = [
'syncTimeWindow' => $this->config->getValueString($this->_appName, 'syncTimeWindow', '10'),
];
-
- return new JSONResponse([
- 'success' => true,
- 'config' => $config
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to get sync config', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get sync config: ' . $e->getMessage()
- ], 500);
- }
- }
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'config' => $config,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to get sync config',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get sync config: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getSyncConfig()
/**
* Update organization synchronization configuration
*
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Update result
*/
public function updateSyncConfig(): JSONResponse
{
try {
$data = $this->request->getParams();
-
- if (isset($data['syncTimeWindow'])) {
+
+ if (isset($data['syncTimeWindow']) === true) {
$this->config->setValueString($this->_appName, 'syncTimeWindow', (string) $data['syncTimeWindow']);
}
-
- return new JSONResponse([
- 'success' => true,
- 'message' => 'Sync configuration updated successfully',
- 'config' => [
- 'syncTimeWindow' => $this->config->getValueString($this->_appName, 'syncTimeWindow', '10'),
- ]
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to update sync config', [
- 'exception' => $e->getMessage(),
- 'requestData' => $this->request->getParams()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to update sync config: ' . $e->getMessage()
- ], 500);
- }
- }
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'message' => 'Sync configuration updated successfully',
+ 'config' => [
+ 'syncTimeWindow' => $this->config->getValueString($this->_appName, 'syncTimeWindow', '10'),
+ ],
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to update sync config',
+ [
+ 'exception' => $e->getMessage(),
+ 'requestData' => $this->request->getParams(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to update sync config: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end updateSyncConfig()
/**
* Load the settings from the publication_register.json file.
@@ -379,7 +425,6 @@ public function load(): JSONResponse
}//end load()
-
/**
* Initialize the SoftwareCatalog settings
*
@@ -393,13 +438,15 @@ public function initialize(): JSONResponse
$result = $this->settingsService->initialize();
return new JSONResponse($result);
} catch (\Exception $e) {
- $this->logger->error('Failed to initialize settings', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to initialize settings',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return new JSONResponse(['error' => $e->getMessage()], 500);
}
- }
-
+ }//end initialize()
/**
* Get configuration status
@@ -413,40 +460,50 @@ public function status(): JSONResponse
{
try {
$this->logger->debug('SettingsController: Getting configuration status');
-
- $status = $this->settingsService->getConfigurationStatus();
+
+ $status = $this->settingsService->getConfigurationStatus();
$isFullyConfigured = $this->settingsService->isFullyConfigured();
- $versionInfo = $this->settingsService->getVersionInfo();
-
+ $versionInfo = $this->settingsService->getVersionInfo();
+
$responseData = [
- 'status' => $status,
- 'fullyConfigured' => $isFullyConfigured,
- 'versionInfo' => $versionInfo,
- 'timestamp' => time(),
- 'autoConfigCompleted' => $this->config->getValueString('softwarecatalog', 'auto_config_completed', 'false') === 'true'
+ 'status' => $status,
+ 'fullyConfigured' => $isFullyConfigured,
+ 'versionInfo' => $versionInfo,
+ 'timestamp' => time(),
+ 'autoConfigCompleted' => $this->config->getValueString(
+ 'softwarecatalog',
+ 'auto_config_completed',
+ 'false'
+ ) === 'true',
];
-
- $this->logger->info('SettingsController: Configuration status compiled', [
- 'fullyConfigured' => $isFullyConfigured,
- 'needsUpdate' => $versionInfo['needsUpdate'] ?? null,
- 'versionsMatch' => $versionInfo['versionsMatch'] ?? null
- ]);
-
- return new JSONResponse($responseData);
- } catch (\Exception $e) {
- $this->logger->error('SettingsController: Failed to get configuration status', [
- 'exception_message' => $e->getMessage(),
- 'exception' => $e
- ]);
- return new JSONResponse([
- 'error' => $e->getMessage(),
- 'timestamp' => time()
- ], 500);
- }
- }
-
+ $this->logger->info(
+ 'SettingsController: Configuration status compiled',
+ [
+ 'fullyConfigured' => $isFullyConfigured,
+ 'needsUpdate' => $versionInfo['needsUpdate'] ?? null,
+ 'versionsMatch' => $versionInfo['versionsMatch'] ?? null,
+ ]
+ );
+ return new JSONResponse($responseData);
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'SettingsController: Failed to get configuration status',
+ [
+ 'exception_message' => $e->getMessage(),
+ 'exception' => $e,
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'error' => $e->getMessage(),
+ 'timestamp' => time(),
+ ],
+ 500
+ );
+ }//end try
+ }//end status()
/**
* Auto-configure settings
@@ -459,25 +516,32 @@ public function autoConfigure(): JSONResponse
{
try {
$configuration = $this->settingsService->autoConfigure();
- if (!empty($configuration)) {
+ if (empty($configuration) === false) {
$result = $this->settingsService->updateSettings($configuration);
- return new JSONResponse([
- 'success' => true,
- 'configuration' => $result
- ]);
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'configuration' => $result,
+ ]
+ );
} else {
- return new JSONResponse([
- 'success' => false,
- 'message' => 'No matching registers or schemas found for auto-configuration'
- ]);
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'No matching registers or schemas found for auto-configuration',
+ ]
+ );
}
} catch (\Exception $e) {
- $this->logger->error('Failed to auto-configure settings', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to auto-configure settings',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return new JSONResponse(['error' => $e->getMessage()], 500);
- }
- }
+ }//end try
+ }//end autoConfigure()
/**
* Get object counts statistics for all configured registers
@@ -491,20 +555,28 @@ public function stats(): JSONResponse
{
try {
$statistics = $this->settingsService->getObjectCountsStatistics();
- return new JSONResponse([
- 'success' => true,
- 'statistics' => $statistics
- ]);
- } catch (\Exception $e) {
- $this->logger->error('Failed to get object counts statistics', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'error' => $e->getMessage()
- ], 500);
- }
- }
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'statistics' => $statistics,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to get object counts statistics',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'error' => $e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end stats()
/**
* Get debug information for settings
@@ -520,12 +592,15 @@ public function debug(): JSONResponse
$debugInfo = $this->settingsService->getDebugInfo();
return new JSONResponse($debugInfo);
} catch (\Exception $e) {
- $this->logger->error('Failed to get debug information', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to get debug information',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return new JSONResponse(['error' => $e->getMessage()], 500);
}
- }
+ }//end debug()
/**
* Send a test email
@@ -537,46 +612,56 @@ public function debug(): JSONResponse
public function sendTestEmail(): JSONResponse
{
try {
- $data = $this->request->getParams();
- $email = $data['email'] ?? '';
+ $data = $this->request->getParams();
+ $email = $data['email'] ?? '';
$emailSettings = $data['emailSettings'] ?? [];
-
- // Delegate all business logic (including validation) to service
- $result = $this->settingsService->sendTestEmail($email, $emailSettings);
-
- return new JSONResponse([
- 'success' => $result['success'],
- 'message' => $result['message']
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('SoftwareCatalog: Failed to send test email in controller', [
- 'exception_class' => get_class($e),
- 'exception_message' => $e->getMessage(),
- 'requestData' => $this->request->getParams()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to send test email: ' . $e->getMessage()
- ], 500);
- }
- }
+
+ // Delegate all business logic (including validation) to service.
+ $result = $this->settingsService->sendTestEmail(
+ email: $email,
+ emailSettings: $emailSettings
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => $result['success'],
+ 'message' => $result['message'],
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'SoftwareCatalog: Failed to send test email in controller',
+ [
+ 'exception_class' => get_class($e),
+ 'exception_message' => $e->getMessage(),
+ 'requestData' => $this->request->getParams(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to send test email: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end sendTestEmail()
/**
* Get organization synchronization status with processing predictions
*
* @param int $minutesBack Number of minutes to look back for prediction (default: 10)
- *
+ *
* @return JSONResponse JSON response containing sync status information
*
* @NoAdminRequired
* @NoCSRFRequired
*/
- public function getSyncStatus(int $minutesBack = 10): JSONResponse
+ public function getSyncStatus(int $minutesBack=10): JSONResponse
{
$status = $this->organizationSyncService->getSyncStatusWithErrorHandling($minutesBack);
return new JSONResponse($status);
- }
+ }//end getSyncStatus()
/**
* Perform manual organization synchronization
@@ -587,45 +672,55 @@ public function getSyncStatus(int $minutesBack = 10): JSONResponse
*
* @NoCSRFRequired
*/
- public function performSync(int $minutesBack = 0): JSONResponse
+ public function performSync(int $minutesBack=0): JSONResponse
{
try {
- // For full sync (minutesBack = 0), use optimized batch processing to handle large datasets
+ // For full sync (minutesBack = 0), use optimized batch processing to handle large datasets.
if ($minutesBack === 0) {
$result = $this->organizationSyncService->performOptimizedManualSync(
- maxRounds: 15, // Up to 15 rounds of processing
- batchSize: 75 // 75 items per batch for good performance
+ maxRounds: 15,
+ // Up to 15 rounds of processing.
+ batchSize: 75
+ // 75 items per batch for good performance.
);
-
- return new JSONResponse([
- 'success' => true,
- 'results' => $result,
- 'message' => 'Optimized synchronization completed successfully',
- 'isOptimized' => true
- ]);
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'results' => $result,
+ 'message' => 'Optimized synchronization completed successfully',
+ 'isOptimized' => true,
+ ]
+ );
} else {
- // For incremental sync, use the original method
+ // For incremental sync, use the original method.
$result = $this->organizationSyncService->performManualSync($minutesBack);
-
- if ($result['success']) {
+
+ if ($result['success'] === true) {
return new JSONResponse($result);
} else {
return new JSONResponse($result, 500);
}
- }
+ }//end if
} catch (\Exception $e) {
- $this->logger->error('Manual sync failed', [
- 'minutesBack' => $minutesBack,
- 'exception' => $e->getMessage()
- ]);
-
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Synchronization failed: ' . $e->getMessage(),
- 'error' => $e->getMessage()
- ], 500);
- }
- }
+ $this->logger->error(
+ 'Manual sync failed',
+ [
+ 'minutesBack' => $minutesBack,
+ 'exception' => $e->getMessage(),
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Synchronization failed: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end performSync()
/**
* Heartbeat endpoint to keep connections alive during long-running operations
@@ -642,30 +737,41 @@ public function heartbeat(): JSONResponse
{
try {
$timestamp = $this->request->getParam('timestamp', time() * 1000);
-
- $this->logger->debug('Heartbeat received', [
- 'timestamp' => $timestamp,
- 'server_time' => time() * 1000
- ]);
-
- return new JSONResponse([
- 'success' => true,
- 'message' => 'Heartbeat received',
- 'timestamp' => $timestamp,
- 'server_time' => time() * 1000
- ]);
- } catch (\Exception $e) {
- $this->logger->error('Heartbeat error: ' . $e->getMessage(), [
- 'exception' => $e
- ]);
-
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Heartbeat failed',
- 'error' => $e->getMessage()
- ], 500);
- }
- }
+
+ $this->logger->debug(
+ 'Heartbeat received',
+ [
+ 'timestamp' => $timestamp,
+ 'server_time' => time() * 1000,
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'message' => 'Heartbeat received',
+ 'timestamp' => $timestamp,
+ 'server_time' => time() * 1000,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Heartbeat error: '.$e->getMessage(),
+ [
+ 'exception' => $e,
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Heartbeat failed',
+ 'error' => $e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end heartbeat()
/**
* Get version information for the app and configuration.
@@ -680,25 +786,34 @@ public function getVersionInfo(): JSONResponse
try {
$this->logger->info('SettingsController: Getting version information');
$data = $this->settingsService->getVersionInfo();
-
- $this->logger->info('SettingsController: Version info retrieved', [
- 'version_info' => $data
- ]);
-
- // Add timestamp for cache busting
+
+ $this->logger->info(
+ 'SettingsController: Version info retrieved',
+ [
+ 'version_info' => $data,
+ ]
+ );
+
+ // Add timestamp for cache busting.
$data['timestamp'] = time();
-
+
return new JSONResponse($data);
} catch (\Exception $e) {
- $this->logger->error('SettingsController: Failed to get version info', [
- 'exception_message' => $e->getMessage(),
- 'exception' => $e
- ]);
- return new JSONResponse([
- 'error' => $e->getMessage(),
- 'timestamp' => time()
- ], 500);
- }
+ $this->logger->error(
+ 'SettingsController: Failed to get version info',
+ [
+ 'exception_message' => $e->getMessage(),
+ 'exception' => $e,
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'error' => $e->getMessage(),
+ 'timestamp' => time(),
+ ],
+ 500
+ );
+ }//end try
}//end getVersionInfo()
/**
@@ -712,23 +827,26 @@ public function resetAutoConfig(): JSONResponse
{
try {
$params = $this->request->getParams();
- $resetConfiguration = isset($params['resetConfiguration']) && $params['resetConfiguration'] === true;
-
+ $resetConfiguration = isset($params['resetConfiguration']) === true && $params['resetConfiguration'] === true;
+
$result = $this->settingsService->resetAutoConfiguration($resetConfiguration);
-
- if ($result['success']) {
+
+ if ($result['success'] === true) {
return new JSONResponse($result);
} else {
return new JSONResponse($result, 400);
}
} catch (\Exception $e) {
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Reset failed: ' . $e->getMessage(),
- 'error' => $e->getMessage()
- ], 500);
- }
- }
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Reset failed: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end resetAutoConfig()
/**
* Clear configuration cache to force reload of schema IDs and register IDs.
@@ -741,25 +859,33 @@ public function clearCache(): JSONResponse
{
try {
$this->logger->info('SettingsController: Clearing configuration cache');
-
+
$this->settingsService->clearConfigurationCache();
-
- return new JSONResponse([
- 'success' => true,
- 'message' => 'Configuration cache cleared successfully'
- ]);
- } catch (\Exception $e) {
- $this->logger->error('SettingsController: Cache clear failed', [
- 'exception' => $e->getMessage()
- ]);
-
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Cache clear failed: ' . $e->getMessage(),
- 'error' => $e->getMessage()
- ], 500);
- }
- }
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'message' => 'Configuration cache cleared successfully',
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'SettingsController: Cache clear failed',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Cache clear failed: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end clearCache()
/**
* Manually trigger configuration import.
@@ -771,40 +897,52 @@ public function clearCache(): JSONResponse
public function manualImport(): JSONResponse
{
try {
- $params = $this->request->getParams();
- $forceImport = isset($params['force']) && $params['force'] === true;
-
- $this->logger->info('SettingsController: Starting manual import', [
- 'force' => $forceImport
- ]);
-
+ $params = $this->request->getParams();
+ $forceImport = isset($params['force']) === true && $params['force'] === true;
+
+ $this->logger->info(
+ 'SettingsController: Starting manual import',
+ [
+ 'force' => $forceImport,
+ ]
+ );
+
$result = $this->settingsService->manualImport($forceImport);
-
- $this->logger->info('SettingsController: Manual import completed', [
- 'success' => $result['success'],
- 'message' => $result['message'] ?? 'No message'
- ]);
-
- // Add timestamp for cache busting
+
+ $this->logger->info(
+ 'SettingsController: Manual import completed',
+ [
+ 'success' => $result['success'],
+ 'message' => $result['message'] ?? 'No message',
+ ]
+ );
+
+ // Add timestamp for cache busting.
$result['timestamp'] = time();
-
- if ($result['success']) {
+
+ if ($result['success'] === true) {
return new JSONResponse($result);
} else {
return new JSONResponse($result, 400);
}
} catch (\Exception $e) {
- $this->logger->error('SettingsController: Manual import failed', [
- 'exception_message' => $e->getMessage(),
- 'exception' => $e
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Import failed: ' . $e->getMessage(),
- 'error' => $e->getMessage(),
- 'timestamp' => time()
- ], 500);
- }
+ $this->logger->error(
+ 'SettingsController: Manual import failed',
+ [
+ 'exception_message' => $e->getMessage(),
+ 'exception' => $e,
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Import failed: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
+ 'timestamp' => time(),
+ ],
+ 500
+ );
+ }//end try
}//end manualImport()
/**
@@ -818,56 +956,64 @@ public function forceUpdate(): JSONResponse
{
try {
$this->logger->info('SettingsController: Starting force update');
-
+
$result = $this->settingsService->forceUpdate();
-
- $this->logger->info('SettingsController: Force update completed', [
- 'success' => $result['success'],
- 'message' => $result['message'] ?? 'No message'
- ]);
-
- // Add timestamp for cache busting
+
+ $this->logger->info(
+ 'SettingsController: Force update completed',
+ [
+ 'success' => $result['success'],
+ 'message' => $result['message'] ?? 'No message',
+ ]
+ );
+
+ // Add timestamp for cache busting.
$result['timestamp'] = time();
-
- // Ensure result is JSON serializable by removing any potential circular references
+
+ // Ensure result is JSON serializable by removing any potential circular references.
$jsonResult = json_decode(json_encode($result), true);
if (json_last_error() !== JSON_ERROR_NONE) {
- $this->logger->error('SettingsController: JSON serialization error', [
- 'json_error' => json_last_error_msg(),
- 'result_keys' => array_keys($result)
- ]);
- // Return a simplified response if serialization fails
- return new JSONResponse([
- 'success' => $result['success'] ?? false,
- 'message' => $result['message'] ?? 'Force update completed but response serialization failed',
- 'timestamp' => time()
- ], 200);
+ $this->logger->error(
+ 'SettingsController: JSON serialization error',
+ [
+ 'json_error' => json_last_error_msg(),
+ 'result_keys' => array_keys($result),
+ ]
+ );
+ // Return a simplified response if serialization fails.
+ return new JSONResponse(
+ [
+ 'success' => $result['success'] ?? false,
+ 'message' => $result['message'] ?? 'Force update completed but response serialization failed',
+ 'timestamp' => time(),
+ ],
+ 200
+ );
}
-
- // Always return 200 since the operation completed, even if configuration needs attention
+
+ // Always return 200 since the operation completed, even if configuration needs attention.
return new JSONResponse($jsonResult, 200);
-
} catch (\Throwable $e) {
- $this->logger->error('SettingsController: Force update failed', [
- 'exception_message' => $e->getMessage(),
- 'exception_class' => get_class($e),
- 'exception_trace' => $e->getTraceAsString()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Force update failed: ' . $e->getMessage(),
- 'error' => $e->getMessage(),
- 'timestamp' => time()
- ], 500);
- }
+ $this->logger->error(
+ 'SettingsController: Force update failed',
+ [
+ 'exception_message' => $e->getMessage(),
+ 'exception_class' => get_class($e),
+ 'exception_trace' => $e->getTraceAsString(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Force update failed: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
+ 'timestamp' => time(),
+ ],
+ 500
+ );
+ }//end try
}//end forceUpdate()
-
-
-
-
-
-
/**
* Consolidated auto-configuration that handles everything
*
@@ -881,1135 +1027,1409 @@ public function forceUpdate(): JSONResponse
public function consolidatedAutoConfigure(): JSONResponse
{
try {
- // Get force parameter from request
+ // Get force parameter from request.
$force = $this->request->getParam('force', false);
-
- // Delegate all business logic to the service
+
+ // Delegate all business logic to the service.
$results = $this->settingsService->performConsolidatedAutoConfiguration($force);
-
- // Determine HTTP status based on results
- if (!$results['success']) {
- $httpStatus = !empty($results['errors']) ? 207 : 500; // Multi-status or Server Error
+
+ // Determine HTTP status based on results.
+ if ($results['success'] === false) {
+ // Multi-status or Server Error.
+ if (empty($results['errors']) === false) {
+ $httpStatus = 207;
+ } else {
+ $httpStatus = 500;
+ }
} else {
- $httpStatus = 200; // Success
+ // Success.
+ $httpStatus = 200;
}
-
+
return new JSONResponse($results, $httpStatus);
-
- } catch (\Exception $e) {
- $this->logger->error('SettingsController: Consolidated auto-configuration failed', [
- 'exception_message' => $e->getMessage(),
- 'exception' => $e
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Auto-configuration failed: ' . $e->getMessage(),
- 'error' => $e->getMessage(),
- 'timestamp' => time()
- ], 500);
- }
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'SettingsController: Consolidated auto-configuration failed',
+ [
+ 'exception_message' => $e->getMessage(),
+ 'exception' => $e,
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Auto-configuration failed: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
+ 'timestamp' => time(),
+ ],
+ 500
+ );
+ }//end try
}//end consolidatedAutoConfigure()
-
-
-
-
-
-
/**
- * Get current progress for an operation
+ * Get current progress for an operation.
+ *
+ * @param string $operationId The operation ID to get progress for.
+ *
+ * @return JSONResponse The current progress data.
*
* @NoAdminRequired
* @NoCSRFRequired
- *
- * @param string $operationId The operation ID to get progress for
- *
- * @return JSONResponse The current progress data
*/
public function getProgress(string $operationId): JSONResponse
{
try {
$progress = $this->progressTracker->getProgress($operationId);
-
+
if ($progress === null) {
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Operation not found',
- 'error' => 'OPERATION_NOT_FOUND'
- ], 404);
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Operation not found',
+ 'error' => 'OPERATION_NOT_FOUND',
+ ],
+ 404
+ );
}
- return new JSONResponse([
- 'success' => true,
- 'progress' => $progress
- ]);
-
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'progress' => $progress,
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('Failed to get progress', [
- 'operation_id' => $operationId,
- 'error' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to get progress',
+ [
+ 'operation_id' => $operationId,
+ 'error' => $e->getMessage(),
+ ]
+ );
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get progress: ' . $e->getMessage(),
- 'error' => $e->getMessage()
- ], 500);
- }
- }
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get progress: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getProgress()
/**
- * Stream progress updates using Server-Sent Events
+ * Stream progress updates using Server-Sent Events.
+ *
+ * @param string $operationId The operation ID to stream progress for.
+ *
+ * @return Response SSE stream response.
*
* @NoAdminRequired
* @NoCSRFRequired
- *
- * @param string $operationId The operation ID to stream progress for
- *
- * @return Response SSE stream response
*/
public function streamProgress(string $operationId): Response
{
- // Set headers for Server-Sent Events
+ // Set headers for Server-Sent Events.
$response = new class($operationId, $this->progressTracker, $this->logger) extends Response {
+ /**
+ * Constructor for the SSE response.
+ *
+ * @param string $operationId The operation ID to stream.
+ * @param ProgressTracker $progressTracker The progress tracker service.
+ * @param LoggerInterface $logger The logger instance.
+ */
public function __construct(
private string $operationId,
private ProgressTracker $progressTracker,
private LoggerInterface $logger
) {
parent::__construct();
- $this->addHeader('Content-Type', 'text/event-stream');
- $this->addHeader('Cache-Control', 'no-cache');
- $this->addHeader('Connection', 'keep-alive');
- $this->addHeader('Access-Control-Allow-Origin', '*');
- $this->addHeader('Access-Control-Allow-Headers', 'Cache-Control');
- }
-
+ $this->addHeader(name: 'Content-Type', value: 'text/event-stream');
+ $this->addHeader(name: 'Cache-Control', value: 'no-cache');
+ $this->addHeader(name: 'Connection', value: 'keep-alive');
+ $this->addHeader(name: 'Access-Control-Allow-Origin', value: '*');
+ $this->addHeader(name: 'Access-Control-Allow-Headers', value: 'Cache-Control');
+ }//end __construct()
+
+ /**
+ * Render the SSE stream.
+ *
+ * @return string Empty string (output is streamed directly).
+ */
public function render(): string
{
- // Enable output buffering and turn off compression
- if (ob_get_level()) {
+ // Enable output buffering and turn off compression.
+ if (ob_get_level() !== 0) {
ob_end_clean();
}
+
ob_implicit_flush(true);
- // Stream progress updates
+ // Stream progress updates.
$lastProgress = null;
- $maxAttempts = 300; // 5 minutes with 1-second intervals
+ $maxAttempts = 300;
+ // 5 minutes with 1-second intervals.
$attempts = 0;
while ($attempts < $maxAttempts) {
try {
$progress = $this->progressTracker->getProgress($this->operationId);
-
+
if ($progress === null) {
- // Operation not found, send error and close
+ // Operation not found, send error and close.
echo "event: error\n";
- echo "data: " . json_encode(['error' => 'Operation not found']) . "\n\n";
+ echo "data: ".json_encode(['error' => 'Operation not found'])."\n\n";
break;
}
- // Only send update if progress changed
+ // Only send update if progress changed.
if ($progress !== $lastProgress) {
echo "event: progress\n";
- echo "data: " . json_encode($progress) . "\n\n";
+ echo "data: ".json_encode($progress)."\n\n";
$lastProgress = $progress;
-
- // If operation completed, send final event and close
+
+ // If operation completed, send final event and close.
if ($progress['phase'] === 'completed') {
echo "event: completed\n";
- echo "data: " . json_encode($progress) . "\n\n";
+ echo "data: ".json_encode($progress)."\n\n";
break;
}
}
- // Send heartbeat every 10 seconds
+ // Send heartbeat every 10 seconds.
if ($attempts % 10 === 0) {
echo "event: heartbeat\n";
- echo "data: " . json_encode(['timestamp' => time()]) . "\n\n";
+ echo "data: ".json_encode(['timestamp' => time()])."\n\n";
}
flush();
sleep(1);
$attempts++;
-
} catch (\Exception $e) {
- $this->logger->error('Progress streaming error', [
- 'operation_id' => $this->operationId,
- 'error' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Progress streaming error',
+ [
+ 'operation_id' => $this->operationId,
+ 'error' => $e->getMessage(),
+ ]
+ );
echo "event: error\n";
- echo "data: " . json_encode(['error' => $e->getMessage()]) . "\n\n";
+ echo "data: ".json_encode(['error' => $e->getMessage()])."\n\n";
break;
- }
- }
+ }//end try
+ }//end while
- // Send final close event
+ // Send final close event.
echo "event: close\n";
- echo "data: " . json_encode(['reason' => 'Stream ended']) . "\n\n";
+ echo "data: ".json_encode(['reason' => 'Stream ended'])."\n\n";
flush();
return '';
- }
+ }//end render()
};
return $response;
- }
+ }//end streamProgress()
/**
- * Import ArchiMate file
+ * Import ArchiMate file.
+ *
+ * @return JSONResponse Result of the import operation with progress tracking.
*
* @NoAdminRequired
* @NoCSRFRequired
- *
- * @return JSONResponse Result of the import operation with progress tracking
*/
public function importArchiMate(): JSONResponse
{
try {
- // Increase memory limit for large imports
+ // Increase memory limit for large imports.
ini_set('memory_limit', '4096M');
- $this->logger->info('Memory limit increased for import', [
- 'old_limit' => ini_get('memory_limit'),
- 'new_limit' => '4096M'
- ]);
- // Get JSON data from request body
+ $this->logger->info(
+ 'Memory limit increased for import',
+ [
+ 'old_limit' => ini_get('memory_limit'),
+ 'new_limit' => '4096M',
+ ]
+ );
+ // Get JSON data from request body.
$rawInput = file_get_contents('php://input');
- $data = json_decode($rawInput, true);
-
- // Enhanced debug logging
- $this->logger->info('ArchiMate import request received', [
- 'rawInput' => $rawInput,
- 'decodedData' => $data,
- 'jsonError' => json_last_error_msg(),
- 'contentType' => $this->request->getHeader('Content-Type'),
- 'isMultipart' => strpos($this->request->getHeader('Content-Type'), 'multipart/form-data') !== false,
- 'requestMethod' => $this->request->getMethod(),
- 'userAgent' => $this->request->getHeader('User-Agent'),
- 'xRequestedWith' => $this->request->getHeader('X-Requested-With'),
- '_FILES' => $_FILES,
- '_POST' => $_POST,
- 'requestParams' => $this->request->getParams()
- ]);
-
- // Check if a file was uploaded (traditional file upload)
+ $data = json_decode($rawInput, true);
+
+ $contentType = $this->request->getHeader('Content-Type');
+ $isMultipart = strpos(haystack: $contentType, needle: 'multipart/form-data') !== false;
+
+ // Enhanced debug logging.
+ $this->logger->info(
+ 'ArchiMate import request received',
+ [
+ 'rawInput' => $rawInput,
+ 'decodedData' => $data,
+ 'jsonError' => json_last_error_msg(),
+ 'contentType' => $contentType,
+ 'isMultipart' => $isMultipart,
+ 'requestMethod' => $this->request->getMethod(),
+ 'userAgent' => $this->request->getHeader('User-Agent'),
+ 'xRequestedWith' => $this->request->getHeader('X-Requested-With'),
+ '_FILES' => $_FILES,
+ '_POST' => $_POST,
+ 'requestParams' => $this->request->getParams(),
+ ]
+ );
+
+ // Check if a file was uploaded (traditional file upload).
$uploadedFiles = $this->request->getUploadedFile('archiMateFile');
-
- // Also check $_FILES directly as fallback
+
+ // Also check $_FILES directly as fallback.
$filesArray = $_FILES['archiMateFile'] ?? null;
-
- $this->logger->info('File upload detection detailed', [
- 'uploadedFiles' => $uploadedFiles,
- 'filesArray' => $filesArray,
- 'requestMethod' => $this->request->getMethod(),
- 'contentType' => $this->request->getHeader('Content-Type'),
- 'hasUploadedFiles' => !empty($uploadedFiles),
- 'hasFilesArray' => !empty($filesArray),
- 'uploadedFilesType' => gettype($uploadedFiles),
- 'filesArrayType' => gettype($filesArray),
- 'allFilesKeys' => array_keys($_FILES ?? [])
- ]);
-
- if ($uploadedFiles || $filesArray) {
- // Use $_FILES as fallback if getUploadedFile doesn't work
- $fileData = $uploadedFiles ?: $filesArray;
-
- // Handle file upload
+
+ $hasUploadedFiles = empty($uploadedFiles) === false;
+ $hasFilesArray = empty($filesArray) === false;
+
+ $this->logger->info(
+ 'File upload detection detailed',
+ [
+ 'uploadedFiles' => $uploadedFiles,
+ 'filesArray' => $filesArray,
+ 'requestMethod' => $this->request->getMethod(),
+ 'contentType' => $contentType,
+ 'hasUploadedFiles' => $hasUploadedFiles,
+ 'hasFilesArray' => $hasFilesArray,
+ 'uploadedFilesType' => gettype($uploadedFiles),
+ 'filesArrayType' => gettype($filesArray),
+ 'allFilesKeys' => array_keys($_FILES ?? []),
+ ]
+ );
+
+ if ($hasUploadedFiles === true || $hasFilesArray === true) {
+ // Use $_FILES as fallback if getUploadedFile doesn't work.
+ if ($uploadedFiles !== null) {
+ $fileData = $uploadedFiles;
+ } else {
+ $fileData = $filesArray;
+ }
+
+ // Handle file upload.
$options = [
'updateExisting' => $this->request->getParam('updateExisting', 'true') === 'true',
'deleteOrphaned' => $this->request->getParam('deleteOrphaned', 'false') === 'true',
- 'preserveIds' => $this->request->getParam('preserveIds', 'true') === 'true',
+ 'preserveIds' => $this->request->getParam('preserveIds', 'true') === 'true',
'processingMode' => $this->request->getParam('processingMode', 'speed'),
- 'filePath' => $fileData['tmp_name'],
- 'fileName' => $fileData['name'],
- 'fileSize' => $fileData['size'] ?? filesize($fileData['tmp_name']),
- 'mimeType' => $fileData['type'] ?? 'text/xml'
+ 'filePath' => $fileData['tmp_name'],
+ 'fileName' => $fileData['name'],
+ 'fileSize' => $fileData['size'] ?? filesize($fileData['tmp_name']),
+ 'mimeType' => $fileData['type'] ?? 'text/xml',
];
-
- $this->logger->info('File upload detected', ['options' => $options]);
- } elseif ($data && isset($data['file_path'])) {
- // Handle file path from JSON payload
+
+ $this->logger->info('File upload detected.', ['options' => $options]);
+ } else if ($data !== null && isset($data['file_path']) === true) {
+ // Handle file path from JSON payload.
+ if (file_exists($data['file_path']) === true) {
+ $fileSize = filesize($data['file_path']);
+ } else {
+ $fileSize = 0;
+ }
+
$options = [
'updateExisting' => $data['updateExisting'] ?? true,
'deleteOrphaned' => $data['deleteOrphaned'] ?? false,
- 'preserveIds' => $data['preserveIds'] ?? true,
+ 'preserveIds' => $data['preserveIds'] ?? true,
'processingMode' => $data['processingMode'] ?? 'speed',
- 'filePath' => $data['file_path'],
- 'fileName' => $data['fileName'] ?? basename($data['file_path']),
- 'fileSize' => $data['fileSize'] ?? (file_exists($data['file_path']) ? filesize($data['file_path']) : 0),
- 'mimeType' => $data['mimeType'] ?? 'text/xml'
+ 'filePath' => $data['file_path'],
+ 'fileName' => $data['fileName'] ?? basename($data['file_path']),
+ 'fileSize' => $data['fileSize'] ?? $fileSize,
+ 'mimeType' => $data['mimeType'] ?? 'text/xml',
];
-
- $this->logger->info('JSON payload detected', ['options' => $options]);
- } else {
- $this->logger->error('No file uploaded or file path provided - DETAILED DEBUG', [
- 'uploadedFiles' => $uploadedFiles,
- 'filesArray' => $filesArray,
- 'data' => $data,
- 'rawInput' => $rawInput,
- 'contentType' => $this->request->getHeader('Content-Type'),
- 'isMultipart' => strpos($this->request->getHeader('Content-Type'), 'multipart/form-data') !== false,
- 'requestMethod' => $this->request->getMethod(),
- '_FILES_DEBUG' => $_FILES,
- '_POST_DEBUG' => $_POST,
- 'requestParams' => $this->request->getParams(),
- 'userAgent' => $this->request->getHeader('User-Agent'),
- 'xRequestedWith' => $this->request->getHeader('X-Requested-With')
- ]);
-
- return new JSONResponse([
- 'success' => false,
- 'message' => 'No ArchiMate file uploaded or file path provided',
- 'error' => 'NO_FILE_UPLOADED_OR_PATH',
- 'debug' => [
- 'contentType' => $this->request->getHeader('Content-Type'),
- 'isMultipart' => strpos($this->request->getHeader('Content-Type'), 'multipart/form-data') !== false,
- 'filesKeys' => array_keys($_FILES ?? [])
- ]
- ], 400);
- }
- // OPTIMIZATION: Use optimized method if available or if explicitly requested
+ $this->logger->info('JSON payload detected.', ['options' => $options]);
+ } else {
+ $this->logger->error(
+ 'No file uploaded or file path provided — DETAILED DEBUG',
+ [
+ 'uploadedFiles' => $uploadedFiles,
+ 'filesArray' => $filesArray,
+ 'data' => $data,
+ 'rawInput' => $rawInput,
+ 'contentType' => $contentType,
+ 'isMultipart' => $isMultipart,
+ 'requestMethod' => $this->request->getMethod(),
+ '_FILES_DEBUG' => $_FILES,
+ '_POST_DEBUG' => $_POST,
+ 'requestParams' => $this->request->getParams(),
+ 'userAgent' => $this->request->getHeader('User-Agent'),
+ 'xRequestedWith' => $this->request->getHeader('X-Requested-With'),
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'No ArchiMate file uploaded or file path provided',
+ 'error' => 'NO_FILE_UPLOADED_OR_PATH',
+ 'debug' => [
+ 'contentType' => $contentType,
+ 'isMultipart' => $isMultipart,
+ 'filesKeys' => array_keys($_FILES ?? []),
+ ],
+ ],
+ 400
+ );
+ }//end if
+
+ // OPTIMIZATION: Use optimized method if available or if explicitly requested.
$useOptimized = $this->request->getParam('useOptimized', 'true') === 'true';
- if ($useOptimized && method_exists($this->archiMateService, 'importArchiMateFileFromPathOptimized')) {
- $this->logger->info('Using OPTIMIZED ArchiMate import method');
+ $hasOptimized = method_exists($this->archiMateService, 'importArchiMateFileFromPathOptimized');
+ if ($useOptimized === true && $hasOptimized === true) {
+ $this->logger->info('Using OPTIMIZED ArchiMate import method.');
$result = $this->archiMateService->importArchiMateFileFromPathOptimized($options);
} else {
- $this->logger->info('Using STANDARD ArchiMate import method');
+ $this->logger->info('Using STANDARD ArchiMate import method.');
$result = $this->archiMateService->importArchiMateFileFromPath($options);
}
return new JSONResponse($result);
-
} catch (\Exception $e) {
- $this->logger->error('ArchiMate import failed', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'ArchiMate import failed',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
- // Determine appropriate HTTP status code based on error type
- $statusCode = $this->getHttpStatusForException($e);
+ // Determine appropriate HTTP status code based on error type.
+ $statusCode = $this->getHttpStatusForException(e: $e);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Import failed: ' . $e->getMessage(),
- 'error' => $e->getMessage()
- ], $statusCode);
- }
- }
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Import failed: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
+ ],
+ $statusCode
+ );
+ }//end try
+ }//end importArchiMate()
/**
- * Export to ArchiMate format - returns file directly for download
+ * Export to ArchiMate format - returns file directly for download.
+ *
+ * @return Response File download response or JSON error response.
*
* @NoAdminRequired
* @NoCSRFRequired
- *
- * @return Response File download response or JSON error response
*/
public function exportArchiMate(): Response
{
try {
- // Get JSON data from request parameters or body
+ // Get JSON data from request parameters or body.
$rawInput = file_get_contents('php://input');
- $data = json_decode($rawInput, true);
-
+ $data = json_decode($rawInput, true);
+
if (json_last_error() !== JSON_ERROR_NONE) {
- // Fallback to request parameters if JSON decode fails
+ // Fallback to request parameters if JSON decode fails.
$data = [
- 'organization' => $this->request->getParam('organization', null)
+ 'organization' => $this->request->getParam('organization', null),
];
}
- // Simple organization filter - only parameter we support
+ // Simple organization filter - only parameter we support.
$organization = $data['organization'] ?? null;
- // Call export service with simplified parameters
+ // Call export service with simplified parameters.
$result = $this->archiMateService->exportToArchiMate($organization);
- // Check if export was successful
- if (!$result['success']) {
- // Determine appropriate status code based on error message
- $statusCode = $this->getHttpStatusForErrorMessage($result['error'] ?? 'Export failed');
-
- return new JSONResponse([
- 'success' => false,
- 'message' => $result['error'] ?? 'Export failed',
- 'error' => $result['error'] ?? 'EXPORT_FAILED'
- ], $statusCode);
+ // Check if export was successful.
+ if ($result['success'] === false) {
+ // Determine appropriate status code based on error message.
+ $statusCode = $this->getHttpStatusForErrorMessage(message: $result['error'] ?? 'Export failed');
+
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => $result['error'] ?? 'Export failed',
+ 'error' => $result['error'] ?? 'EXPORT_FAILED',
+ ],
+ $statusCode
+ );
}
- // Return the XML file directly for download
- $fileName = $result['file_name'] ?? 'archimate_export_' . date('Y-m-d_H-i-s') . '.xml';
+ // Return the XML file directly for download.
+ $fileName = $result['file_name'] ?? 'archimate_export_'.date('Y-m-d_H-i-s').'.xml';
$xmlContent = $result['xml'] ?? ' ';
-
- // Always return XML format
+
+ // Always return XML format.
$contentType = 'application/xml';
- // Create direct download response
+ // Create direct download response.
$response = new class($xmlContent) extends Response {
- public function __construct(private string $content) {
- parent::__construct();
- }
-
- public function render(): string {
+ /**
+ * Constructor for the download response.
+ *
+ * @param string $content The XML content to return.
+ */
+ public function __construct(private string $content)
+ {
+ parent::__construct();
+ }//end __construct()
+
+ /**
+ * Render the response content.
+ *
+ * @return string The response content.
+ */
+ public function render(): string
+ {
return $this->content;
- }
+ }//end render()
};
-
+
$response->setStatus(200);
$response->addHeader('Content-Type', $contentType);
- $response->addHeader('Content-Disposition', 'attachment; filename="' . $fileName . '"');
- $response->addHeader('Content-Length', (string)strlen($xmlContent));
+ $response->addHeader('Content-Disposition', 'attachment; filename="'.$fileName.'"');
+ $response->addHeader('Content-Length', (string) strlen($xmlContent));
$response->addHeader('Cache-Control', 'no-cache');
-
- $this->logger->info('ArchiMate export completed', [
- 'fileName' => $fileName,
- 'size' => strlen($xmlContent),
- 'objects_exported' => $result['statistics']['objects_exported'] ?? 0
- ]);
- return $response;
+ $this->logger->info(
+ 'ArchiMate export completed',
+ [
+ 'fileName' => $fileName,
+ 'size' => strlen($xmlContent),
+ 'objects_exported' => $result['statistics']['objects_exported'] ?? 0,
+ ]
+ );
+ return $response;
} catch (\Exception $e) {
- $this->logger->error('ArchiMate export failed', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'ArchiMate export failed',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
- // Determine appropriate HTTP status code based on error type
- $statusCode = $this->getHttpStatusForException($e);
+ // Determine appropriate HTTP status code based on error type.
+ $statusCode = $this->getHttpStatusForException(e: $e);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Export failed: ' . $e->getMessage(),
- 'error' => $e->getMessage()
- ], $statusCode);
- }
- }
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Export failed: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
+ ],
+ $statusCode
+ );
+ }//end try
+ }//end exportArchiMate()
/**
- * Export organization-specific ArchiMate file with enriched views
+ * Export organization-specific ArchiMate file with enriched views.
+ *
+ * @param string $organizationUuid The organization UUID to export for.
+ *
+ * @return Response File download response or JSON error response.
*
* @NoAdminRequired
* @NoCSRFRequired
- *
- * @return Response File download response or JSON error response
*/
public function exportOrgArchiMate(string $organizationUuid): Response
{
try {
- // Read boolean query parameters
- $modules = $this->request->getParam('modules', 'true') === 'true';
+ // Read boolean query parameters.
+ $modules = $this->request->getParam('modules', 'true') === 'true';
$deelnames = $this->request->getParam('deelnames', 'false') === 'true';
- $gebruik = $this->request->getParam('gebruik', 'false') === 'true';
+ $gebruik = $this->request->getParam('gebruik', 'false') === 'true';
$options = [
- 'modules' => $modules,
+ 'modules' => $modules,
'deelnames' => $deelnames,
- 'gebruik' => $gebruik,
+ 'gebruik' => $gebruik,
];
- $result = $this->archiMateService->exportOrgArchiMate($organizationUuid, $options);
+ $result = $this->archiMateService->exportOrgArchiMate(
+ organizationUuid: $organizationUuid,
+ options: $options
+ );
- if (!$result['success']) {
+ if ($result['success'] === false) {
$statusCode = 500;
- if (str_contains($result['error'] ?? '', 'not found')) {
+ if (str_contains(haystack: ($result['error'] ?? '') === true, needle: 'not found') === true) {
$statusCode = 404;
}
- return new JSONResponse([
- 'success' => false,
- 'message' => $result['error'] ?? 'Export failed',
- 'error' => $result['error'] ?? 'EXPORT_FAILED'
- ], $statusCode);
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => $result['error'] ?? 'Export failed',
+ 'error' => $result['error'] ?? 'EXPORT_FAILED',
+ ],
+ $statusCode
+ );
}
- $fileName = $result['file_name'] ?? 'archimate_org_export_' . date('Y-m-d_H-i-s') . '.xml';
+ $fileName = $result['file_name'] ?? 'archimate_org_export_'.date('Y-m-d_H-i-s').'.xml';
$xmlContent = $result['xml'] ?? ' ';
$response = new class($xmlContent) extends Response {
- public function __construct(private string $content) {
- parent::__construct();
- }
-
- public function render(): string {
+ /**
+ * Constructor for the org download response.
+ *
+ * @param string $content The XML content to return.
+ */
+ public function __construct(private string $content)
+ {
+ parent::__construct();
+ }//end __construct()
+
+ /**
+ * Render the response content.
+ *
+ * @return string The response content.
+ */
+ public function render(): string
+ {
return $this->content;
- }
+ }//end render()
};
$response->setStatus(200);
$response->addHeader('Content-Type', 'application/xml');
- $response->addHeader('Content-Disposition', 'attachment; filename="' . $fileName . '"');
- $response->addHeader('Content-Length', (string)strlen($xmlContent));
+ $response->addHeader('Content-Disposition', 'attachment; filename="'.$fileName.'"');
+ $response->addHeader('Content-Length', (string) strlen($xmlContent));
$response->addHeader('Cache-Control', 'no-cache');
return $response;
-
} catch (\Exception $e) {
- $this->logger->error('Organization ArchiMate export failed', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'Organization ArchiMate export failed',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
- $statusCode = $this->getHttpStatusForException($e);
+ $statusCode = $this->getHttpStatusForException(e: $e);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Export failed: ' . $e->getMessage(),
- 'error' => $e->getMessage()
- ], $statusCode);
- }
- }
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Export failed: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
+ ],
+ $statusCode
+ );
+ }//end try
+ }//end exportOrgArchiMate()
/**
- * Download ArchiMate file
+ * Download ArchiMate file.
*
- * @NoAdminRequired
- * @NoCSRFRequired
+ * @param string $fileName The filename to download.
*
- * @param string $fileName The filename to download
+ * @return Response File download response.
*
- * @return Response File download response
+ * @NoAdminRequired
+ * @NoCSRFRequired
*/
public function downloadArchiMate(string $fileName): Response
{
try {
- // Security: validate filename to prevent path traversal
- if (strpos($fileName, '..') !== false || strpos($fileName, '/') !== false) {
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Invalid filename',
- 'error' => 'INVALID_FILENAME'
- ], 400);
+ // Security: validate filename to prevent path traversal.
+ if (strpos(haystack: $fileName, needle: '..') !== false
+ || strpos(haystack: $fileName, needle: '/') !== false
+ ) {
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Invalid filename',
+ 'error' => 'INVALID_FILENAME',
+ ],
+ 400
+ );
}
- // Get user folder
+ // Get user folder.
$userSession = $this->container->get(\OCP\IUserSession::class);
- $rootFolder = $this->container->get(\OCP\Files\IRootFolder::class);
- $userFolder = $rootFolder->getUserFolder($userSession->getUser()->getUID());
-
- // Check if file exists
- if (!$userFolder->nodeExists($fileName)) {
- return new JSONResponse([
- 'success' => false,
- 'message' => 'File not found',
- 'error' => 'FILE_NOT_FOUND'
- ], 404);
+ $rootFolder = $this->container->get(\OCP\Files\IRootFolder::class);
+ $userFolder = $rootFolder->getUserFolder($userSession->getUser()->getUID());
+
+ // Check if file exists.
+ if ($userFolder->nodeExists($fileName) === false) {
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'File not found',
+ 'error' => 'FILE_NOT_FOUND',
+ ],
+ 404
+ );
}
$file = $userFolder->get($fileName);
-
- if (!($file instanceof \OCP\Files\File)) {
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Invalid file type',
- 'error' => 'INVALID_FILE_TYPE'
- ], 400);
+
+ if (($file instanceof \OCP\Files\File) === false) {
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Invalid file type',
+ 'error' => 'INVALID_FILE_TYPE',
+ ],
+ 400
+ );
}
- // Determine content type based on file extension
- $extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
- $contentType = match($extension) {
+ // Determine content type based on file extension.
+ $extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
+ $contentType = match ($extension) {
'xml', 'archimate' => 'application/xml',
'json' => 'application/json',
default => 'application/octet-stream'
};
- // Create download response
+ // Create download response.
$response = new StreamResponse($file->fopen('r'));
$response->addHeader('Content-Type', $contentType);
- $response->addHeader('Content-Disposition', 'attachment; filename="' . $fileName . '"');
- $response->addHeader('Content-Length', (string)$file->getSize());
+ $response->addHeader('Content-Disposition', 'attachment; filename="'.$fileName.'"');
+ $response->addHeader('Content-Length', (string) $file->getSize());
return $response;
-
} catch (\Exception $e) {
- $this->logger->error('ArchiMate download failed', [
- 'fileName' => $fileName,
- 'error' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'ArchiMate download failed',
+ [
+ 'fileName' => $fileName,
+ 'error' => $e->getMessage(),
+ ]
+ );
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Download failed: ' . $e->getMessage(),
- 'error' => $e->getMessage()
- ], 500);
- }
- }
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Download failed: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end downloadArchiMate()
- // ========================================================================
- // EMAIL MANAGEMENT METHODS
- // ========================================================================
+ // ===.
+ // EMAIL MANAGEMENT METHODS.
+ // ===.
/**
* Test email connection (separate from sending test email)
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Test connection result
*/
public function testEmailConnection(): JSONResponse
{
$this->logger->info('SoftwareCatalog: Email connection test endpoint called');
-
+
try {
- $data = $this->request->getParams();
+ $data = $this->request->getParams();
$emailSettings = $data['emailSettings'] ?? $data ?? [];
-
- $this->logger->info('SoftwareCatalog: Email connection test request data', [
- 'has_email_settings' => !empty($emailSettings),
- 'transport_type' => $emailSettings['transportType'] ?? 'not specified'
- ]);
-
- // Call the settings service to test the connection (without sending email)
+
+ $this->logger->info(
+ 'SoftwareCatalog: Email connection test request data',
+ [
+ 'has_email_settings' => empty($emailSettings) === false,
+ 'transport_type' => $emailSettings['transportType'] ?? 'not specified',
+ ]
+ );
+
+ // Call the settings service to test the connection (without sending email).
$result = $this->settingsService->testEmailConnection($emailSettings);
-
- $this->logger->info('SoftwareCatalog: Email connection test result from service', [
- 'success' => $result['success'],
- 'message' => $result['message'] ?? 'no message'
- ]);
-
- return new JSONResponse([
- 'success' => $result['success'],
- 'message' => $result['message'],
- 'details' => $result['details'] ?? null
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('SoftwareCatalog: Failed to test email connection', [
- 'exception_class' => get_class($e),
- 'exception_message' => $e->getMessage(),
- 'requestData' => $this->request->getParams()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to test email connection: ' . $e->getMessage()
- ], 500);
- }
- }
+
+ $this->logger->info(
+ 'SoftwareCatalog: Email connection test result from service',
+ [
+ 'success' => $result['success'],
+ 'message' => $result['message'] ?? 'no message',
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => $result['success'],
+ 'message' => $result['message'],
+ 'details' => $result['details'] ?? null,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'SoftwareCatalog: Failed to test email connection',
+ [
+ 'exception_class' => get_class($e),
+ 'exception_message' => $e->getMessage(),
+ 'requestData' => $this->request->getParams(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to test email connection: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end testEmailConnection()
/**
* Get email settings
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Current email settings
*/
public function getEmailSettings(): JSONResponse
{
try {
$emailSettings = $this->settingsService->getEmailSettings();
-
- return new JSONResponse([
- 'success' => true,
- 'emailSettings' => $emailSettings
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to get email settings', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get email settings: ' . $e->getMessage()
- ], 500);
- }
- }
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'emailSettings' => $emailSettings,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to get email settings',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get email settings: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getEmailSettings()
/**
* Update email settings
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Update result
*/
public function updateEmailSettings(): JSONResponse
{
try {
- $data = $this->request->getParams();
+ $data = $this->request->getParams();
$emailSettings = $data['emailSettings'] ?? $data;
-
+
$result = $this->settingsService->updateEmailSettings($emailSettings);
-
- return new JSONResponse([
- 'success' => $result['success'],
- 'message' => $result['message'] ?? 'Email settings updated successfully',
- 'emailSettings' => $result['emailSettings'] ?? null
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to update email settings', [
- 'exception' => $e->getMessage(),
- 'requestData' => $this->request->getParams()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to update email settings: ' . $e->getMessage()
- ], 500);
- }
- }
- // ========================================================================
- // EMAIL TEMPLATE METHODS
- // ========================================================================
+ return new JSONResponse(
+ [
+ 'success' => $result['success'],
+ 'message' => $result['message'] ?? 'Email settings updated successfully',
+ 'emailSettings' => $result['emailSettings'] ?? null,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to update email settings',
+ [
+ 'exception' => $e->getMessage(),
+ 'requestData' => $this->request->getParams(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to update email settings: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end updateEmailSettings()
+
+ // ===.
+ // EMAIL TEMPLATE METHODS.
+ // ===.
/**
- * Get all email templates
+ * Get all email templates.
+ *
+ * @return JSONResponse List of available templates.
*
* @NoAdminRequired
* @NoCSRFRequired
- *
- * @return JSONResponse List of available templates
*/
public function getEmailTemplates(): JSONResponse
{
try {
- // Delegate all business logic to service
+ // Delegate all business logic to service.
$templates = $this->settingsService->getAllEmailTemplates();
-
- return new JSONResponse([
- 'success' => true,
- 'templates' => $templates
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to get email templates', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get email templates: ' . $e->getMessage()
- ], 500);
- }
- }
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'templates' => $templates,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to get email templates',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get email templates: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getEmailTemplates()
/**
- * Get specific email template
+ * Get specific email template.
+ *
+ * @param string $templateName Template name.
+ *
+ * @return JSONResponse Template content.
*
* @NoAdminRequired
* @NoCSRFRequired
- *
- * @param string $templateName Template name
- * @return JSONResponse Template content
*/
public function getEmailTemplate(string $templateName): JSONResponse
{
try {
$template = $this->settingsService->getEmailTemplate($templateName);
-
- return new JSONResponse([
- 'success' => true,
- 'template' => $template,
- 'templateName' => $templateName
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error("Failed to get email template {$templateName}", [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => "Failed to get email template {$templateName}: " . $e->getMessage()
- ], 500);
- }
- }
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'template' => $template,
+ 'templateName' => $templateName,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ "Failed to get email template {$templateName}",
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => "Failed to get email template {$templateName}: ".$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getEmailTemplate()
/**
- * Update email template
+ * Update email template.
+ *
+ * @param string $templateName Template name.
+ *
+ * @return JSONResponse Update result.
*
* @NoAdminRequired
* @NoCSRFRequired
- *
- * @param string $templateName Template name
- * @return JSONResponse Update result
*/
public function updateEmailTemplate(string $templateName): JSONResponse
{
try {
- $data = $this->request->getParams();
+ $data = $this->request->getParams();
$templateContent = $data['template'] ?? $data['content'] ?? '';
-
- if (empty($templateContent)) {
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Template content is required'
- ], 400);
+
+ if (empty($templateContent) === true) {
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Template content is required',
+ ],
+ 400
+ );
}
-
- $success = $this->settingsService->updateEmailTemplate($templateName, $templateContent);
-
- return new JSONResponse([
- 'success' => $success,
- 'message' => $success ? "Template {$templateName} updated successfully" : "Failed to update template {$templateName}"
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error("Failed to update email template {$templateName}", [
- 'exception' => $e->getMessage(),
- 'requestData' => $this->request->getParams()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => "Failed to update email template {$templateName}: " . $e->getMessage()
- ], 500);
- }
- }
+
+ $success = $this->settingsService->updateEmailTemplate(
+ templateName: $templateName,
+ content: $templateContent
+ );
+
+ if ($success === true) {
+ $updateMsg = "Template {$templateName} updated successfully";
+ } else {
+ $updateMsg = "Failed to update template {$templateName}";
+ }
+
+ return new JSONResponse(
+ [
+ 'success' => $success,
+ 'message' => $updateMsg,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ "Failed to update email template {$templateName}",
+ [
+ 'exception' => $e->getMessage(),
+ 'requestData' => $this->request->getParams(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => "Failed to update email template {$templateName}: ".$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end updateEmailTemplate()
/**
- * Get default email template
+ * Get default email template.
+ *
+ * @param string $templateName Template name.
+ *
+ * @return JSONResponse Default template content.
*
* @NoAdminRequired
* @NoCSRFRequired
- *
- * @param string $templateName Template name
- * @return JSONResponse Default template content
*/
public function getEmailTemplateDefault(string $templateName): JSONResponse
{
try {
$defaultTemplate = $this->settingsService->getDefaultEmailTemplate($templateName);
-
- return new JSONResponse([
- 'success' => true,
- 'template' => $defaultTemplate,
- 'templateName' => $templateName
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error("Failed to get default email template {$templateName}", [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => "Failed to get default email template {$templateName}: " . $e->getMessage()
- ], 500);
- }
- }
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'template' => $defaultTemplate,
+ 'templateName' => $templateName,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ "Failed to get default email template {$templateName}",
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => "Failed to get default email template {$templateName}: ".$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getEmailTemplateDefault()
/**
- * Get email template variables
+ * Get email template variables.
+ *
+ * @param string $templateName Template name.
+ *
+ * @return JSONResponse Available variables for template.
*
* @NoAdminRequired
* @NoCSRFRequired
- *
- * @param string $templateName Template name
- * @return JSONResponse Available variables for template
*/
public function getEmailTemplateVariables(string $templateName): JSONResponse
{
try {
$variables = $this->settingsService->getEmailTemplateVariables($templateName);
-
- return new JSONResponse([
- 'success' => true,
- 'variables' => $variables,
- 'templateName' => $templateName
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error("Failed to get email template variables for {$templateName}", [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => "Failed to get email template variables for {$templateName}: " . $e->getMessage()
- ], 500);
- }
- }
- // ========================================================================
- // USER GROUPS MANAGEMENT METHODS
- // ========================================================================
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'variables' => $variables,
+ 'templateName' => $templateName,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ "Failed to get email template variables for {$templateName}",
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => "Failed to get email template variables for {$templateName}: ".$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getEmailTemplateVariables()
+
+ // ===.
+ // USER GROUPS MANAGEMENT METHODS.
+ // ===.
/**
* Get generic user groups
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Generic user groups
*/
public function getGenericUserGroups(): JSONResponse
{
try {
$groups = $this->settingsService->getGenericUserGroups();
-
- return new JSONResponse([
- 'success' => true,
- 'groups' => $groups
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to get generic user groups', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get generic user groups: ' . $e->getMessage()
- ], 500);
- }
- }
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'groups' => $groups,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to get generic user groups',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get generic user groups: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getGenericUserGroups()
/**
* Set generic user groups
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Update result
*/
public function setGenericUserGroups(): JSONResponse
{
try {
- $data = $this->request->getParams();
+ $data = $this->request->getParams();
$groups = $data['groups'] ?? [];
-
- // Delegate all business logic (including validation) to service
+
+ // Delegate all business logic (including validation) to service.
$result = $this->settingsService->updateGenericUserGroups($groups);
-
- return new JSONResponse([
- 'success' => $result['success'],
- 'message' => $result['message'],
- 'groups' => $result['groups'] ?? null
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to set generic user groups', [
- 'exception' => $e->getMessage(),
- 'requestData' => $this->request->getParams()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to set generic user groups: ' . $e->getMessage()
- ], 500);
- }
- }
+
+ return new JSONResponse(
+ [
+ 'success' => $result['success'],
+ 'message' => $result['message'],
+ 'groups' => $result['groups'] ?? null,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to set generic user groups',
+ [
+ 'exception' => $e->getMessage(),
+ 'requestData' => $this->request->getParams(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to set generic user groups: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end setGenericUserGroups()
/**
* Get organization admin groups
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Organization admin groups
*/
public function getOrganizationAdminGroups(): JSONResponse
{
try {
$groups = $this->settingsService->getOrganizationAdminGroups();
-
- return new JSONResponse([
- 'success' => true,
- 'groups' => $groups
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to get organization admin groups', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get organization admin groups: ' . $e->getMessage()
- ], 500);
- }
- }
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'groups' => $groups,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to get organization admin groups',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get organization admin groups: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getOrganizationAdminGroups()
/**
* Set organization admin groups
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Update result
*/
public function setOrganizationAdminGroups(): JSONResponse
{
try {
- $data = $this->request->getParams();
+ $data = $this->request->getParams();
$groups = $data['groups'] ?? [];
-
- // Delegate all business logic (including validation) to service
+
+ // Delegate all business logic (including validation) to service.
$result = $this->settingsService->updateOrganizationAdminGroups($groups);
-
- return new JSONResponse([
- 'success' => $result['success'],
- 'message' => $result['message'],
- 'groups' => $result['groups'] ?? null
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to set organization admin groups', [
- 'exception' => $e->getMessage(),
- 'requestData' => $this->request->getParams()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to set organization admin groups: ' . $e->getMessage()
- ], 500);
- }
- }
+
+ return new JSONResponse(
+ [
+ 'success' => $result['success'],
+ 'message' => $result['message'],
+ 'groups' => $result['groups'] ?? null,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to set organization admin groups',
+ [
+ 'exception' => $e->getMessage(),
+ 'requestData' => $this->request->getParams(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to set organization admin groups: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end setOrganizationAdminGroups()
/**
* Get super user groups
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Super user groups
*/
public function getSuperUserGroups(): JSONResponse
{
try {
$groups = $this->settingsService->getSuperUserGroups();
-
- return new JSONResponse([
- 'success' => true,
- 'groups' => $groups
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to get super user groups', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get super user groups: ' . $e->getMessage()
- ], 500);
- }
- }
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'groups' => $groups,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to get super user groups',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get super user groups: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getSuperUserGroups()
/**
* Set super user groups
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Update result
*/
public function setSuperUserGroups(): JSONResponse
{
try {
- $data = $this->request->getParams();
+ $data = $this->request->getParams();
$groups = $data['groups'] ?? [];
-
- // Delegate all business logic (including validation) to service
+
+ // Delegate all business logic (including validation) to service.
$result = $this->settingsService->updateSuperUserGroups($groups);
-
- return new JSONResponse([
- 'success' => $result['success'],
- 'message' => $result['message'],
- 'groups' => $result['groups'] ?? null
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to set super user groups', [
- 'exception' => $e->getMessage(),
- 'requestData' => $this->request->getParams()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to set super user groups: ' . $e->getMessage()
- ], 500);
- }
- }
+
+ return new JSONResponse(
+ [
+ 'success' => $result['success'],
+ 'message' => $result['message'],
+ 'groups' => $result['groups'] ?? null,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to set super user groups',
+ [
+ 'exception' => $e->getMessage(),
+ 'requestData' => $this->request->getParams(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to set super user groups: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end setSuperUserGroups()
/**
* Get all user groups
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse All user groups
*/
public function getAllGroups(): JSONResponse
{
try {
$allGroups = $this->settingsService->getAllGroups();
-
- return new JSONResponse([
- 'success' => true,
- 'groups' => $allGroups
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to get all groups', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get all groups: ' . $e->getMessage()
- ], 500);
- }
- }
- // ========================================================================
- // ARCHIMATE STATUS MANAGEMENT METHODS
- // ========================================================================
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'groups' => $allGroups,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to get all groups',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get all groups: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getAllGroups()
+
+ // ===.
+ // ARCHIMATE STATUS MANAGEMENT METHODS.
+ // ===.
/**
* Clear ArchiMate import status
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Clear result
*/
public function clearArchiMateImportStatus(): JSONResponse
{
try {
$result = $this->settingsService->clearArchiMateImportStatus();
-
- return new JSONResponse([
- 'success' => true,
- 'message' => 'ArchiMate import status cleared successfully',
- 'details' => $result
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to clear ArchiMate import status', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to clear ArchiMate import status: ' . $e->getMessage()
- ], 500);
- }
- }
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'message' => 'ArchiMate import status cleared successfully',
+ 'details' => $result,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to clear ArchiMate import status',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to clear ArchiMate import status: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end clearArchiMateImportStatus()
/**
- * Force kill running ArchiMate import process and clear status
+ * Force kill running ArchiMate import process and clear status.
+ *
+ * @return JSONResponse Kill result.
+ *
+ * @deprecated Use cancelArchiMateImport() instead.
*
* @NoAdminRequired
* @NoCSRFRequired
- *
- * @return JSONResponse Kill result
- * @deprecated Use cancelArchiMateImport() instead
*/
public function killArchiMateImport(): JSONResponse
{
try {
$result = $this->settingsService->killArchiMateImport();
-
- return new JSONResponse([
- 'success' => true,
- 'message' => 'ArchiMate import termination completed',
- 'details' => $result
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to kill ArchiMate import process', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to kill ArchiMate import process: ' . $e->getMessage()
- ], 500);
- }
- }
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'message' => 'ArchiMate import termination completed',
+ 'details' => $result,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to kill ArchiMate import process',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to kill ArchiMate import process: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end killArchiMateImport()
/**
* Cancel a running ArchiMate import
@@ -2017,539 +2437,643 @@ public function killArchiMateImport(): JSONResponse
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Cancellation result
*/
public function cancelArchiMateImport(): JSONResponse
{
try {
$result = $this->settingsService->cancelArchiMateImport();
-
- $message = $result['cancelled']
- ? 'ArchiMate import cancelled successfully'
- : 'ArchiMate import cancellation failed';
-
- return new JSONResponse([
- 'success' => $result['cancelled'],
- 'message' => $message,
- 'details' => $result
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to cancel ArchiMate import', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to cancel ArchiMate import: ' . $e->getMessage()
- ], 500);
- }
- }
+
+ if ($result['cancelled'] === true) {
+ $message = 'ArchiMate import cancelled successfully';
+ } else {
+ $message = 'ArchiMate import cancellation failed';
+ }
+
+ return new JSONResponse(
+ [
+ 'success' => $result['cancelled'],
+ 'message' => $message,
+ 'details' => $result,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to cancel ArchiMate import',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to cancel ArchiMate import: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end cancelArchiMateImport()
/**
* Clear ArchiMate export status
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Clear result
*/
public function clearArchiMateExportStatus(): JSONResponse
{
try {
$this->settingsService->clearArchiMateExportStatus();
-
- return new JSONResponse([
- 'success' => true,
- 'message' => 'ArchiMate export status cleared successfully'
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to clear ArchiMate export status', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to clear ArchiMate export status: ' . $e->getMessage()
- ], 500);
- }
- }
-
- // ========================================================================
- // ARCHIMATE TESTING METHODS
- // ========================================================================
-
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'message' => 'ArchiMate export status cleared successfully',
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to clear ArchiMate export status',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to clear ArchiMate export status: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end clearArchiMateExportStatus()
+
+ // ===.
+ // ARCHIMATE TESTING METHODS.
+ // ===.
/**
* Test ArchiMate round-trip functionality
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Round-trip test result
*/
public function testArchiMateRoundTrip(): JSONResponse
{
try {
$this->logger->info('SoftwareCatalog: ArchiMate round-trip test started');
-
- // Call the ArchiMate service to perform round-trip test
+
+ // Call the ArchiMate service to perform round-trip test.
$result = $this->archiMateService->testRoundTrip();
-
- $this->logger->info('SoftwareCatalog: ArchiMate round-trip test completed', [
- 'success' => $result['success'],
- 'message' => $result['message'] ?? 'no message'
- ]);
-
- return new JSONResponse([
- 'success' => $result['success'],
- 'message' => $result['message'],
- 'details' => $result['details'] ?? null,
- 'statistics' => $result['statistics'] ?? null
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('SoftwareCatalog: ArchiMate round-trip test failed', [
- 'exception_class' => get_class($e),
- 'exception_message' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Round-trip test failed: ' . $e->getMessage()
- ], 500);
- }
- }
+
+ $this->logger->info(
+ 'SoftwareCatalog: ArchiMate round-trip test completed',
+ [
+ 'success' => $result['success'],
+ 'message' => $result['message'] ?? 'no message',
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => $result['success'],
+ 'message' => $result['message'],
+ 'details' => $result['details'] ?? null,
+ 'statistics' => $result['statistics'] ?? null,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'SoftwareCatalog: ArchiMate round-trip test failed',
+ [
+ 'exception_class' => get_class($e),
+ 'exception_message' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Round-trip test failed: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end testArchiMateRoundTrip()
/**
* Get ArchiMate settings and status (without object counts for performance)
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse ArchiMate settings and status
*/
public function getArchiMateSettings(): JSONResponse
{
try {
$archimateStatus = $this->settingsService->getArchiMateStatus();
-
- return new JSONResponse([
- 'success' => true,
- 'archimate' => $archimateStatus,
- 'timestamp' => time()
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to get ArchiMate settings', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get ArchiMate settings: ' . $e->getMessage()
- ], 500);
- }
- }
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'archimate' => $archimateStatus,
+ 'timestamp' => time(),
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to get ArchiMate settings',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get ArchiMate settings: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getArchiMateSettings()
/**
* Get object counts for all registers (separate endpoint for performance)
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Object counts for all registers
*/
public function getObjectCounts(): JSONResponse
{
try {
$objectCounts = $this->settingsService->getObjectCounts();
-
- return new JSONResponse([
- 'success' => true,
- 'objectCounts' => $objectCounts
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to get object counts', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get object counts: ' . $e->getMessage()
- ], 500);
- }
- }
- // ========================================================================
- // FOCUSED ENDPOINT CONTROLLER METHODS FOR PERFORMANCE OPTIMIZATION
- // ========================================================================
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'objectCounts' => $objectCounts,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to get object counts',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get object counts: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end getObjectCounts()
+
+ // ===.
+ // FOCUSED ENDPOINT CONTROLLER METHODS FOR PERFORMANCE OPTIMIZATION.
+ // ===.
/**
* Get ArchiMate configuration only
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse ArchiMate configuration
*/
public function getArchiMateConfig(): JSONResponse
{
try {
$config = $this->settingsService->getArchiMateConfig();
-
+
return new JSONResponse($config);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to get ArchiMate config', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get ArchiMate config: ' . $e->getMessage()
- ], 500);
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to get ArchiMate config',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get ArchiMate config: '.$e->getMessage(),
+ ],
+ 500
+ );
}
- }
+ }//end getArchiMateConfig()
/**
* Update ArchiMate configuration
*
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Update result
*/
public function updateArchiMateConfig(): JSONResponse
{
try {
- $data = $this->request->getParams();
+ $data = $this->request->getParams();
$result = $this->settingsService->updateArchiMateConfig($data);
-
+
return new JSONResponse($result);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to update ArchiMate config', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to update ArchiMate config: ' . $e->getMessage()
- ], 500);
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to update ArchiMate config',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to update ArchiMate config: '.$e->getMessage(),
+ ],
+ 500
+ );
}
- }
+ }//end updateArchiMateConfig()
/**
* Get email configuration only
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Email configuration
*/
public function getEmailConfig(): JSONResponse
{
try {
$config = $this->settingsService->getEmailConfigFocused();
-
+
return new JSONResponse($config);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to get email config', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get email config: ' . $e->getMessage()
- ], 500);
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to get email config',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get email config: '.$e->getMessage(),
+ ],
+ 500
+ );
}
- }
+ }//end getEmailConfig()
/**
* Update email configuration
*
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Update result
*/
public function updateEmailConfig(): JSONResponse
{
try {
- $data = $this->request->getParams();
+ $data = $this->request->getParams();
$result = $this->settingsService->updateEmailConfig($data);
-
+
return new JSONResponse($result);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to update email config', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to update email config: ' . $e->getMessage()
- ], 500);
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to update email config',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to update email config: '.$e->getMessage(),
+ ],
+ 500
+ );
}
- }
+ }//end updateEmailConfig()
/**
* Get AMEF configuration only
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse AMEF configuration
*/
public function getAmefConfig(): JSONResponse
{
try {
$config = $this->settingsService->getAmefConfigFocused();
-
+
return new JSONResponse($config);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to get AMEF config', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get AMEF config: ' . $e->getMessage()
- ], 500);
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to get AMEF config',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get AMEF config: '.$e->getMessage(),
+ ],
+ 500
+ );
}
- }
+ }//end getAmefConfig()
/**
* Update AMEF configuration
*
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Update result
*/
public function updateAmefConfig(): JSONResponse
{
try {
- $data = $this->request->getParams();
+ $data = $this->request->getParams();
$result = $this->settingsService->updateAmefConfig($data);
-
+
return new JSONResponse($result);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to update AMEF config', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to update AMEF config: ' . $e->getMessage()
- ], 500);
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to update AMEF config',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to update AMEF config: '.$e->getMessage(),
+ ],
+ 500
+ );
}
- }
+ }//end updateAmefConfig()
/**
* Get Voorzieningen configuration only
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Voorzieningen configuration
*/
public function getVoorzieningenConfig(): JSONResponse
{
try {
$config = $this->settingsService->getVoorzieningenConfigFocused();
-
+
return new JSONResponse($config);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to get Voorzieningen config', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get Voorzieningen config: ' . $e->getMessage()
- ], 500);
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to get Voorzieningen config',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get Voorzieningen config: '.$e->getMessage(),
+ ],
+ 500
+ );
}
- }
+ }//end getVoorzieningenConfig()
/**
* Update Voorzieningen configuration
*
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Update result
*/
public function updateVoorzieningenConfig(): JSONResponse
{
try {
- $data = $this->request->getParams();
+ $data = $this->request->getParams();
$result = $this->settingsService->updateVoorzieningenConfig($data);
-
+
return new JSONResponse($result);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to update Voorzieningen config', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to update Voorzieningen config: ' . $e->getMessage()
- ], 500);
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to update Voorzieningen config',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to update Voorzieningen config: '.$e->getMessage(),
+ ],
+ 500
+ );
}
- }
+ }//end updateVoorzieningenConfig()
/**
* Get object counts only (lightweight)
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Object counts
*/
public function getObjectsCounts(): JSONResponse
{
try {
$counts = $this->settingsService->getObjectsCounts();
-
+
return new JSONResponse($counts);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to get object counts', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get object counts: ' . $e->getMessage()
- ], 500);
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to get object counts',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get object counts: '.$e->getMessage(),
+ ],
+ 500
+ );
}
- }
+ }//end getObjectsCounts()
/**
* Get object statistics (full statistics with configuration)
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Object statistics
*/
public function getObjectsStatistics(): JSONResponse
{
try {
$statistics = $this->settingsService->getObjectsStatistics();
-
+
return new JSONResponse($statistics);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to get object statistics', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get object statistics: ' . $e->getMessage()
- ], 500);
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to get object statistics',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get object statistics: '.$e->getMessage(),
+ ],
+ 500
+ );
}
- }
+ }//end getObjectsStatistics()
/**
* Get user groups configuration only
*
* @NoAdminRequired
* @NoCSRFRequired
- *
+ *
* @return JSONResponse User groups configuration
*/
public function getUserGroupsConfig(): JSONResponse
{
try {
$config = $this->settingsService->getUserGroupsConfig();
-
+
return new JSONResponse($config);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to get user groups config', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get user groups config: ' . $e->getMessage()
- ], 500);
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to get user groups config',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get user groups config: '.$e->getMessage(),
+ ],
+ 500
+ );
}
- }
+ }//end getUserGroupsConfig()
/**
* Update user groups configuration
*
* @NoCSRFRequired
- *
+ *
* @return JSONResponse Update result
*/
public function updateUserGroupsConfig(): JSONResponse
{
try {
- $data = $this->request->getParams();
+ $data = $this->request->getParams();
$result = $this->settingsService->updateUserGroupsConfig($data);
-
+
return new JSONResponse($result);
-
- } catch (\Exception $e) {
- $this->logger->error('Failed to update user groups config', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to update user groups config: ' . $e->getMessage()
- ], 500);
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Failed to update user groups config',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to update user groups config: '.$e->getMessage(),
+ ],
+ 500
+ );
}
- }
+ }//end updateUserGroupsConfig()
/**
- * Determine appropriate HTTP status code for an exception
- *
- * @param \Exception $e The exception to classify
- * @return int HTTP status code (400, 404, 422, or 500)
+ * Determine appropriate HTTP status code for an exception.
+ *
+ * @param \Exception $e The exception to classify.
+ *
+ * @return int HTTP status code (400, 404, 422, or 500).
*/
private function getHttpStatusForException(\Exception $e): int
{
- // Check exception type first
+ // Check exception type first.
if ($e instanceof \InvalidArgumentException) {
- return 400; // Bad Request for invalid arguments/configuration
+ // Bad Request for invalid arguments/configuration.
+ return 400;
}
-
- // Fallback to message-based classification
+
+ // Fallback to message-based classification.
$message = $e->getMessage();
- return $this->getHttpStatusForErrorMessage($message);
- }
+ return $this->getHttpStatusForErrorMessage(message: $message);
+ }//end getHttpStatusForException()
/**
- * Determine appropriate HTTP status code for an error message
- *
- * @param string $message The error message to classify
- * @return int HTTP status code (400, 404, 422, or 500)
+ * Determine appropriate HTTP status code for an error message.
+ *
+ * @param string $message The error message to classify.
+ *
+ * @return int HTTP status code (400, 404, 422, or 500).
*/
private function getHttpStatusForErrorMessage(string $message): int
{
$message = strtolower($message);
-
- // Configuration errors - 400 Bad Request
- if (str_contains($message, 'not configured') ||
- str_contains($message, 'missing configuration') ||
- str_contains($message, 'invalid configuration')) {
+
+ // Configuration errors — 400 Bad Request.
+ if (str_contains(haystack: $message, needle: 'not configured') === true
+ || str_contains(haystack: $message, needle: 'missing configuration') === true
+ || str_contains(haystack: $message, needle: 'invalid configuration') === true
+ ) {
return 400;
}
-
- // File not found errors - 404 Not Found
- if (str_contains($message, 'file not found') ||
- str_contains($message, 'not found') ||
- str_contains($message, 'missing file')) {
+
+ // File not found errors — 404 Not Found.
+ if (str_contains(haystack: $message, needle: 'file not found') === true
+ || str_contains(haystack: $message, needle: 'not found') === true
+ || str_contains(haystack: $message, needle: 'missing file') === true
+ ) {
return 404;
}
-
- // Validation errors - 422 Unprocessable Entity
- if (str_contains($message, 'validation') ||
- str_contains($message, 'invalid xml') ||
- str_contains($message, 'parsing error') ||
- str_contains($message, 'malformed') ||
- str_contains($message, 'could not be parsed')) {
+
+ // Validation errors — 422 Unprocessable Entity.
+ if (str_contains(haystack: $message, needle: 'validation') === true
+ || str_contains(haystack: $message, needle: 'invalid xml') === true
+ || str_contains(haystack: $message, needle: 'parsing error') === true
+ || str_contains(haystack: $message, needle: 'malformed') === true
+ || str_contains(haystack: $message, needle: 'could not be parsed') === true
+ ) {
return 422;
}
-
- // Default to 500 Internal Server Error for unknown issues
+
+ // Default to 500 Internal Server Error for unknown issues.
return 500;
- }
+ }//end getHttpStatusForErrorMessage()
/**
* Sync OpenRegister organisations to voorzieningen register
@@ -2564,93 +3088,115 @@ public function syncOrganisations(): JSONResponse
try {
$this->logger->info('SettingsController: Starting organisation sync via API');
- // Get request parameters
+ // Get request parameters.
$requestBody = $this->request->getParams();
- $options = [
- 'batch_size' => (int)($requestBody['batch_size'] ?? 500),
- 'dry_run' => filter_var($requestBody['dry_run'] ?? false, FILTER_VALIDATE_BOOLEAN)
+ $options = [
+ 'batch_size' => (int) ($requestBody['batch_size'] ?? 500),
+ 'dry_run' => filter_var($requestBody['dry_run'] ?? false, FILTER_VALIDATE_BOOLEAN),
];
- $this->logger->debug('SettingsController: Sync options', $options);
+ $this->logger->debug('SettingsController: Sync options.', $options);
- // Call the settings service method
+ // Call the settings service method.
$result = $this->settingsService->syncOrganisationsToVoorzieningenOptimized($options);
- $statusCode = $result['success'] ? 200 : 500;
+ if ($result['success'] === true) {
+ $statusCode = 200;
+ } else {
+ $statusCode = 500;
+ }
- $this->logger->info('SettingsController: Organisation sync completed', [
- 'success' => $result['success'],
- 'message' => $result['message'] ?? 'No message'
- ]);
+ $this->logger->info(
+ 'SettingsController: Organisation sync completed',
+ [
+ 'success' => $result['success'],
+ 'message' => $result['message'] ?? 'No message',
+ ]
+ );
return new JSONResponse($result, $statusCode);
-
} catch (\Exception $e) {
- $this->logger->error('SettingsController: Organisation sync failed', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'SettingsController: Organisation sync failed',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Organisation sync failed: ' . $e->getMessage(),
- 'error' => $e->getMessage()
- ], 500);
- }
- }
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Organisation sync failed: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end syncOrganisations()
/**
- * Bulk sync module standards from all compliance objects
+ * Bulk sync module standards from all compliance objects.
+ *
+ * @return JSONResponse Response containing sync results.
*
- * @return JSONResponse Response containing sync results
* @NoCSRFRequired
*/
public function bulkSyncStandards(): JSONResponse
{
try {
- $this->logger->info('SettingsController: Starting bulk sync of module standards');
+ $this->logger->info('SettingsController: Starting bulk sync of module standards.');
- // Get the ModuleComplianceService from the container
+ // Get the ModuleComplianceService from the container.
$moduleComplianceService = $this->container->get(\OCA\SoftwareCatalog\Service\ModuleComplianceService::class);
-
- // Perform the bulk sync
+
+ // Perform the bulk sync.
$results = $moduleComplianceService->bulkSyncModuleStandards();
- $this->logger->info('SettingsController: Bulk sync completed successfully', [
- 'results' => $results
- ]);
-
- return new JSONResponse([
- 'success' => true,
- 'message' => 'Bulk sync completed successfully',
- 'data' => $results
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('SettingsController: Bulk sync failed', [
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
- ]);
-
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Bulk sync failed: ' . $e->getMessage(),
- 'error' => $e->getMessage()
- ], $this->getHttpStatusForException($e));
- }
- }
+ $this->logger->info(
+ 'SettingsController: Bulk sync completed successfully',
+ [
+ 'results' => $results,
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => true,
+ 'message' => 'Bulk sync completed successfully',
+ 'data' => $results,
+ ]
+ );
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'SettingsController: Bulk sync failed',
+ [
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Bulk sync failed: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
+ ],
+ $this->getHttpStatusForException(e: $e)
+ );
+ }//end try
+ }//end bulkSyncStandards()
- // ========================================================================
- // CRONJOB CONFIGURATION ENDPOINTS
- // ========================================================================
+ // ===.
+ // CRONJOB CONFIGURATION ENDPOINTS (deprecated — sync now uses _rbac: false).
+ // ===.
/**
* Get cronjob configuration
*
- * Returns configuration for all registered cronjobs including their
- * user and organisation context settings.
+ * @deprecated Cronjob context is no longer needed. Will be removed in a future version.
*
* @NoAdminRequired
* @NoCSRFRequired
@@ -2663,20 +3209,26 @@ public function getCronjobConfig(): JSONResponse
$config = $this->settingsService->getCronjobConfig();
return new JSONResponse($config);
} catch (\Exception $e) {
- $this->logger->error('Failed to get cronjob config', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get cronjob config: ' . $e->getMessage()
- ], 500);
+ $this->logger->error(
+ 'Failed to get cronjob config',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get cronjob config: '.$e->getMessage(),
+ ],
+ 500
+ );
}
- }
+ }//end getCronjobConfig()
/**
* Update cronjob configuration
*
- * Updates the user and organisation context for a specific cronjob.
+ * @deprecated Cronjob context is no longer needed. Will be removed in a future version.
*
* @NoCSRFRequired
*
@@ -2685,27 +3237,38 @@ public function getCronjobConfig(): JSONResponse
public function updateCronjobConfig(): JSONResponse
{
try {
- $data = $this->request->getParams();
+ $data = $this->request->getParams();
$result = $this->settingsService->updateCronjobConfig($data);
-
- $statusCode = $result['success'] ? 200 : 400;
+
+ if ($result['success'] === true) {
+ $statusCode = 200;
+ } else {
+ $statusCode = 400;
+ }
+
return new JSONResponse($result, $statusCode);
} catch (\Exception $e) {
- $this->logger->error('Failed to update cronjob config', [
- 'exception' => $e->getMessage(),
- 'requestData' => $this->request->getParams()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to update cronjob config: ' . $e->getMessage()
- ], 500);
- }
- }
+ $this->logger->error(
+ 'Failed to update cronjob config',
+ [
+ 'exception' => $e->getMessage(),
+ 'requestData' => $this->request->getParams(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to update cronjob config: '.$e->getMessage(),
+ ],
+ 500
+ );
+ }//end try
+ }//end updateCronjobConfig()
/**
* Get available users for cronjob configuration
*
- * Returns a list of users that can be selected for running cronjobs.
+ * @deprecated Cronjob context is no longer needed. Will be removed in a future version.
*
* @NoAdminRequired
* @NoCSRFRequired
@@ -2718,21 +3281,27 @@ public function getCronjobUsers(): JSONResponse
$result = $this->settingsService->getAvailableUsersForCronjobs();
return new JSONResponse($result);
} catch (\Exception $e) {
- $this->logger->error('Failed to get cronjob users', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get cronjob users: ' . $e->getMessage(),
- 'users' => []
- ], 500);
+ $this->logger->error(
+ 'Failed to get cronjob users',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get cronjob users: '.$e->getMessage(),
+ 'users' => [],
+ ],
+ 500
+ );
}
- }
+ }//end getCronjobUsers()
/**
* Get available organisations for cronjob configuration
*
- * Returns a list of organisations that can be selected for running cronjobs.
+ * @deprecated Cronjob context is no longer needed. Will be removed in a future version.
*
* @NoAdminRequired
* @NoCSRFRequired
@@ -2745,17 +3314,20 @@ public function getCronjobOrganisations(): JSONResponse
$result = $this->settingsService->getAvailableOrganisationsForCronjobs();
return new JSONResponse($result);
} catch (\Exception $e) {
- $this->logger->error('Failed to get cronjob organisations', [
- 'exception' => $e->getMessage()
- ]);
- return new JSONResponse([
- 'success' => false,
- 'message' => 'Failed to get cronjob organisations: ' . $e->getMessage(),
- 'organisations' => []
- ], 500);
- }
- }
-
+ $this->logger->error(
+ 'Failed to get cronjob organisations',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'message' => 'Failed to get cronjob organisations: '.$e->getMessage(),
+ 'organisations' => [],
+ ],
+ 500
+ );
+ }
+ }//end getCronjobOrganisations()
}//end class
-
-
diff --git a/lib/Controller/ViewController.php b/lib/Controller/ViewController.php
index c3b22a58..a3cc44f1 100644
--- a/lib/Controller/ViewController.php
+++ b/lib/Controller/ViewController.php
@@ -2,15 +2,15 @@
/**
* View Controller for SoftwareCatalog
- *
+ *
* Handles HTTP requests for view-related operations including querying views
* with enrichment options for products and usage data.
- *
+ *
* @category Controller
* @package OCA\SoftwareCatalog\Controller
- * @author SoftwareCatalog Team
+ * @author Conduction b.v.
* @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/nextcloud/softwarecatalog
*/
@@ -23,27 +23,27 @@
use Psr\Log\LoggerInterface;
/**
- * Controller for handling view-related API operations
- *
+ * Controller for handling view-related API operations.
+ *
* This controller provides REST API endpoints for querying and managing ArchiMate views
* with optional enrichment capabilities for products, usage data (gebruik), and related information.
- *
+ *
* @category Controller
* @package OCA\SoftwareCatalog\Controller
- * @author SoftwareCatalog Team
+ * @author Conduction b.v.
* @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/nextcloud/softwarecatalog
*/
class ViewController extends Controller
{
/**
* Constructor for ViewController
- *
- * @param string $appName The app name
- * @param IRequest $request The request object
- * @param ViewService $viewService The view service for business logic
- * @param LoggerInterface $logger The logger service
+ *
+ * @param string $appName The app name
+ * @param IRequest $request The request object
+ * @param ViewService $viewService The view service for business logic
+ * @param LoggerInterface $logger The logger service
*/
public function __construct(
string $appName,
@@ -51,338 +51,381 @@ public function __construct(
private readonly ViewService $viewService,
private readonly LoggerInterface $logger
) {
- parent::__construct($appName, $request);
- }
+ parent::__construct(appName: $appName, request: $request);
+ }//end __construct()
/**
* Get all views with optional enrichment
- *
+ *
* API Endpoint: GET /api/views
- *
+ *
* Query Parameters:
* - include_products (bool): Include product data in view nodes
* - include_modules (bool): Include module data in view nodes (linked via elementRef)
- * - include_gebruik (bool): Include usage data in view nodes
+ * - include_gebruik (bool): Include usage data in view nodes
* - include_deelnames_gebruik (bool): Include participation usage data in view nodes
- *
+ *
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
- *
+ *
* @return JSONResponse JSON response with views array
*/
public function getAllViews(): JSONResponse
{
- $this->logger->info('API: Getting all views', [
- 'endpoint' => '/api/views',
- 'method' => 'GET',
- 'query_params' => $this->request->getParams()
- ]);
+ $this->logger->info(
+ 'API: Getting all views',
+ [
+ 'endpoint' => '/api/views',
+ 'method' => 'GET',
+ 'query_params' => $this->request->getParams(),
+ ]
+ );
try {
- // Parse query parameters for enrichment options
+ // Parse query parameters for enrichment options.
$options = $this->parseEnrichmentOptions();
-
- // Get views from service with enrichments
+
+ // Get views from service with enrichments.
$result = $this->viewService->getAllViews($options);
-
- // Return appropriate HTTP status code
- $statusCode = $result['success'] ? 200 : 500;
-
- $this->logger->info('API: All views request completed', [
- 'success' => $result['success'],
- 'views_count' => $result['count'] ?? 0,
- 'enrichments_applied' => $result['enrichments_applied'] ?? []
- ]);
-
+
+ // Return appropriate HTTP status code.
+ if ($result['success'] === true) {
+ $statusCode = 200;
+ } else {
+ $statusCode = 500;
+ }
+
+ $this->logger->info(
+ 'API: All views request completed',
+ [
+ 'success' => $result['success'],
+ 'views_count' => $result['count'] ?? 0,
+ 'enrichments_applied' => $result['enrichments_applied'] ?? [],
+ ]
+ );
+
return new JSONResponse($result, $statusCode);
-
} catch (\Exception $e) {
- $this->logger->error('API: Failed to get all views', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
-
- return new JSONResponse([
- 'success' => false,
- 'error' => 'Internal server error: ' . $e->getMessage(),
- 'views' => [],
- 'count' => 0
- ], 500);
- }
- }
+ $this->logger->error(
+ 'API: Failed to get all views',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'error' => 'Internal server error: '.$e->getMessage(),
+ 'views' => [],
+ 'count' => 0,
+ ],
+ 500
+ );
+ }//end try
+ }//end getAllViews()
/**
* Get a specific view by ID with optional enrichment
- *
+ *
* API Endpoint: GET /api/views/{viewId}
- *
+ *
* Query Parameters:
* - include_products (bool): Include product data in view nodes
* - include_modules (bool): Include module data in view nodes (linked via elementRef)
* - include_gebruik (bool): Include usage data in view nodes
* - include_deelnames_gebruik (bool): Include participation usage data in view nodes
- *
+ *
+ * @param string $viewId The view identifier
+ *
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
- *
- * @param string $viewId The view identifier
+ *
* @return JSONResponse JSON response with view object
*/
public function getView(string $viewId): JSONResponse
{
- $this->logger->info('API: Getting specific view', [
- 'endpoint' => "/api/views/{$viewId}",
- 'method' => 'GET',
- 'view_id' => $viewId,
- 'query_params' => $this->request->getParams()
- ]);
+ $this->logger->info(
+ 'API: Getting specific view',
+ [
+ 'endpoint' => "/api/views/{$viewId}",
+ 'method' => 'GET',
+ 'view_id' => $viewId,
+ 'query_params' => $this->request->getParams(),
+ ]
+ );
try {
- // Validate view ID
- if (empty($viewId)) {
- return new JSONResponse([
- 'success' => false,
- 'error' => 'View ID is required',
- 'view' => null
- ], 400);
+ // Validate view ID.
+ if (empty($viewId) === true) {
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'error' => 'View ID is required',
+ 'view' => null,
+ ],
+ 400
+ );
}
- // Parse query parameters for enrichment options
+ // Parse query parameters for enrichment options.
$options = $this->parseEnrichmentOptions();
-
- // Get view from service with enrichments
- $result = $this->viewService->getView($viewId, $options);
-
- // Return appropriate HTTP status code
- $statusCode = $result['success'] ? 200 : ($result['view'] === null ? 404 : 500);
-
- $this->logger->info('API: Specific view request completed', [
- 'view_id' => $viewId,
- 'success' => $result['success'],
- 'found' => $result['view'] !== null,
- 'enrichments_applied' => $result['enrichments_applied'] ?? []
- ]);
-
+
+ // Get view from service with enrichments.
+ $result = $this->viewService->getView(
+ viewId: $viewId,
+ options: $options
+ );
+
+ // Return appropriate HTTP status code.
+ if ($result['success'] === true) {
+ $statusCode = 200;
+ } else if ($result['view'] === null) {
+ $statusCode = 404;
+ } else {
+ $statusCode = 500;
+ }
+
+ $this->logger->info(
+ 'API: Specific view request completed',
+ [
+ 'view_id' => $viewId,
+ 'success' => $result['success'],
+ 'found' => $result['view'] !== null,
+ 'enrichments_applied' => $result['enrichments_applied'] ?? [],
+ ]
+ );
+
return new JSONResponse($result, $statusCode);
-
} catch (\Exception $e) {
- $this->logger->error('API: Failed to get specific view', [
- 'view_id' => $viewId,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
-
- return new JSONResponse([
- 'success' => false,
- 'error' => 'Internal server error: ' . $e->getMessage(),
- 'view' => null
- ], 500);
- }
- }
+ $this->logger->error(
+ 'API: Failed to get specific view',
+ [
+ 'view_id' => $viewId,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+
+ return new JSONResponse(
+ [
+ 'success' => false,
+ 'error' => 'Internal server error: '.$e->getMessage(),
+ 'view' => null,
+ ],
+ 500
+ );
+ }//end try
+ }//end getView()
/**
* Parse enrichment options from query parameters
- *
+ *
* @return array Parsed enrichment options
*/
private function parseEnrichmentOptions(): array
{
$options = [];
-
- // Parse boolean query parameters
+
+ // Parse boolean query parameters.
$includeProducts = $this->request->getParam('include_products');
if ($includeProducts !== null) {
- $options['include_products'] = $this->parseBooleanParam($includeProducts);
+ $options['include_products'] = $this->parseBooleanParam(value: $includeProducts);
}
-
+
$includeModules = $this->request->getParam('include_modules');
if ($includeModules !== null) {
- $options['include_modules'] = $this->parseBooleanParam($includeModules);
+ $options['include_modules'] = $this->parseBooleanParam(value: $includeModules);
}
-
+
$includeGebruik = $this->request->getParam('include_gebruik');
if ($includeGebruik !== null) {
- $options['include_gebruik'] = $this->parseBooleanParam($includeGebruik);
+ $options['include_gebruik'] = $this->parseBooleanParam(value: $includeGebruik);
}
-
+
$includeDeelnamesGebruik = $this->request->getParam('include_deelnames_gebruik');
if ($includeDeelnamesGebruik !== null) {
- $options['include_deelnames_gebruik'] = $this->parseBooleanParam($includeDeelnamesGebruik);
+ $options['include_deelnames_gebruik'] = $this->parseBooleanParam(value: $includeDeelnamesGebruik);
}
-
- $this->logger->debug('Parsed enrichment options', [
- 'raw_params' => [
- 'include_products' => $includeProducts,
- 'include_modules' => $includeModules,
- 'include_gebruik' => $includeGebruik,
- 'include_deelnames_gebruik' => $includeDeelnamesGebruik
- ],
- 'parsed_options' => $options
- ]);
-
+
+ $this->logger->debug(
+ 'Parsed enrichment options',
+ [
+ 'raw_params' => [
+ 'include_products' => $includeProducts,
+ 'include_modules' => $includeModules,
+ 'include_gebruik' => $includeGebruik,
+ 'include_deelnames_gebruik' => $includeDeelnamesGebruik,
+ ],
+ 'parsed_options' => $options,
+ ]
+ );
+
return $options;
- }
+ }//end parseEnrichmentOptions()
/**
- * Parse a boolean parameter from string values
- *
+ * Parse a boolean parameter from string values.
+ *
* Accepts: true, false, 1, 0, "true", "false", "1", "0", "yes", "no"
- *
+ *
* @param mixed $value The parameter value to parse
+ *
* @return bool The parsed boolean value
*/
private function parseBooleanParam($value): bool
{
- if (is_bool($value)) {
+ if (is_bool($value) === true) {
return $value;
}
-
- if (is_numeric($value)) {
- return (int)$value > 0;
+
+ if (is_numeric($value) === true) {
+ return (int) $value > 0;
}
-
- if (is_string($value)) {
+
+ if (is_string($value) === true) {
$lowerValue = strtolower(trim($value));
- return in_array($lowerValue, ['true', '1', 'yes', 'on'], true);
+ return in_array($lowerValue, ['true', '1', 'yes', 'on'], true) === true;
}
-
+
return false;
- }
+ }//end parseBooleanParam()
/**
* Get API documentation for view endpoints
- *
+ *
* API Endpoint: GET /api/views/docs
- *
+ *
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
- *
+ *
* @return JSONResponse JSON response with API documentation
*/
public function getApiDocumentation(): JSONResponse
{
$documentation = [
- 'api_version' => '1.0.0',
- 'description' => 'SoftwareCatalog View API - Query and enrich ArchiMate views with additional data',
- 'base_url' => '/api/views',
- 'endpoints' => [
+ 'api_version' => '1.0.0',
+ 'description' => 'SoftwareCatalog View API - Query and enrich ArchiMate views',
+ 'base_url' => '/api/views',
+ 'endpoints' => [
[
- 'method' => 'GET',
- 'path' => '/api/views',
- 'description' => 'Get all views with optional enrichment',
- 'parameters' => [
+ 'method' => 'GET',
+ 'path' => '/api/views',
+ 'description' => 'Get all views with optional enrichment',
+ 'parameters' => [
[
- 'name' => 'include_products',
- 'type' => 'boolean',
- 'required' => false,
- 'description' => 'Include product data in view nodes'
+ 'name' => 'include_products',
+ 'type' => 'boolean',
+ 'required' => false,
+ 'description' => 'Include product data in view nodes',
],
[
- 'name' => 'include_gebruik',
- 'type' => 'boolean',
- 'required' => false,
- 'description' => 'Include usage data in view nodes'
+ 'name' => 'include_gebruik',
+ 'type' => 'boolean',
+ 'required' => false,
+ 'description' => 'Include usage data in view nodes',
],
[
- 'name' => 'include_deelnames_gebruik',
- 'type' => 'boolean',
- 'required' => false,
- 'description' => 'Include participation usage data in view nodes'
- ]
+ 'name' => 'include_deelnames_gebruik',
+ 'type' => 'boolean',
+ 'required' => false,
+ 'description' => 'Include participation usage data in view nodes',
+ ],
],
'response_example' => [
- 'success' => true,
- 'views' => [
+ 'success' => true,
+ 'views' => [
[
- 'id' => 'view-lv01',
- 'name' => 'LV01 BGT basisregistratie en SVB view',
- 'documentation' => 'Mocked from GEMMA LV01.',
- 'viewNodes' => [],
- 'viewRelationships' => []
- ]
+ 'id' => 'view-lv01',
+ 'name' => 'LV01 BGT basisregistratie en SVB view',
+ 'documentation' => 'Mocked from GEMMA LV01.',
+ 'viewNodes' => [],
+ 'viewRelationships' => [],
+ ],
],
- 'count' => 1,
- 'enrichments_applied' => []
- ]
+ 'count' => 1,
+ 'enrichments_applied' => [],
+ ],
],
[
- 'method' => 'GET',
- 'path' => '/api/views/{viewId}',
- 'description' => 'Get a specific view by ID with optional enrichment',
- 'parameters' => [
+ 'method' => 'GET',
+ 'path' => '/api/views/{viewId}',
+ 'description' => 'Get a specific view by ID with optional enrichment',
+ 'parameters' => [
[
- 'name' => 'viewId',
- 'type' => 'string',
- 'required' => true,
- 'description' => 'The view identifier (in URL path)'
+ 'name' => 'viewId',
+ 'type' => 'string',
+ 'required' => true,
+ 'description' => 'The view identifier (in URL path)',
],
[
- 'name' => 'include_products',
- 'type' => 'boolean',
- 'required' => false,
- 'description' => 'Include product data in view nodes'
+ 'name' => 'include_products',
+ 'type' => 'boolean',
+ 'required' => false,
+ 'description' => 'Include product data in view nodes',
],
[
- 'name' => 'include_gebruik',
- 'type' => 'boolean',
- 'required' => false,
- 'description' => 'Include usage data in view nodes'
+ 'name' => 'include_gebruik',
+ 'type' => 'boolean',
+ 'required' => false,
+ 'description' => 'Include usage data in view nodes',
],
[
- 'name' => 'include_deelnames_gebruik',
- 'type' => 'boolean',
- 'required' => false,
- 'description' => 'Include participation usage data in view nodes'
- ]
+ 'name' => 'include_deelnames_gebruik',
+ 'type' => 'boolean',
+ 'required' => false,
+ 'description' => 'Include participation usage data in view nodes',
+ ],
],
'response_example' => [
- 'success' => true,
- 'view' => [
- 'id' => 'view-lv01',
- 'name' => 'LV01 BGT basisregistratie en SVB view',
- 'documentation' => 'Mocked from GEMMA LV01.',
- 'viewNodes' => [],
- 'viewRelationships' => []
+ 'success' => true,
+ 'view' => [
+ 'id' => 'view-lv01',
+ 'name' => 'LV01 BGT basisregistratie en SVB view',
+ 'documentation' => 'Mocked from GEMMA LV01.',
+ 'viewNodes' => [],
+ 'viewRelationships' => [],
],
- 'enrichments_applied' => []
- ]
+ 'enrichments_applied' => [],
+ ],
],
[
- 'method' => 'GET',
- 'path' => '/api/views/docs',
- 'description' => 'Get this API documentation',
- 'parameters' => [],
- 'response_example' => '(this response)'
- ]
+ 'method' => 'GET',
+ 'path' => '/api/views/docs',
+ 'description' => 'Get this API documentation',
+ 'parameters' => [],
+ 'response_example' => '(this response)',
+ ],
],
- 'enrichment_options' => [
+ 'enrichment_options' => [
[
- 'name' => 'products',
- 'description' => 'Adds product information to view nodes that reference elements with product associations'
+ 'name' => 'products',
+ 'description' => 'Adds product information to nodes with product associations',
],
[
- 'name' => 'gebruik',
- 'description' => 'Adds usage/usage statistics to view nodes'
+ 'name' => 'gebruik',
+ 'description' => 'Adds usage/usage statistics to view nodes',
],
[
- 'name' => 'deelnames_gebruik',
- 'description' => 'Adds participation-based usage data to view nodes created from participation records'
- ]
+ 'name' => 'deelnames_gebruik',
+ 'description' => 'Adds participation-based usage data to view nodes created from participation records',
+ ],
],
'boolean_parameter_formats' => [
- 'accepted_true_values' => ['true', '1', 'yes', 'on', true, 1],
+ 'accepted_true_values' => ['true', '1', 'yes', 'on', true, 1],
'accepted_false_values' => ['false', '0', 'no', 'off', false, 0],
- 'examples' => [
+ 'examples' => [
'/api/views?include_products=true',
'/api/views?include_gebruik=1&include_products=false',
- '/api/views/view-123?include_deelnames_gebruik=yes'
- ]
- ]
+ '/api/views/view-123?include_deelnames_gebruik=yes',
+ ],
+ ],
];
return new JSONResponse($documentation, 200);
- }
-}
+ }//end getApiDocumentation()
+}//end class
diff --git a/lib/Dashboard/ConceptOrganisatiesWidget.php b/lib/Dashboard/ConceptOrganisatiesWidget.php
index d20d456a..06a8a014 100644
--- a/lib/Dashboard/ConceptOrganisatiesWidget.php
+++ b/lib/Dashboard/ConceptOrganisatiesWidget.php
@@ -1,4 +1,15 @@
+ * @copyright 2024 Conduction B.V.
+ * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
+ */
namespace OCA\SoftwareCatalog\Dashboard;
@@ -11,75 +22,78 @@
class ConceptOrganisatiesWidget implements IWidget
{
-
-
+ /**
+ * Constructor for ConceptOrganisatiesWidget.
+ *
+ * @param IL10N $l10n The localization service
+ * @param IURLGenerator $url The URL generator service
+ */
public function __construct(
private IL10N $l10n,
private IURLGenerator $url
) {
-
}//end __construct()
-
/**
- * @inheritDoc
+ * Returns the unique widget identifier.
+ *
+ * @return string The widget ID
*/
public function getId(): string
{
return 'softwarecatalog_concept_organisaties_widget';
-
}//end getId()
-
/**
- * @inheritDoc
+ * Returns the widget title.
+ *
+ * @return string The translated widget title
*/
public function getTitle(): string
{
return $this->l10n->t('Concept organisaties');
-
}//end getTitle()
-
/**
- * @inheritDoc
+ * Returns the display order for this widget.
+ *
+ * @return int The widget order value
*/
public function getOrder(): int
{
return 10;
-
}//end getOrder()
-
/**
- * @inheritDoc
+ * Returns the CSS icon class for this widget.
+ *
+ * @return string The icon CSS class name
*/
public function getIconClass(): string
{
return 'icon-softwarecatalog-widget';
-
}//end getIconClass()
-
/**
- * @inheritDoc
+ * Returns the URL for the full widget page.
+ *
+ * @return string|null The URL or null if no dedicated page
*/
public function getUrl(): ?string
{
return null;
-
}//end getUrl()
-
/**
- * @inheritDoc
+ * Loads the required scripts and styles for this widget.
+ *
+ * @return void
*/
public function load(): void
{
- Util::addScript(Application::APP_ID, Application::APP_ID.'-conceptOrganisatiesWidget');
- Util::addStyle(Application::APP_ID, 'dashboardWidgets');
-
+ $appId = Application::APP_ID;
+ $scriptName = $appId.'-conceptOrganisatiesWidget';
+ Util::addScript(application: $appId, file: $scriptName);
+ Util::addStyle(application: $appId, file: 'dashboardWidgets');
}//end load()
-
-
}//end class
diff --git a/lib/EventListener/ModuleComplianceSubscriber.php b/lib/EventListener/ModuleComplianceSubscriber.php
index fb21f0df..b1806653 100644
--- a/lib/EventListener/ModuleComplianceSubscriber.php
+++ b/lib/EventListener/ModuleComplianceSubscriber.php
@@ -1,6 +1,6 @@
* @copyright 2024 Conduction B.V.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
@@ -38,23 +38,23 @@
* @package OCA\SoftwareCatalog\EventListener
* @author Conduction b.v.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
class ModuleComplianceSubscriber implements IEventListener
{
/**
- * Constructor for ModuleComplianceSubscriber
+ * Constructor for ModuleComplianceSubscriber.
*
* @param ContainerInterface $container The DI container
*/
public function __construct(
private readonly ContainerInterface $container
) {
- }
+ }//end __construct()
/**
- * Handle the event
+ * Handle the event.
*
* @param Event $event The event to handle
*
@@ -62,80 +62,96 @@ public function __construct(
*/
public function handle(Event $event): void
{
- // Log when subscriber is called for debugging
+ // Log when subscriber is called for debugging.
$logger = $this->container->get(LoggerInterface::class);
- $logger->info('ModuleComplianceSubscriber: SUBSCRIBER CALLED', [
- 'eventType' => get_class($event),
- 'timestamp' => date('Y-m-d H:i:s')
- ]);
-
- // Only handle ObjectCreatedEvent and ObjectUpdatedEvent
- if (!($event instanceof ObjectCreatedEvent) && !($event instanceof ObjectUpdatedEvent)) {
+ $logger->info(
+ 'ModuleComplianceSubscriber: SUBSCRIBER CALLED',
+ [
+ 'eventType' => get_class($event),
+ 'timestamp' => date('Y-m-d H:i:s'),
+ ]
+ );
+
+ // Only handle ObjectCreatedEvent and ObjectUpdatedEvent.
+ if (($event instanceof ObjectCreatedEvent) === false && ($event instanceof ObjectUpdatedEvent) === false) {
return;
}
- // Get object from event - different methods for different event types
+ // Get object from event - different methods for different event types.
if ($event instanceof ObjectCreatedEvent) {
$object = $event->getObject();
- } elseif ($event instanceof ObjectUpdatedEvent) {
- $object = $event->getNewObject(); // Use getNewObject() for updated events
+ } else if ($event instanceof ObjectUpdatedEvent) {
+ // Use getNewObject() for updated events.
+ $object = $event->getNewObject();
} else {
return;
}
- $objectId = $object->getId();
+ $objectId = $object->getId();
$objectSchemaId = $object->getSchema();
- // Get module schema ID from configuration
+ // Get module schema ID from configuration.
$settingsService = $this->container->get(SettingsService::class);
- $moduleSchemaId = $settingsService->getSchemaIdForObjectType('module');
+ $moduleSchemaId = $settingsService->getSchemaIdForObjectType('module');
- if (!$moduleSchemaId) {
- $logger->debug('ModuleComplianceSubscriber: Module schema not configured, skipping', [
- 'objectId' => $objectId,
- 'schemaId' => $objectSchemaId
- ]);
+ if ($moduleSchemaId === null) {
+ $logger->debug(
+ 'ModuleComplianceSubscriber: Module schema not configured, skipping',
+ [
+ 'objectId' => $objectId,
+ 'schemaId' => $objectSchemaId,
+ ]
+ );
return;
}
$moduleSchemaIdInt = (int) $moduleSchemaId;
$objectSchemaIdInt = (int) $objectSchemaId;
- // Check if this is a module object
+ // Check if this is a module object.
if ($objectSchemaIdInt !== $moduleSchemaIdInt) {
return;
}
try {
- // Handle module compliance update
+ // Handle module compliance update.
$moduleComplianceService = $this->container->get(ModuleComplianceService::class);
$moduleComplianceService->handleModuleComplianceUpdate($object);
- $logger->info('ModuleComplianceSubscriber: Successfully processed module compliance update', [
- 'objectId' => $objectId,
- 'timestamp' => date('Y-m-d H:i:s')
- ]);
+ $logger->info(
+ 'ModuleComplianceSubscriber: Successfully processed module compliance update',
+ [
+ 'objectId' => $objectId,
+ 'timestamp' => date('Y-m-d H:i:s'),
+ ]
+ );
} catch (\Exception $e) {
- $logger->error('ModuleComplianceSubscriber: Failed to process module compliance update', [
- 'objectId' => $objectId,
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
- ]);
- }
+ $logger->error(
+ 'ModuleComplianceSubscriber: Failed to process module compliance update',
+ [
+ 'objectId' => $objectId,
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+ }//end try
// Ensure the module has at least one version (default 1.0.0).
try {
$moduleVersionService = $this->container->get(ModuleVersionService::class);
$moduleVersionService->ensureDefaultVersion($object);
} catch (\Exception $e) {
- $logger->error('ModuleComplianceSubscriber: Failed to ensure default module version', [
- 'objectId' => $objectId,
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- ]);
+ $logger->error(
+ 'ModuleComplianceSubscriber: Failed to ensure default module version',
+ [
+ 'objectId' => $objectId,
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]
+ );
}
- }
-}
+ }//end handle()
+}//end class
diff --git a/lib/EventListener/ModuleRegistrationSubscriber.php b/lib/EventListener/ModuleRegistrationSubscriber.php
index ab466a99..02beab25 100644
--- a/lib/EventListener/ModuleRegistrationSubscriber.php
+++ b/lib/EventListener/ModuleRegistrationSubscriber.php
@@ -1,4 +1,18 @@
+ * @copyright 2024 Conduction B.V.
+ * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
+ */
declare(strict_types=1);
@@ -16,23 +30,38 @@
/**
* Event subscriber that auto-sets geregistreerdDoor on module objects
* based on the owning organisation's type.
+ *
+ * @category EventListener
+ * @package OCA\SoftwareCatalog\EventListener
*/
class ModuleRegistrationSubscriber implements IEventListener
{
+ /**
+ * Constructor for ModuleRegistrationSubscriber.
+ *
+ * @param ContainerInterface $container The DI container
+ */
public function __construct(
private readonly ContainerInterface $container
) {
- }
+ }//end __construct()
+ /**
+ * Handle the event.
+ *
+ * @param Event $event The event to handle
+ *
+ * @return void
+ */
public function handle(Event $event): void
{
- if (!($event instanceof ObjectCreatedEvent) && !($event instanceof ObjectUpdatedEvent)) {
+ if (($event instanceof ObjectCreatedEvent) === false && ($event instanceof ObjectUpdatedEvent) === false) {
return;
}
if ($event instanceof ObjectCreatedEvent) {
$object = $event->getObject();
- } elseif ($event instanceof ObjectUpdatedEvent) {
+ } else if ($event instanceof ObjectUpdatedEvent) {
$object = $event->getNewObject();
} else {
return;
@@ -42,9 +71,9 @@ public function handle(Event $event): void
// Check if this is a module object.
$settingsService = $this->container->get(SettingsService::class);
- $moduleSchemaId = $settingsService->getSchemaIdForObjectType('module');
+ $moduleSchemaId = $settingsService->getSchemaIdForObjectType('module');
- if (!$moduleSchemaId || (int) $objectSchemaId !== (int) $moduleSchemaId) {
+ if ($moduleSchemaId === null || (int) $objectSchemaId !== (int) $moduleSchemaId) {
return;
}
@@ -53,10 +82,13 @@ public function handle(Event $event): void
$moduleRegistrationService->handleModuleRegistration($object);
} catch (\Exception $e) {
$logger = $this->container->get(LoggerInterface::class);
- $logger->error('ModuleRegistrationSubscriber: Failed to handle module registration', [
- 'objectId' => $object->getId(),
- 'exception' => $e->getMessage(),
- ]);
+ $logger->error(
+ 'ModuleRegistrationSubscriber: Failed to handle module registration',
+ [
+ 'objectId' => $object->getId(),
+ 'exception' => $e->getMessage(),
+ ]
+ );
}
- }
-}
+ }//end handle()
+}//end class
diff --git a/lib/EventListener/OpenRegisterEventsDebugListener.php b/lib/EventListener/OpenRegisterEventsDebugListener.php
index 83d3ba23..e5caf967 100644
--- a/lib/EventListener/OpenRegisterEventsDebugListener.php
+++ b/lib/EventListener/OpenRegisterEventsDebugListener.php
@@ -14,7 +14,7 @@
* @copyright 2024 Conduction B.V.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
*
- * @version GIT:
+ * @version GIT:
*
* @link https://SoftwareCatalog.app
*/
@@ -64,29 +64,27 @@ class OpenRegisterEventsDebugListener implements IEventListener
/**
* Whether debug logging is enabled
*
- * @var bool
+ * @var boolean
*/
private readonly bool $debugEnabled;
-
/**
* Constructor for the debug listener
*
- * @param LoggerInterface $logger Logger instance for debug output
- * @param bool $debugEnabled Whether debug logging should be enabled
+ * @param LoggerInterface $logger Logger instance for debug output
+ * @param bool $debugEnabled Whether debug logging should be enabled
*
* @return void
*/
public function __construct(
LoggerInterface $logger,
- bool $debugEnabled = true
+ bool $debugEnabled=true
) {
- $this->logger = $logger;
+ $this->logger = $logger;
$this->debugEnabled = $debugEnabled;
}//end __construct()
-
/**
* Handle any OpenRegister event for debugging purposes
*
@@ -102,42 +100,42 @@ public function __construct(
public function handle(Event $event): void
{
$eventClass = get_class($event);
- $eventType = $this->getEventTypeName($eventClass);
+ $eventType = $this->getEventTypeName(eventClass: $eventClass);
- $this->logger->debug('OpenRegister debug listener triggered', [
- 'app' => 'softwarecatalog',
- 'eventType' => $eventType,
- 'eventClass' => $eventClass,
- 'debugEnabled' => $this->debugEnabled,
- ]);
-
+ $this->logger->debug(
+ 'OpenRegister debug listener triggered',
+ [
+ 'app' => 'softwarecatalog',
+ 'eventType' => $eventType,
+ 'eventClass' => $eventClass,
+ 'debugEnabled' => $this->debugEnabled,
+ ]
+ );
-
- if (!$this->debugEnabled) {
- $this->logger->warning('SoftwareCatalog OpenRegister Debug: Debug disabled, skipping detailed logging');
+ if ($this->debugEnabled === false) {
+ $this->logger->warning('SoftwareCatalog OpenRegister Debug: Debug disabled, skipping detailed logging.');
return;
}
- $eventData = $this->extractEventData($event);
+ $eventData = $this->extractEventData(event: $event);
- // Log comprehensive debug information
+ // Log comprehensive debug information.
$this->logger->info(
- '[SoftwareCatalog] 🔍 OPENREGISTER EVENT: {eventType} received from OpenRegister',
+ '[SoftwareCatalog] OPENREGISTER EVENT: {eventType} received from OpenRegister',
[
- 'app' => 'softwarecatalog',
- 'eventType' => $eventType,
- 'eventClass' => $eventClass,
+ 'app' => 'softwarecatalog',
+ 'eventType' => $eventType,
+ 'eventClass' => $eventClass,
'listenerClass' => self::class,
- 'eventData' => $eventData,
- 'timestamp' => date('Y-m-d H:i:s'),
- 'source' => 'OpenRegister',
+ 'eventData' => $eventData,
+ 'timestamp' => date('Y-m-d H:i:s'),
+ 'source' => 'OpenRegister',
]
);
}//end handle()
-
/**
* Extract a human-readable event type name from the class name
*
@@ -150,11 +148,11 @@ public function handle(Event $event): void
*/
private function getEventTypeName(string $eventClass): string
{
- // Extract the class name without namespace
+ // Extract the class name without namespace.
$className = substr($eventClass, strrpos($eventClass, '\\') + 1);
-
- // Remove 'Event' suffix if present
- if (str_ends_with($className, 'Event')) {
+
+ // Remove 'Event' suffix if present.
+ if (str_ends_with(haystack: $className, needle: 'Event') === true) {
$className = substr($className, 0, -5);
}
@@ -162,7 +160,6 @@ private function getEventTypeName(string $eventClass): string
}//end getEventTypeName()
-
/**
* Extract relevant data from the event for debugging
*
@@ -182,153 +179,190 @@ private function extractEventData(Event $event): array
'eventClass' => get_class($event),
];
- // Handle Object events
+ // Handle Object events.
if ($event instanceof ObjectCreatedEvent) {
$object = $event->getObject();
- $data = array_merge($data, [
- 'eventType' => 'ObjectCreated',
- 'objectId' => $object->getId(),
- 'objectUuid' => $object->getUuid(),
- 'registerId' => $object->getRegister(),
- 'schemaId' => $object->getSchema(),
- 'owner' => $object->getOwner(),
- 'created' => $object->getCreated()?->format('Y-m-d H:i:s'),
- 'objectData' => $this->getSafeObjectData($object->getObject()),
- ]);
+ $data = array_merge(
+ $data,
+ [
+ 'eventType' => 'ObjectCreated',
+ 'objectId' => $object->getId(),
+ 'objectUuid' => $object->getUuid(),
+ 'registerId' => $object->getRegister(),
+ 'schemaId' => $object->getSchema(),
+ 'owner' => $object->getOwner(),
+ 'created' => $object->getCreated()?->format('Y-m-d H:i:s'),
+ 'objectData' => $this->getSafeObjectData(objectData: $object->getObject()),
+ ]
+ );
} else if ($event instanceof ObjectUpdatedEvent) {
$newObject = $event->getNewObject();
$oldObject = $event->getOldObject();
- $data = array_merge($data, [
- 'eventType' => 'ObjectUpdated',
- 'newObjectId' => $newObject->getId(),
- 'newObjectUuid' => $newObject->getUuid(),
- 'oldObjectId' => $oldObject?->getId(),
- 'oldObjectUuid' => $oldObject?->getUuid(),
- 'registerId' => $newObject->getRegister(),
- 'schemaId' => $newObject->getSchema(),
- 'owner' => $newObject->getOwner(),
- 'updated' => $newObject->getUpdated()?->format('Y-m-d H:i:s'),
- 'newObjectData' => $this->getSafeObjectData($newObject->getObject()),
- 'oldObjectData' => $oldObject ? $this->getSafeObjectData($oldObject->getObject()) : null,
- ]);
+
+ if ($oldObject !== null) {
+ $oldObjectData = $this->getSafeObjectData(objectData: $oldObject->getObject());
+ } else {
+ $oldObjectData = null;
+ }
+
+ $data = array_merge(
+ $data,
+ [
+ 'eventType' => 'ObjectUpdated',
+ 'newObjectId' => $newObject->getId(),
+ 'newObjectUuid' => $newObject->getUuid(),
+ 'oldObjectId' => $oldObject?->getId(),
+ 'oldObjectUuid' => $oldObject?->getUuid(),
+ 'registerId' => $newObject->getRegister(),
+ 'schemaId' => $newObject->getSchema(),
+ 'owner' => $newObject->getOwner(),
+ 'updated' => $newObject->getUpdated()?->format('Y-m-d H:i:s'),
+ 'newObjectData' => $this->getSafeObjectData(objectData: $newObject->getObject()),
+ 'oldObjectData' => $oldObjectData,
+ ]
+ );
} else if ($event instanceof ObjectDeletedEvent) {
$object = $event->getObject();
- $data = array_merge($data, [
- 'eventType' => 'ObjectDeleted',
- 'objectId' => $object->getId(),
- 'objectUuid' => $object->getUuid(),
- 'registerId' => $object->getRegister(),
- 'schemaId' => $object->getSchema(),
- 'owner' => $object->getOwner(),
- 'objectData' => $this->getSafeObjectData($object->getObject()),
- ]);
+ $data = array_merge(
+ $data,
+ [
+ 'eventType' => 'ObjectDeleted',
+ 'objectId' => $object->getId(),
+ 'objectUuid' => $object->getUuid(),
+ 'registerId' => $object->getRegister(),
+ 'schemaId' => $object->getSchema(),
+ 'owner' => $object->getOwner(),
+ 'objectData' => $this->getSafeObjectData(objectData: $object->getObject()),
+ ]
+ );
} else if ($event instanceof ObjectLockedEvent) {
$object = $event->getObject();
- $data = array_merge($data, [
- 'eventType' => 'ObjectLocked',
- 'objectId' => $object->getId(),
- 'objectUuid' => $object->getUuid(),
- 'registerId' => $object->getRegister(),
- 'schemaId' => $object->getSchema(),
- 'lockedBy' => $object->getLockedBy(),
- 'lockedAt' => $object->getLockedAt()?->format('Y-m-d H:i:s'),
- ]);
+ $data = array_merge(
+ $data,
+ [
+ 'eventType' => 'ObjectLocked',
+ 'objectId' => $object->getId(),
+ 'objectUuid' => $object->getUuid(),
+ 'registerId' => $object->getRegister(),
+ 'schemaId' => $object->getSchema(),
+ 'lockedBy' => $object->getLockedBy(),
+ 'lockedAt' => $object->getLockedAt()?->format('Y-m-d H:i:s'),
+ ]
+ );
} else if ($event instanceof ObjectUnlockedEvent) {
$object = $event->getObject();
- $data = array_merge($data, [
- 'eventType' => 'ObjectUnlocked',
- 'objectId' => $object->getId(),
- 'objectUuid' => $object->getUuid(),
- 'registerId' => $object->getRegister(),
- 'schemaId' => $object->getSchema(),
- ]);
+ $data = array_merge(
+ $data,
+ [
+ 'eventType' => 'ObjectUnlocked',
+ 'objectId' => $object->getId(),
+ 'objectUuid' => $object->getUuid(),
+ 'registerId' => $object->getRegister(),
+ 'schemaId' => $object->getSchema(),
+ ]
+ );
} else if ($event instanceof ObjectRevertedEvent) {
$object = $event->getObject();
- $data = array_merge($data, [
- 'eventType' => 'ObjectReverted',
- 'objectId' => $object->getId(),
- 'objectUuid' => $object->getUuid(),
- 'registerId' => $object->getRegister(),
- 'schemaId' => $object->getSchema(),
- 'revertedTo' => $event->getRevertedToVersion(),
- ]);
- }
-
- // Handle Register events
- else if ($event instanceof RegisterCreatedEvent) {
+ $data = array_merge(
+ $data,
+ [
+ 'eventType' => 'ObjectReverted',
+ 'objectId' => $object->getId(),
+ 'objectUuid' => $object->getUuid(),
+ 'registerId' => $object->getRegister(),
+ 'schemaId' => $object->getSchema(),
+ 'revertedTo' => $event->getRevertedToVersion(),
+ ]
+ );
+ // Handle Register events.
+ } else if ($event instanceof RegisterCreatedEvent) {
$register = $event->getRegister();
- $data = array_merge($data, [
- 'eventType' => 'RegisterCreated',
- 'registerId' => $register->getId(),
- 'registerTitle' => $register->getTitle(),
- 'registerSlug' => $register->getSlug(),
- ]);
+ $data = array_merge(
+ $data,
+ [
+ 'eventType' => 'RegisterCreated',
+ 'registerId' => $register->getId(),
+ 'registerTitle' => $register->getTitle(),
+ 'registerSlug' => $register->getSlug(),
+ ]
+ );
} else if ($event instanceof RegisterUpdatedEvent) {
$register = $event->getRegister();
- $data = array_merge($data, [
- 'eventType' => 'RegisterUpdated',
- 'registerId' => $register->getId(),
- 'registerTitle' => $register->getTitle(),
- 'registerSlug' => $register->getSlug(),
- ]);
+ $data = array_merge(
+ $data,
+ [
+ 'eventType' => 'RegisterUpdated',
+ 'registerId' => $register->getId(),
+ 'registerTitle' => $register->getTitle(),
+ 'registerSlug' => $register->getSlug(),
+ ]
+ );
} else if ($event instanceof RegisterDeletedEvent) {
$register = $event->getRegister();
- $data = array_merge($data, [
- 'eventType' => 'RegisterDeleted',
- 'registerId' => $register->getId(),
- 'registerTitle' => $register->getTitle(),
- 'registerSlug' => $register->getSlug(),
- ]);
- }
-
- // Handle Schema events
- else if ($event instanceof SchemaCreatedEvent) {
+ $data = array_merge(
+ $data,
+ [
+ 'eventType' => 'RegisterDeleted',
+ 'registerId' => $register->getId(),
+ 'registerTitle' => $register->getTitle(),
+ 'registerSlug' => $register->getSlug(),
+ ]
+ );
+ // Handle Schema events.
+ } else if ($event instanceof SchemaCreatedEvent) {
$schema = $event->getSchema();
- $data = array_merge($data, [
- 'eventType' => 'SchemaCreated',
- 'schemaId' => $schema->getId(),
- 'schemaTitle' => $schema->getTitle(),
- 'schemaVersion' => $schema->getVersion(),
- ]);
+ $data = array_merge(
+ $data,
+ [
+ 'eventType' => 'SchemaCreated',
+ 'schemaId' => $schema->getId(),
+ 'schemaTitle' => $schema->getTitle(),
+ 'schemaVersion' => $schema->getVersion(),
+ ]
+ );
} else if ($event instanceof SchemaUpdatedEvent) {
$schema = $event->getSchema();
- $data = array_merge($data, [
- 'eventType' => 'SchemaUpdated',
- 'schemaId' => $schema->getId(),
- 'schemaTitle' => $schema->getTitle(),
- 'schemaVersion' => $schema->getVersion(),
- ]);
+ $data = array_merge(
+ $data,
+ [
+ 'eventType' => 'SchemaUpdated',
+ 'schemaId' => $schema->getId(),
+ 'schemaTitle' => $schema->getTitle(),
+ 'schemaVersion' => $schema->getVersion(),
+ ]
+ );
} else if ($event instanceof SchemaDeletedEvent) {
$schema = $event->getSchema();
- $data = array_merge($data, [
- 'eventType' => 'SchemaDeleted',
- 'schemaId' => $schema->getId(),
- 'schemaTitle' => $schema->getTitle(),
- 'schemaVersion' => $schema->getVersion(),
- ]);
- }
-
- // Handle Organisation events
- else if ($event instanceof OrganisationCreatedEvent) {
+ $data = array_merge(
+ $data,
+ [
+ 'eventType' => 'SchemaDeleted',
+ 'schemaId' => $schema->getId(),
+ 'schemaTitle' => $schema->getTitle(),
+ 'schemaVersion' => $schema->getVersion(),
+ ]
+ );
+ // Handle Organisation events.
+ } else if ($event instanceof OrganisationCreatedEvent) {
$organisation = $event->getOrganisation();
- $data = array_merge($data, [
- 'eventType' => 'OrganisationCreated',
- 'organisationId' => $organisation->getId(),
- 'organisationTitle' => $organisation->getTitle(),
- ]);
- }
-
- // Unknown event type
- else {
+ $data = array_merge(
+ $data,
+ [
+ 'eventType' => 'OrganisationCreated',
+ 'organisationId' => $organisation->getId(),
+ 'organisationTitle' => $organisation->getTitle(),
+ ]
+ );
+ // Unknown event type.
+ } else {
$data['eventType'] = 'Unknown';
- $data['note'] = 'Event type not specifically handled by SoftwareCatalog debug listener';
- }
+ $data['note'] = 'Event type not specifically handled by SoftwareCatalog debug listener';
+ }//end if
return $data;
}//end extractEventData()
-
/**
* Get safe object data for logging (truncated if too large)
*
@@ -341,22 +375,20 @@ private function extractEventData(Event $event): array
*/
private function getSafeObjectData(mixed $objectData): mixed
{
- // Convert to JSON string to check size
+ // Convert to JSON string to check size.
$jsonData = json_encode($objectData);
-
- // If the data is too large (>2KB), truncate it
+
+ // If the data is too large (>2KB), truncate it.
if (strlen($jsonData) > 2048) {
return [
- '_truncated' => true,
+ '_truncated' => true,
'_originalSize' => strlen($jsonData),
- '_preview' => substr($jsonData, 0, 500) . '...',
- '_note' => 'Object data truncated for logging - too large to display fully'
+ '_preview' => substr($jsonData, 0, 500).'...',
+ '_note' => 'Object data truncated for logging - too large to display fully',
];
}
return $objectData;
}//end getSafeObjectData()
-
-
}//end class
diff --git a/lib/EventListener/SoftwareCatalogEventListener.php b/lib/EventListener/SoftwareCatalogEventListener.php
index 12def1ed..175b9357 100644
--- a/lib/EventListener/SoftwareCatalogEventListener.php
+++ b/lib/EventListener/SoftwareCatalogEventListener.php
@@ -10,7 +10,7 @@
* @author Conduction b.v.
* @copyright 2024 Conduction B.V.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/ConductionNL/OpenConnector
*/
@@ -33,35 +33,36 @@
/**
* Event listener for handling software catalog specific events.
- *
- * This listener handles organization, contact, and user (gebruiker) related events
- * in the software catalog, including user management, email notifications, and
+ *
+ * This listener handles organization, contact, and user (gebruiker) related events
+ * in the software catalog, including user management, email notifications, and
* user blocking/unblocking functionality.
- *
+ *
* @category EventListener
* @package OCA\SoftwareCatalog\EventListener
* @author Conduction b.v.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/ConductionNL/OpenConnector
- * @todo This listener should be moved to the software catalog app
+ * @todo This listener should be moved to the software catalog app.
*/
class SoftwareCatalogEventListener implements IEventListener
{
/**
* Constructor for SoftwareCatalogEventListener
*/
- public function __construct() {
- // Empty constructor - we'll get services from the server container
- }
+ public function __construct()
+ {
+ // Empty constructor - we'll get services from the server container.
+ }//end __construct()
/**
* Handles events related to software catalog objects
- *
+ *
* DISABLED: All processing is now handled by cron-based OrganizationSyncService
* to avoid race conditions and ensure consistent processing.
*
- * @param Event $event The event to handle
+ * @param Event $event The event to handle
*
* @return void
*/
@@ -70,656 +71,774 @@ public function handle(Event $event): void
try {
$logger = \OC::$server->get(LoggerInterface::class);
$contactpersoonService = \OC::$server->get(ContactpersoonService::class);
- $settingsService = \OC::$server->get(SettingsService::class);
-
- $logger->info('SoftwareCatalog: Processing event', [
- 'eventType' => get_class($event),
- 'timestamp' => date('Y-m-d H:i:s')
- ]);
-
+ $settingsService = \OC::$server->get(SettingsService::class);
+
+ $logger->info(
+ 'SoftwareCatalog: Processing event',
+ [
+ 'eventType' => get_class($event),
+ 'timestamp' => date('Y-m-d H:i:s'),
+ ]
+ );
+
if ($event instanceof ObjectCreatedEvent) {
- $this->handleObjectCreated($event, $contactpersoonService, $settingsService, $logger);
- } elseif ($event instanceof ObjectUpdatedEvent) {
- $this->handleObjectUpdated($event, $contactpersoonService, $settingsService, $logger);
- } elseif ($event instanceof ObjectDeletedEvent) {
- $this->handleObjectDeleted($event, $contactpersoonService, $settingsService, $logger);
- } elseif ($event instanceof ObjectLockedEvent || $event instanceof ObjectUnlockedEvent || $event instanceof ObjectRevertedEvent) {
- $logger->debug('SoftwareCatalog: Ignoring object lifecycle event', [
- 'eventType' => get_class($event)
- ]);
+ $this->handleObjectCreated(
+ event: $event,
+ contactpersoonService: $contactpersoonService,
+ settingsService: $settingsService,
+ logger: $logger
+ );
+ } else if ($event instanceof ObjectUpdatedEvent) {
+ $this->handleObjectUpdated(
+ event: $event,
+ contactpersoonService: $contactpersoonService,
+ settingsService: $settingsService,
+ logger: $logger
+ );
+ } else if ($event instanceof ObjectDeletedEvent) {
+ $this->handleObjectDeleted(
+ event: $event,
+ contactpersoonService: $contactpersoonService,
+ settingsService: $settingsService,
+ logger: $logger
+ );
+ } else if ($event instanceof ObjectLockedEvent
+ || $event instanceof ObjectUnlockedEvent
+ || $event instanceof ObjectRevertedEvent
+ ) {
+ $logger->debug(
+ 'SoftwareCatalog: Ignoring object lifecycle event',
+ [
+ 'eventType' => get_class($event),
+ ]
+ );
} else {
- $logger->debug('SoftwareCatalog: Unknown event type ignored', [
- 'eventType' => get_class($event)
- ]);
- }
+ $logger->debug(
+ 'SoftwareCatalog: Unknown event type ignored',
+ [
+ 'eventType' => get_class($event),
+ ]
+ );
+ }//end if
} catch (\Exception $e) {
try {
$logger = \OC::$server->get(LoggerInterface::class);
- $logger->error('SoftwareCatalog: Error in event handler', [
- 'eventType' => get_class($event),
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $logger->error(
+ 'SoftwareCatalog: Error in event handler',
+ [
+ 'eventType' => get_class($event),
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
} catch (\Exception $logException) {
- // Silently fail if logging fails - better than breaking the event system
+ // Silently fail if logging fails - better than breaking the event system.
}
- }
- }
-
-
+ }//end try
+ }//end handle()
/**
* Handles object creation events
*
- * @param ObjectCreatedEvent $event The creation event
+ * @param ObjectCreatedEvent $event The creation event
* @param ContactpersoonService $contactpersoonService The contact person service
- * @param SettingsService $settingsService The settings service
- * @param LoggerInterface $logger The logger instance
+ * @param SettingsService $settingsService The settings service
+ * @param LoggerInterface $logger The logger instance
+ *
* @return void
*/
- private function handleObjectCreated(ObjectCreatedEvent $event, ContactpersoonService $contactpersoonService, SettingsService $settingsService, LoggerInterface $logger): void
- {
+ private function handleObjectCreated(
+ ObjectCreatedEvent $event,
+ ContactpersoonService $contactpersoonService,
+ SettingsService $settingsService,
+ LoggerInterface $logger
+ ): void {
$object = $event->getObject();
if ($object === null) {
$logger->warning('SoftwareCatalog: ObjectCreatedEvent received with null object');
return;
}
- $objectSchemaId = $object->getSchema();
- $objectId = $object->getUuid();
+ $objectSchemaId = $object->getSchema();
+ $objectId = $object->getUuid();
$objectRegisterId = $object->getRegister();
-
- // Convert schema ID to integer for consistent comparison
+
+ // Convert schema ID to integer for consistent comparison.
$objectSchemaIdInt = (int) $objectSchemaId;
-
+
$logger->info(
'SoftwareCatalog: Processing object creation',
[
- 'objectId' => $objectId,
- 'schemaId' => $objectSchemaId,
+ 'objectId' => $objectId,
+ 'schemaId' => $objectSchemaId,
'schemaIdInt' => $objectSchemaIdInt,
- 'registerId' => $objectRegisterId,
- 'objectData' => json_encode($object->getObject())
+ 'registerId' => $objectRegisterId,
+ 'objectData' => json_encode($object->getObject()),
]
);
- // Get configuration for different object types
- $organisatieSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'organisatie');
- $contactpersoonSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'contactpersoon');
+ // Get configuration for different object types.
+ $organisatieSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'organisatie');
+ $contactpersoonSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'contactpersoon');
$contactgegevensSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'contactgegevens');
- $gebruikSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'gebruik');
+ $gebruikSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'gebruik');
$logger->debug(
'SoftwareCatalog: Configuration lookup results',
[
- 'organisatieSchemaId' => $organisatieSchemaId,
- 'contactpersoonSchemaId' => $contactpersoonSchemaId,
+ 'organisatieSchemaId' => $organisatieSchemaId,
+ 'contactpersoonSchemaId' => $contactpersoonSchemaId,
'contactgegevensSchemaId' => $contactgegevensSchemaId,
- 'gebruikSchemaId' => $gebruikSchemaId,
- 'objectSchemaId' => $objectSchemaIdInt
+ 'gebruikSchemaId' => $gebruikSchemaId,
+ 'objectSchemaId' => $objectSchemaIdInt,
]
);
- // Check if this is an organization object
- if ($organisatieSchemaId && $objectSchemaIdInt === (int) $organisatieSchemaId) {
+ // Check if this is an organization object.
+ if ($organisatieSchemaId !== null && $objectSchemaIdInt === (int) $organisatieSchemaId) {
$objectData = $object->getObject();
- $status = strtolower($objectData['status'] ?? '');
-
- // Only process active organizations
- if (in_array($status, ['actief', 'active'])) {
- $logger->info('SoftwareCatalog: Processing active organization creation', [
- 'objectId' => $objectId,
- 'status' => $status
- ]);
-
+ $status = strtolower($objectData['status'] ?? '');
+
+ // Only process active organizations.
+ if (in_array(needle: $status, haystack: ['actief', 'active']) === true) {
+ $logger->info(
+ 'SoftwareCatalog: Processing active organization creation',
+ [
+ 'objectId' => $objectId,
+ 'status' => $status,
+ ]
+ );
+
try {
- // Process organization with OrganizationSyncService
+ // Process organization with OrganizationSyncService.
$organizationSyncService = \OC::$server->get('OCA\SoftwareCatalog\Service\OrganizationSyncService');
$result = $organizationSyncService->processSpecificOrganization($object);
-
- $logger->info('SoftwareCatalog: Successfully processed organization creation', [
- 'objectId' => $objectId,
- 'processResult' => $result
- ]);
+
+ $logger->info(
+ 'SoftwareCatalog: Successfully processed organization creation',
+ [
+ 'objectId' => $objectId,
+ 'processResult' => $result,
+ ]
+ );
} catch (\Exception $e) {
- $logger->error('SoftwareCatalog: Failed to process organization creation', [
- 'objectId' => $objectId,
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine()
- ]);
- }
+ $logger->error(
+ 'SoftwareCatalog: Failed to process organization creation',
+ [
+ 'objectId' => $objectId,
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]
+ );
+ }//end try
} else {
- $logger->debug('SoftwareCatalog: Skipping non-active organization creation', [
- 'objectId' => $objectId,
- 'status' => $status
- ]);
- }
+ $logger->debug(
+ 'SoftwareCatalog: Skipping non-active organization creation',
+ [
+ 'objectId' => $objectId,
+ 'status' => $status,
+ ]
+ );
+ }//end if
+
return;
- }
+ }//end if
- // Check if this is a contactpersoon object
- if ($contactpersoonSchemaId && $objectSchemaIdInt === (int) $contactpersoonSchemaId) {
+ // Check if this is a contactpersoon object.
+ if ($contactpersoonSchemaId !== null && $objectSchemaIdInt === (int) $contactpersoonSchemaId) {
$logger->info('SoftwareCatalog: Processing contactpersoon creation', ['objectId' => $objectId]);
$contactpersoonService->processContactpersoon($object);
return;
}
- // Check if this is a contactgegevens object (deprecated - use contactpersoon instead)
- if ($contactgegevensSchemaId && $objectSchemaIdInt === (int) $contactgegevensSchemaId) {
+ // Check if this is a contactgegevens object (deprecated - use contactpersoon instead).
+ if ($contactgegevensSchemaId !== null && $objectSchemaIdInt === (int) $contactgegevensSchemaId) {
$logger->info('SoftwareCatalog: Processing contactgegevens creation (deprecated)', ['objectId' => $objectId]);
- // Contactgegevens is deprecated, use contactpersoon instead
+ // Contactgegevens is deprecated, use contactpersoon instead.
return;
}
- // Check if this is a gebruik object
- if ($gebruikSchemaId && $objectSchemaIdInt === (int) $gebruikSchemaId) {
+ // Check if this is a gebruik object.
+ if ($gebruikSchemaId !== null && $objectSchemaIdInt === (int) $gebruikSchemaId) {
$logger->info('SoftwareCatalog: Processing gebruik creation', ['objectId' => $objectId]);
-
+
try {
- // Process gebruik object with GebruikSyncService
+ // Process gebruik object with GebruikSyncService.
$gebruikSyncService = \OC::$server->get(GebruikSyncService::class);
$result = $gebruikSyncService->processSpecificGebruik($object);
-
- $logger->info('SoftwareCatalog: Successfully processed gebruik creation', [
- 'objectId' => $objectId,
- 'processResult' => $result
- ]);
+
+ $logger->info(
+ 'SoftwareCatalog: Successfully processed gebruik creation',
+ [
+ 'objectId' => $objectId,
+ 'processResult' => $result,
+ ]
+ );
} catch (\Exception $e) {
- $logger->error('SoftwareCatalog: Failed to process gebruik creation', [
- 'objectId' => $objectId,
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine()
- ]);
- }
+ $logger->error(
+ 'SoftwareCatalog: Failed to process gebruik creation',
+ [
+ 'objectId' => $objectId,
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]
+ );
+ }//end try
+
return;
- }
+ }//end if
- // Log unhandled object types
+ // Log unhandled object types.
$logger->debug(
'SoftwareCatalog: Object creation not handled - not a supported object type',
[
- 'objectId' => $objectId,
- 'schemaId' => $objectSchemaIdInt,
- 'registerId' => $objectRegisterId,
+ 'objectId' => $objectId,
+ 'schemaId' => $objectSchemaIdInt,
+ 'registerId' => $objectRegisterId,
'supportedSchemas' => [
- 'organisatie' => $organisatieSchemaId,
- 'contactpersoon' => $contactpersoonSchemaId,
+ 'organisatie' => $organisatieSchemaId,
+ 'contactpersoon' => $contactpersoonSchemaId,
'contactgegevens' => $contactgegevensSchemaId,
- 'gebruik' => $gebruikSchemaId
- ]
+ 'gebruik' => $gebruikSchemaId,
+ ],
]
);
- }
+ }//end handleObjectCreated()
/**
* Handles object update events
*
- * @param ObjectUpdatedEvent $event The update event
+ * @param ObjectUpdatedEvent $event The update event
* @param ContactpersoonService $contactpersoonService The contact person service
- * @param SettingsService $settingsService The settings service
- * @param LoggerInterface $logger The logger instance
+ * @param SettingsService $settingsService The settings service
+ * @param LoggerInterface $logger The logger instance
+ *
* @return void
*/
- private function handleObjectUpdated(ObjectUpdatedEvent $event, ContactpersoonService $contactpersoonService, SettingsService $settingsService, LoggerInterface $logger): void
- {
- $object = $event->getNewObject();
+ private function handleObjectUpdated(
+ ObjectUpdatedEvent $event,
+ ContactpersoonService $contactpersoonService,
+ SettingsService $settingsService,
+ LoggerInterface $logger
+ ): void {
+ $object = $event->getNewObject();
$oldObject = $event->getOldObject();
-
+
if ($object === null) {
$logger->warning('SoftwareCatalog: ObjectUpdatedEvent received with null object');
return;
}
- $objectSchemaId = $object->getSchema();
- $objectId = $object->getUuid();
+ $objectSchemaId = $object->getSchema();
+ $objectId = $object->getUuid();
$objectRegisterId = $object->getRegister();
-
- // Convert schema ID to integer for consistent comparison
+
+ // Convert schema ID to integer for consistent comparison.
$objectSchemaIdInt = (int) $objectSchemaId;
-
+
$logger->info(
'SoftwareCatalog: Processing object update',
[
- 'objectId' => $objectId,
- 'schemaId' => $objectSchemaId,
- 'schemaIdInt' => $objectSchemaIdInt,
- 'registerId' => $objectRegisterId,
- 'hasOldObject' => $oldObject !== null
+ 'objectId' => $objectId,
+ 'schemaId' => $objectSchemaId,
+ 'schemaIdInt' => $objectSchemaIdInt,
+ 'registerId' => $objectRegisterId,
+ 'hasOldObject' => $oldObject !== null,
]
);
-
- // Check if this is an organization update
- $organisatieSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'organisatie');
+
+ // Check if this is an organization update.
+ $organisatieSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'organisatie');
$organisatieSchemaIdInt = (int) $organisatieSchemaId;
-
- $logger->debug('Got organisation schema ID', [
- 'app' => 'softwarecatalog',
- 'organisatieSchemaId' => $organisatieSchemaId,
- 'organisatieSchemaIdInt' => $organisatieSchemaIdInt
- ]);
-
- $logger->debug('Organization schema check', [
- 'app' => 'softwarecatalog',
- 'objectSchemaId' => $objectSchemaId,
- 'objectSchemaIdInt' => $objectSchemaIdInt,
- 'organisatieSchemaId' => $organisatieSchemaId,
- 'organisatieSchemaIdInt' => $organisatieSchemaIdInt,
- 'matches' => ($objectSchemaIdInt === $organisatieSchemaIdInt)
- ]);
-
- if ($organisatieSchemaId && $objectSchemaIdInt === $organisatieSchemaIdInt) {
+
+ $logger->debug(
+ 'Got organisation schema ID',
+ [
+ 'app' => 'softwarecatalog',
+ 'organisatieSchemaId' => $organisatieSchemaId,
+ 'organisatieSchemaIdInt' => $organisatieSchemaIdInt,
+ ]
+ );
+
+ $logger->debug(
+ 'Organization schema check',
+ [
+ 'app' => 'softwarecatalog',
+ 'objectSchemaId' => $objectSchemaId,
+ 'objectSchemaIdInt' => $objectSchemaIdInt,
+ 'organisatieSchemaId' => $organisatieSchemaId,
+ 'organisatieSchemaIdInt' => $organisatieSchemaIdInt,
+ 'matches' => ($objectSchemaIdInt === $organisatieSchemaIdInt),
+ ]
+ );
+
+ if ($organisatieSchemaId !== null && $objectSchemaIdInt === $organisatieSchemaIdInt) {
$objectData = $object->getObject();
- $status = strtolower($objectData['status'] ?? '');
- $oldStatus = $oldObject ? strtolower($oldObject->getObject()['status'] ?? '') : '';
-
- $logger->debug('Organization status check', [
- 'app' => 'softwarecatalog',
- 'objectId' => $objectId,
- 'status' => $status,
- 'oldStatus' => $oldStatus,
- 'statusChanged' => ($status !== $oldStatus),
- 'isActief' => in_array($status, ['actief', 'active']),
- 'willProcess' => (in_array($status, ['actief', 'active']) && $status !== $oldStatus)
- ]);
-
- // Only process active organizations
- if (in_array($status, ['actief', 'active']) === true && $status !== $oldStatus) {
- $logger->info('SoftwareCatalog: Processing active organization update', [
- 'objectId' => $objectId,
- 'status' => $status,
- 'schemaId' => $objectSchemaId
- ]);
-
+ $status = strtolower($objectData['status'] ?? '');
+
+ if ($oldObject !== null) {
+ $oldStatus = strtolower($oldObject->getObject()['status'] ?? '');
+ } else {
+ $oldStatus = '';
+ }
+
+ $logger->debug(
+ 'Organization status check',
+ [
+ 'app' => 'softwarecatalog',
+ 'objectId' => $objectId,
+ 'status' => $status,
+ 'oldStatus' => $oldStatus,
+ 'statusChanged' => ($status !== $oldStatus),
+ 'isActief' => in_array(needle: $status, haystack: ['actief', 'active']),
+ 'willProcess' => (in_array(needle: $status, haystack: ['actief', 'active']) === true
+ && $status !== $oldStatus),
+ ]
+ );
+
+ // Only process active organizations.
+ if (in_array(needle: $status, haystack: ['actief', 'active']) === true && $status !== $oldStatus) {
+ $logger->info(
+ 'SoftwareCatalog: Processing active organization update',
+ [
+ 'objectId' => $objectId,
+ 'status' => $status,
+ 'schemaId' => $objectSchemaId,
+ ]
+ );
+
try {
- // Refetch organization WITH contactpersonen expanded to get full contact data
+ // Refetch organization WITH contactpersonen expanded to get full contact data.
$voorzieningenConfig = $settingsService->getVoorzieningenConfig();
- $register = $voorzieningenConfig['register'] ?? '';
- $organizationSchema = $voorzieningenConfig['organisatie_schema'] ?? '';
-
- $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
+ $register = $voorzieningenConfig['register'] ?? '';
+ $organizationSchema = $voorzieningenConfig['organisatie_schema'] ?? '';
+
+ $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
$organizationWithContacts = $objectService->find(
id: $objectId,
register: $register,
schema: $organizationSchema,
- _extend: ['contactpersonen'], // This expands contactpersonen with full data!
+ // This expands contactpersonen with full data!
+ _extend: ['contactpersonen'],
_rbac: false,
_multitenancy: false
);
-
- $logger->info('SoftwareCatalog: Refetched organization with contactpersonen', [
- 'objectId' => $objectId,
- 'contactperso nenCount' => count($organizationWithContacts->getObject()['contactpersonen'] ?? [])
- ]);
-
- // Process organization with OrganizationSyncService
+
+ $logger->info(
+ 'SoftwareCatalog: Refetched organization with contactpersonen',
+ [
+ 'objectId' => $objectId,
+ 'contactpersonenCount' => count(
+ $organizationWithContacts->getObject()['contactpersonen'] ?? []
+ ),
+ ]
+ );
+
+ // Process organization with OrganizationSyncService.
$organizationSyncService = \OC::$server->get('OCA\SoftwareCatalog\Service\OrganizationSyncService');
$result = $organizationSyncService->processSpecificOrganization($organizationWithContacts);
-
- $logger->info('SoftwareCatalog: Successfully processed organization update', [
- 'objectId' => $objectId,
- 'processResult' => $result
- ]);
+
+ $logger->info(
+ 'SoftwareCatalog: Successfully processed organization update',
+ [
+ 'objectId' => $objectId,
+ 'processResult' => $result,
+ ]
+ );
} catch (\Exception $e) {
- $logger->error('SoftwareCatalog: Failed to process organization update', [
- 'objectId' => $objectId,
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine()
- ]);
- }
+ $logger->error(
+ 'SoftwareCatalog: Failed to process organization update',
+ [
+ 'objectId' => $objectId,
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]
+ );
+ }//end try
} else {
- $logger->debug('SoftwareCatalog: Skipping non-active organization update', [
- 'objectId' => $objectId,
- 'status' => $status,
- 'schemaId' => $objectSchemaId
- ]);
- }
+ $logger->debug(
+ 'SoftwareCatalog: Skipping non-active organization update',
+ [
+ 'objectId' => $objectId,
+ 'status' => $status,
+ 'schemaId' => $objectSchemaId,
+ ]
+ );
+ }//end if
+
return;
- }
-
- // Handle contactpersoon updates
- $contactpersoonSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'contactpersoon');
+ }//end if
+
+ // Handle contactpersoon updates.
+ $contactpersoonSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'contactpersoon');
$contactpersoonSchemaIdInt = (int) $contactpersoonSchemaId;
-
- if ($contactpersoonSchemaId && $objectSchemaIdInt === $contactpersoonSchemaIdInt) {
+
+ if ($contactpersoonSchemaId !== null && $objectSchemaIdInt === $contactpersoonSchemaIdInt) {
$logger->info(
'SoftwareCatalog: Matched contactpersoon schema - processing update',
[
- 'objectId' => $objectId,
- 'schemaId' => $objectSchemaId,
- 'configuredSchemaId' => $contactpersoonSchemaId
+ 'objectId' => $objectId,
+ 'schemaId' => $objectSchemaId,
+ 'configuredSchemaId' => $contactpersoonSchemaId,
]
);
-
+
try {
- $contactpersoonService->handleContactpersoonUpdate($object, $oldObject);
-
+ $contactpersoonService->handleContactpersoonUpdate(
+ contactpersoonObject: $object,
+ oldContactpersoonObject: $oldObject
+ );
+
$logger->info(
'SoftwareCatalog: Successfully processed contactpersoon update',
[
- 'objectId' => $objectId,
- 'timestamp' => date('Y-m-d H:i:s')
+ 'objectId' => $objectId,
+ 'timestamp' => date('Y-m-d H:i:s'),
]
);
} catch (\Exception $e) {
$logger->error(
'SoftwareCatalog: Failed to process contactpersoon update',
[
- 'objectId' => $objectId,
+ 'objectId' => $objectId,
'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
]
);
- }
+ }//end try
+
return;
- }
-
- // Handle contactgegevens updates (backward compatibility)
- $contactgegevensSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'contactgegevens');
+ }//end if
+
+ // Handle contactgegevens updates (backward compatibility).
+ $contactgegevensSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'contactgegevens');
$contactgegevensSchemaIdInt = (int) $contactgegevensSchemaId;
-
- if ($contactgegevensSchemaId && $objectSchemaIdInt === $contactgegevensSchemaIdInt) {
+
+ if ($contactgegevensSchemaId !== null && $objectSchemaIdInt === $contactgegevensSchemaIdInt) {
$logger->info(
'SoftwareCatalog: Matched contactgegevens schema - processing update (backward compatibility)',
[
- 'objectId' => $objectId,
- 'schemaId' => $objectSchemaId,
- 'configuredSchemaId' => $contactgegevensSchemaId
+ 'objectId' => $objectId,
+ 'schemaId' => $objectSchemaId,
+ 'configuredSchemaId' => $contactgegevensSchemaId,
]
);
-
+
try {
- // Handle contactgegevens as contactpersoon (backward compatibility)
- $contactpersoonService->handleContactpersoonUpdate($object, $oldObject);
-
+ // Handle contactgegevens as contactpersoon (backward compatibility).
+ $contactpersoonService->handleContactpersoonUpdate(
+ contactpersoonObject: $object,
+ oldContactpersoonObject: $oldObject
+ );
+
$logger->info(
'SoftwareCatalog: Successfully processed contactgegevens update (as contactpersoon)',
[
- 'objectId' => $objectId,
- 'timestamp' => date('Y-m-d H:i:s')
+ 'objectId' => $objectId,
+ 'timestamp' => date('Y-m-d H:i:s'),
]
);
} catch (\Exception $e) {
$logger->error(
'SoftwareCatalog: Failed to process contactgegevens update',
[
- 'objectId' => $objectId,
+ 'objectId' => $objectId,
'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
]
);
- }
+ }//end try
+
return;
- }
+ }//end if
- // Handle gebruik updates
- $gebruikSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'gebruik');
+ // Handle gebruik updates.
+ $gebruikSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'gebruik');
$gebruikSchemaIdInt = (int) $gebruikSchemaId;
-
- if ($gebruikSchemaId && $objectSchemaIdInt === $gebruikSchemaIdInt) {
+
+ if ($gebruikSchemaId !== null && $objectSchemaIdInt === $gebruikSchemaIdInt) {
$logger->info(
'SoftwareCatalog: Matched gebruik schema - processing update',
[
- 'objectId' => $objectId,
- 'schemaId' => $objectSchemaId,
- 'configuredSchemaId' => $gebruikSchemaId
+ 'objectId' => $objectId,
+ 'schemaId' => $objectSchemaId,
+ 'configuredSchemaId' => $gebruikSchemaId,
]
);
-
+
try {
- // Process gebruik object with GebruikSyncService
+ // Process gebruik object with GebruikSyncService.
$gebruikSyncService = \OC::$server->get(GebruikSyncService::class);
$result = $gebruikSyncService->processSpecificGebruik($object);
-
+
$logger->info(
'SoftwareCatalog: Successfully processed gebruik update',
[
- 'objectId' => $objectId,
+ 'objectId' => $objectId,
'processResult' => $result,
- 'timestamp' => date('Y-m-d H:i:s')
+ 'timestamp' => date('Y-m-d H:i:s'),
]
);
} catch (\Exception $e) {
$logger->error(
'SoftwareCatalog: Failed to process gebruik update',
[
- 'objectId' => $objectId,
+ 'objectId' => $objectId,
'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
]
);
- }
+ }//end try
+
return;
- }
+ }//end if
- // Log if we don't handle this schema type
+ // Log if we don't handle this schema type.
$logger->debug(
'SoftwareCatalog: Object update not handled - focusing only on organisatie, contactpersonen, and gebruik',
[
- 'objectId' => $objectId,
- 'schemaId' => $objectSchemaId,
- 'schemaIdInt' => $objectSchemaIdInt,
- 'schemaIdType' => gettype($objectSchemaId),
- 'registerId' => $objectRegisterId,
+ 'objectId' => $objectId,
+ 'schemaId' => $objectSchemaId,
+ 'schemaIdInt' => $objectSchemaIdInt,
+ 'schemaIdType' => gettype($objectSchemaId),
+ 'registerId' => $objectRegisterId,
'handledSchemas' => [
- 'organisatie' => $organisatieSchemaId,
- 'contactpersoon' => $contactpersoonSchemaId,
+ 'organisatie' => $organisatieSchemaId,
+ 'contactpersoon' => $contactpersoonSchemaId,
'contactgegevens' => $contactgegevensSchemaId,
- 'gebruik' => $gebruikSchemaId
- ]
+ 'gebruik' => $gebruikSchemaId,
+ ],
]
);
- }
+ }//end handleObjectUpdated()
/**
* Handles object deletion events
*
- * @param ObjectDeletedEvent $event The deletion event
+ * @param ObjectDeletedEvent $event The deletion event
* @param ContactpersoonService $contactpersoonService The contact person service
- * @param SettingsService $settingsService The settings service
- * @param LoggerInterface $logger The logger instance
+ * @param SettingsService $settingsService The settings service
+ * @param LoggerInterface $logger The logger instance
+ *
* @return void
*/
- private function handleObjectDeleted(ObjectDeletedEvent $event, ContactpersoonService $contactpersoonService, SettingsService $settingsService, LoggerInterface $logger): void
- {
+ private function handleObjectDeleted(
+ ObjectDeletedEvent $event,
+ ContactpersoonService $contactpersoonService,
+ SettingsService $settingsService,
+ LoggerInterface $logger
+ ): void {
$object = $event->getObject();
if ($object === null) {
$logger->warning('SoftwareCatalog: ObjectDeletedEvent received with null object');
return;
}
- $objectSchemaId = $object->getSchema();
- $objectId = $object->getUuid();
+ $objectSchemaId = $object->getSchema();
+ $objectId = $object->getUuid();
$objectRegisterId = $object->getRegister();
-
+
$logger->info(
'SoftwareCatalog: Processing object deletion',
[
- 'objectId' => $objectId,
- 'schemaId' => $objectSchemaId,
+ 'objectId' => $objectId,
+ 'schemaId' => $objectSchemaId,
'registerId' => $objectRegisterId,
- 'objectData' => $object->getObject()
+ 'objectData' => $object->getObject(),
]
);
-
- // Check if this is an organization deletion
- $organisatieSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'organisatie');
+
+ // Check if this is an organization deletion.
+ $organisatieSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'organisatie');
$organisatieSchemaIdInt = (int) $organisatieSchemaId;
- $objectSchemaIdInt = (int) $objectSchemaId;
-
- if ($organisatieSchemaId && $objectSchemaIdInt === $organisatieSchemaIdInt) {
+ $objectSchemaIdInt = (int) $objectSchemaId;
+
+ if ($organisatieSchemaId !== null && $objectSchemaIdInt === $organisatieSchemaIdInt) {
$logger->info('SoftwareCatalog: Processing organization deletion', ['objectId' => $objectId]);
-
+
try {
- // For deletions, we may need to handle cleanup regardless of status
- // The OrganizationSyncService can determine what cleanup is needed
+ // For deletions, we may need to handle cleanup regardless of status.
+ // The OrganizationSyncService can determine what cleanup is needed.
$organizationSyncService = \OC::$server->get('OCA\SoftwareCatalog\Service\OrganizationSyncService');
-
- // Note: processSpecificOrganization may handle cleanup for deleted organizations
- // The service can check if the organization exists and handle accordingly
+
+ // Note: processSpecificOrganization may handle cleanup for deleted organizations.
+ // The service can check if the organization exists and handle accordingly.
$result = $organizationSyncService->processSpecificOrganization($object);
-
- $logger->info('SoftwareCatalog: Successfully processed organization deletion', [
- 'objectId' => $objectId,
- 'processResult' => $result
- ]);
+
+ $logger->info(
+ 'SoftwareCatalog: Successfully processed organization deletion',
+ [
+ 'objectId' => $objectId,
+ 'processResult' => $result,
+ ]
+ );
} catch (\Exception $e) {
- $logger->error('SoftwareCatalog: Failed to process organization deletion', [
- 'objectId' => $objectId,
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine()
- ]);
- }
+ $logger->error(
+ 'SoftwareCatalog: Failed to process organization deletion',
+ [
+ 'objectId' => $objectId,
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]
+ );
+ }//end try
+
return;
- }
-
- // Handle contactpersoon deletion
- $contactpersoonSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'contactpersoon');
+ }//end if
+
+ // Handle contactpersoon deletion.
+ $contactpersoonSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'contactpersoon');
$contactpersoonSchemaIdInt = (int) $contactpersoonSchemaId;
-
- if ($contactpersoonSchemaId && $objectSchemaIdInt === $contactpersoonSchemaIdInt) {
+
+ if ($contactpersoonSchemaId !== null && $objectSchemaIdInt === $contactpersoonSchemaIdInt) {
$logger->info(
'SoftwareCatalog: Matched contactpersoon schema - processing deletion',
[
- 'objectId' => $objectId,
- 'schemaId' => $objectSchemaId,
- 'configuredSchemaId' => $contactpersoonSchemaId
+ 'objectId' => $objectId,
+ 'schemaId' => $objectSchemaId,
+ 'configuredSchemaId' => $contactpersoonSchemaId,
]
);
-
+
try {
$contactpersoonService->handleContactDeletion($object);
-
+
$logger->info(
'SoftwareCatalog: Successfully processed contactpersoon deletion',
[
- 'objectId' => $objectId,
- 'timestamp' => date('Y-m-d H:i:s')
+ 'objectId' => $objectId,
+ 'timestamp' => date('Y-m-d H:i:s'),
]
);
} catch (\Exception $e) {
$logger->error(
'SoftwareCatalog: Failed to process contactpersoon deletion',
[
- 'objectId' => $objectId,
+ 'objectId' => $objectId,
'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
]
);
- }
+ }//end try
+
return;
- }
-
- // Handle contactgegevens deletion (backward compatibility)
- $contactgegevensSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'contactgegevens');
+ }//end if
+
+ // Handle contactgegevens deletion (backward compatibility).
+ $contactgegevensSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'contactgegevens');
$contactgegevensSchemaIdInt = (int) $contactgegevensSchemaId;
-
- if ($contactgegevensSchemaId && $objectSchemaIdInt === $contactgegevensSchemaIdInt) {
+
+ if ($contactgegevensSchemaId !== null && $objectSchemaIdInt === $contactgegevensSchemaIdInt) {
$logger->info(
'SoftwareCatalog: Matched contactgegevens schema - processing deletion (backward compatibility)',
[
- 'objectId' => $objectId,
- 'schemaId' => $objectSchemaId,
- 'configuredSchemaId' => $contactgegevensSchemaId
+ 'objectId' => $objectId,
+ 'schemaId' => $objectSchemaId,
+ 'configuredSchemaId' => $contactgegevensSchemaId,
]
);
-
+
try {
$contactpersoonService->handleContactDeletion($object);
-
+
$logger->info(
'SoftwareCatalog: Successfully processed contactgegevens deletion',
[
- 'objectId' => $objectId,
- 'timestamp' => date('Y-m-d H:i:s')
+ 'objectId' => $objectId,
+ 'timestamp' => date('Y-m-d H:i:s'),
]
);
} catch (\Exception $e) {
$logger->error(
'SoftwareCatalog: Failed to process contactgegevens deletion',
[
- 'objectId' => $objectId,
+ 'objectId' => $objectId,
'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
]
);
- }
+ }//end try
+
return;
- }
+ }//end if
- // Handle gebruik deletion
- $gebruikSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'gebruik');
+ // Handle gebruik deletion.
+ $gebruikSchemaId = $settingsService->getSchemaIdForObjectType(objectType: 'gebruik');
$gebruikSchemaIdInt = (int) $gebruikSchemaId;
-
- if ($gebruikSchemaId && $objectSchemaIdInt === $gebruikSchemaIdInt) {
+
+ if ($gebruikSchemaId !== null && $objectSchemaIdInt === $gebruikSchemaIdInt) {
$objectData = $object->getObject();
-
+
$logger->info(
'SoftwareCatalog: Matched gebruik schema - processing deletion',
[
- 'objectId' => $objectId,
- 'schemaId' => $objectSchemaId,
+ 'objectId' => $objectId,
+ 'schemaId' => $objectSchemaId,
'configuredSchemaId' => $gebruikSchemaId,
- 'afnemer' => $objectData['afnemer']['naam'] ?? 'Unknown',
- 'product' => $objectData['product']['naam'] ?? 'Unknown'
+ 'afnemer' => $objectData['afnemer']['naam'] ?? 'Unknown',
+ 'product' => $objectData['product']['naam'] ?? 'Unknown',
]
);
-
- // For deletions, we mainly log the event since the object is being removed
- // No specific cleanup needed for gebruik objects currently
+
+ // For deletions, we mainly log the event since the object is being removed.
+ // No specific cleanup needed for gebruik objects currently.
$logger->info(
'SoftwareCatalog: Gebruik object deleted - no specific cleanup required',
[
- 'objectId' => $objectId,
- 'timestamp' => date('Y-m-d H:i:s')
+ 'objectId' => $objectId,
+ 'timestamp' => date('Y-m-d H:i:s'),
]
);
return;
- }
+ }//end if
- // Log if we don't handle this schema type
+ // Log if we don't handle this schema type.
$logger->debug(
'SoftwareCatalog: Object deletion not handled - focusing only on organisatie, contactpersonen, and gebruik',
[
- 'objectId' => $objectId,
- 'schemaId' => $objectSchemaId,
- 'registerId' => $objectRegisterId,
+ 'objectId' => $objectId,
+ 'schemaId' => $objectSchemaId,
+ 'registerId' => $objectRegisterId,
'handledSchemas' => [
- 'organisatie' => $organisatieSchemaId,
- 'contactpersoon' => $contactpersoonSchemaId,
+ 'organisatie' => $organisatieSchemaId,
+ 'contactpersoon' => $contactpersoonSchemaId,
'contactgegevens' => $contactgegevensSchemaId,
- 'gebruik' => $gebruikSchemaId
- ]
+ 'gebruik' => $gebruikSchemaId,
+ ],
]
);
- }
+ }//end handleObjectDeleted()
/**
* Handles object locking events
*
- * @param ObjectLockedEvent $event The locking event
- * @param SoftwareCatalogueService $softwareCatalogueService The software catalog service
- * @param SettingsService $settingsService The settings service
- * @param LoggerInterface $logger The logger instance
+ * @param ObjectLockedEvent $event The locking event
+ * @param SettingsService $settingsService The settings service
+ * @param LoggerInterface $logger The logger instance
+ *
* @return void
*/
- private function handleObjectLocked(ObjectLockedEvent $event, SoftwareCatalogueService $softwareCatalogueService, SettingsService $settingsService, LoggerInterface $logger): void
- {
+ private function handleObjectLocked(
+ ObjectLockedEvent $event,
+ SettingsService $settingsService,
+ LoggerInterface $logger
+ ): void {
$object = $event->getObject();
if ($object === null) {
$logger->warning('SoftwareCatalog: ObjectLockedEvent received with null object');
@@ -727,38 +846,41 @@ private function handleObjectLocked(ObjectLockedEvent $event, SoftwareCatalogueS
}
$objectSchemaId = $object->getSchema();
- $objectId = $object->getUuid();
-
+ $objectId = $object->getUuid();
+
$logger->info(
'SoftwareCatalog: Processing object locking',
[
- 'objectId' => $objectId,
- 'schemaId' => $objectSchemaId,
- 'timestamp' => date('Y-m-d H:i:s')
+ 'objectId' => $objectId,
+ 'schemaId' => $objectSchemaId,
+ 'timestamp' => date('Y-m-d H:i:s'),
]
);
-
- // Currently no specific handling for locking events
+
+ // Currently no specific handling for locking events.
$logger->debug(
'SoftwareCatalog: Object locking event received but no specific handling implemented',
[
'objectId' => $objectId,
- 'schemaId' => $objectSchemaId
+ 'schemaId' => $objectSchemaId,
]
);
- }
+ }//end handleObjectLocked()
/**
* Handles object unlocking events
*
- * @param ObjectUnlockedEvent $event The unlocking event
- * @param SoftwareCatalogueService $softwareCatalogueService The software catalog service
- * @param SettingsService $settingsService The settings service
- * @param LoggerInterface $logger The logger instance
+ * @param ObjectUnlockedEvent $event The unlocking event
+ * @param SettingsService $settingsService The settings service
+ * @param LoggerInterface $logger The logger instance
+ *
* @return void
*/
- private function handleObjectUnlocked(ObjectUnlockedEvent $event, SoftwareCatalogueService $softwareCatalogueService, SettingsService $settingsService, LoggerInterface $logger): void
- {
+ private function handleObjectUnlocked(
+ ObjectUnlockedEvent $event,
+ SettingsService $settingsService,
+ LoggerInterface $logger
+ ): void {
$object = $event->getObject();
if ($object === null) {
$logger->warning('SoftwareCatalog: ObjectUnlockedEvent received with null object');
@@ -766,38 +888,41 @@ private function handleObjectUnlocked(ObjectUnlockedEvent $event, SoftwareCatalo
}
$objectSchemaId = $object->getSchema();
- $objectId = $object->getUuid();
-
+ $objectId = $object->getUuid();
+
$logger->info(
'SoftwareCatalog: Processing object unlocking',
[
- 'objectId' => $objectId,
- 'schemaId' => $objectSchemaId,
- 'timestamp' => date('Y-m-d H:i:s')
+ 'objectId' => $objectId,
+ 'schemaId' => $objectSchemaId,
+ 'timestamp' => date('Y-m-d H:i:s'),
]
);
-
- // Currently no specific handling for unlocking events
+
+ // Currently no specific handling for unlocking events.
$logger->debug(
'SoftwareCatalog: Object unlocking event received but no specific handling implemented',
[
'objectId' => $objectId,
- 'schemaId' => $objectSchemaId
+ 'schemaId' => $objectSchemaId,
]
);
- }
+ }//end handleObjectUnlocked()
/**
* Handles object reversion events
*
- * @param ObjectRevertedEvent $event The reversion event
- * @param SoftwareCatalogueService $softwareCatalogueService The software catalog service
- * @param SettingsService $settingsService The settings service
- * @param LoggerInterface $logger The logger instance
+ * @param ObjectRevertedEvent $event The reversion event
+ * @param SettingsService $settingsService The settings service
+ * @param LoggerInterface $logger The logger instance
+ *
* @return void
*/
- private function handleObjectReverted(ObjectRevertedEvent $event, SoftwareCatalogueService $softwareCatalogueService, SettingsService $settingsService, LoggerInterface $logger): void
- {
+ private function handleObjectReverted(
+ ObjectRevertedEvent $event,
+ SettingsService $settingsService,
+ LoggerInterface $logger
+ ): void {
$object = $event->getObject();
if ($object === null) {
$logger->warning('SoftwareCatalog: ObjectRevertedEvent received with null object');
@@ -805,24 +930,24 @@ private function handleObjectReverted(ObjectRevertedEvent $event, SoftwareCatalo
}
$objectSchemaId = $object->getSchema();
- $objectId = $object->getUuid();
-
+ $objectId = $object->getUuid();
+
$logger->info(
'SoftwareCatalog: Processing object reversion',
[
- 'objectId' => $objectId,
- 'schemaId' => $objectSchemaId,
- 'timestamp' => date('Y-m-d H:i:s')
+ 'objectId' => $objectId,
+ 'schemaId' => $objectSchemaId,
+ 'timestamp' => date('Y-m-d H:i:s'),
]
);
-
- // Currently no specific handling for reversion events
+
+ // Currently no specific handling for reversion events.
$logger->debug(
'SoftwareCatalog: Object reversion event received but no specific handling implemented',
[
'objectId' => $objectId,
- 'schemaId' => $objectSchemaId
+ 'schemaId' => $objectSchemaId,
]
);
- }
-}
\ No newline at end of file
+ }//end handleObjectReverted()
+}//end class
diff --git a/lib/EventListener/TestEventListener.php b/lib/EventListener/TestEventListener.php
index 0996f61b..904d5c14 100644
--- a/lib/EventListener/TestEventListener.php
+++ b/lib/EventListener/TestEventListener.php
@@ -10,7 +10,7 @@
* @author Conduction b.v.
* @copyright 2024 Conduction B.V.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/ConductionNL/OpenConnector
*/
@@ -25,33 +25,33 @@
/**
* Test event listener for verifying event listener functionality.
- *
- * This listener handles user login events to test that our event system
- * is working correctly. It logs when users log in and can be easily
+ *
+ * This listener handles user login events to test that our event system
+ * is working correctly. It logs when users log in and can be easily
* triggered for testing purposes.
- *
+ *
* @category EventListener
* @package OCA\SoftwareCatalog\EventListener
* @author Conduction b.v.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/ConductionNL/OpenConnector
*/
class TestEventListener implements IEventListener
{
/**
* Constructor for TestEventListener
- *
+ *
* @param LoggerInterface $logger The logger service for logging events
*/
public function __construct(
private readonly LoggerInterface $logger
) {
- }
+ }//end __construct()
/**
* Handles events related to user login for testing purposes
- *
+ *
* This method processes UserLoggedInEvent events and logs detailed
* information to verify that the event listener system is working
* correctly.
@@ -62,48 +62,59 @@ public function __construct(
*/
public function handle(Event $event): void
{
- // Log that we received ANY event first
- $this->logger->info('SoftwareCatalog TestEventListener: Event received!', [
- 'eventClass' => get_class($event),
- 'timestamp' => date('Y-m-d H:i:s'),
- 'microtime' => microtime(true)
- ]);
+ // Log that we received ANY event first.
+ $this->logger->info(
+ 'SoftwareCatalog TestEventListener: Event received!',
+ [
+ 'eventClass' => get_class($event),
+ 'timestamp' => date('Y-m-d H:i:s'),
+ 'microtime' => microtime(true),
+ ]
+ );
-
-
- // Handle UserLoggedInEvent specifically
+ // Handle UserLoggedInEvent specifically.
if ($event instanceof UserLoggedInEvent) {
$user = $event->getUser();
-
- $this->logger->info('SoftwareCatalog TestEventListener: User logged in successfully!', [
- 'userId' => $user->getUID(),
- 'userDisplayName' => $user->getDisplayName(),
- 'userEmail' => $user->getEMailAddress(),
- 'timestamp' => date('Y-m-d H:i:s'),
- 'eventType' => 'UserLoggedInEvent'
- ]);
-
+ $this->logger->info(
+ 'SoftwareCatalog TestEventListener: User logged in successfully!',
+ [
+ 'userId' => $user->getUID(),
+ 'userDisplayName' => $user->getDisplayName(),
+ 'userEmail' => $user->getEMailAddress(),
+ 'timestamp' => date('Y-m-d H:i:s'),
+ 'eventType' => 'UserLoggedInEvent',
+ ]
+ );
- // Test that we can access Nextcloud services
+ // Test that we can access Nextcloud services.
try {
- $this->logger->debug('SoftwareCatalog TestEventListener: Event listener is working correctly!', [
- 'message' => 'This confirms that event listeners are properly registered and triggered',
- 'userId' => $user->getUID(),
- 'eventClass' => get_class($event)
- ]);
+ $this->logger->debug(
+ 'SoftwareCatalog TestEventListener: Event listener is working correctly!',
+ [
+ 'message' => 'This confirms that event listeners are properly registered and triggered',
+ 'userId' => $user->getUID(),
+ 'eventClass' => get_class($event),
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('SoftwareCatalog TestEventListener: Error in event processing', [
- 'exception' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'SoftwareCatalog TestEventListener: Error in event processing',
+ [
+ 'exception' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
}
} else {
- // Log other events we might receive
- $this->logger->debug('SoftwareCatalog TestEventListener: Received unhandled event', [
- 'eventClass' => get_class($event),
- 'timestamp' => date('Y-m-d H:i:s')
- ]);
- }
- }
-}
+ // Log other events we might receive.
+ $this->logger->debug(
+ 'SoftwareCatalog TestEventListener: Received unhandled event',
+ [
+ 'eventClass' => get_class($event),
+ 'timestamp' => date('Y-m-d H:i:s'),
+ ]
+ );
+ }//end if
+ }//end handle()
+}//end class
diff --git a/lib/EventListener/UserProfileUpdatedEventListener.php b/lib/EventListener/UserProfileUpdatedEventListener.php
index 64e9901f..1c3e909f 100644
--- a/lib/EventListener/UserProfileUpdatedEventListener.php
+++ b/lib/EventListener/UserProfileUpdatedEventListener.php
@@ -45,9 +45,12 @@ class UserProfileUpdatedEventListener implements IEventListener
'email' => 'e-mailadres',
];
+ /**
+ * Constructor for UserProfileUpdatedEventListener.
+ */
public function __construct()
{
- }
+ }//end __construct()
/**
* Handle the UserProfileUpdatedEvent.
@@ -63,42 +66,51 @@ public function handle(Event $event): void
}
try {
- $logger = \OC::$server->get(LoggerInterface::class);
+ $logger = \OC::$server->get(LoggerInterface::class);
$changes = $event->getChanges();
// Check if any of the mapped fields were changed.
$relevantChanges = array_intersect($changes, array_keys(self::FIELD_MAP));
if (empty($relevantChanges) === true) {
- $logger->debug('[UserProfileUpdatedEventListener] No relevant field changes for contactpersoon sync', [
- 'userId' => $event->getUserId(),
- 'changes' => $changes,
- ]);
+ $logger->debug(
+ '[UserProfileUpdatedEventListener] No relevant field changes for contactpersoon sync',
+ [
+ 'userId' => $event->getUserId(),
+ 'changes' => $changes,
+ ]
+ );
return;
}
- $logger->info('[UserProfileUpdatedEventListener] Syncing user profile changes to contactpersoon', [
- 'userId' => $event->getUserId(),
- 'relevantChanges' => $relevantChanges,
- ]);
+ $logger->info(
+ '[UserProfileUpdatedEventListener] Syncing user profile changes to contactpersoon',
+ [
+ 'userId' => $event->getUserId(),
+ 'relevantChanges' => $relevantChanges,
+ ]
+ );
- $this->syncToContactpersoon($event, $logger);
+ $this->syncToContactpersoon(event: $event, logger: $logger);
} catch (\Exception $e) {
try {
$logger = \OC::$server->get(LoggerInterface::class);
- $logger->error('[UserProfileUpdatedEventListener] Error syncing profile to contactpersoon', [
- 'userId' => $event->getUserId(),
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- ]);
+ $logger->error(
+ '[UserProfileUpdatedEventListener] Error syncing profile to contactpersoon',
+ [
+ 'userId' => $event->getUserId(),
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]
+ );
} catch (\Exception $logException) {
// Silently fail if logging fails.
}
- }
- }
+ }//end try
+ }//end handle()
/**
- * Find the contactpersoon object by username and update its fields.
+ * Find the contactpersoon object by username (with email fallback) and update its fields.
*
* @param UserProfileUpdatedEvent $event The profile updated event.
* @param LoggerInterface $logger The logger.
@@ -107,50 +119,50 @@ public function handle(Event $event): void
*/
private function syncToContactpersoon(UserProfileUpdatedEvent $event, LoggerInterface $logger): void
{
- $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
+ $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
$settingsService = \OC::$server->get(SettingsService::class);
// Get the voorzieningen config for register and schema.
$voorzieningenConfig = $settingsService->getVoorzieningenConfig();
- $register = $voorzieningenConfig['register'] ?? '';
+ $register = $voorzieningenConfig['register'] ?? '';
$contactpersoonSchema = $voorzieningenConfig['contactpersoon_schema'] ?? '';
if (empty($register) === true || empty($contactpersoonSchema) === true) {
- $logger->warning('[UserProfileUpdatedEventListener] Voorzieningen config missing register or contactpersoon_schema');
+ $logger->warning(
+ '[UserProfileUpdatedEventListener] Voorzieningen config missing register or contactpersoon_schema'
+ );
return;
}
- $userId = $event->getUserId();
-
- // Find contactpersoon by username field using searchObjects.
- $query = [
- '@self' => [
- 'register' => (int) $register,
- 'schema' => (int) $contactpersoonSchema,
- ],
- 'username' => $userId,
+ $userId = $event->getUserId();
+ $selfQuery = [
+ 'register' => (int) $register,
+ 'schema' => (int) $contactpersoonSchema,
];
- $results = $objectService->searchObjects(
- query: $query,
- _rbac: false,
- _multitenancy: false
+ $contactpersoon = $this->findContactpersoon(
+ objectService: $objectService,
+ selfQuery: $selfQuery,
+ userId: $userId,
+ event: $event,
+ logger: $logger
);
- if (empty($results) === true || (is_array($results) === true && count($results) === 0)) {
- $logger->info('[UserProfileUpdatedEventListener] No contactpersoon found for user', [
- 'userId' => $userId,
- 'register' => $register,
- 'schema' => $contactpersoonSchema,
- ]);
+ if ($contactpersoon === null) {
+ $logger->info(
+ '[UserProfileUpdatedEventListener] No contactpersoon found for user',
+ [
+ 'userId' => $userId,
+ 'register' => $register,
+ 'schema' => $contactpersoonSchema,
+ ]
+ );
return;
}
- // Get the first matching contactpersoon.
- $contactpersoon = is_array($results) === true ? reset($results) : $results;
$contactData = $contactpersoon->getObject();
- $newData = $event->getNewData();
- $changes = $event->getChanges();
+ $newData = $event->getNewData();
+ $changes = $event->getChanges();
// Build the patch with only changed fields.
$patch = [];
@@ -163,34 +175,174 @@ private function syncToContactpersoon(UserProfileUpdatedEvent $event, LoggerInte
$patch[$contactField] = $newValue ?? '';
}
+ // Backfill the username field if it was missing (found via email fallback).
+ if (empty($contactData['username']) === true) {
+ $patch['username'] = $userId;
+ $logger->info(
+ '[UserProfileUpdatedEventListener] Backfilling username on contactpersoon',
+ [
+ 'userId' => $userId,
+ 'contactpersoonId' => $contactpersoon->getUuid(),
+ ]
+ );
+ }
+
if (empty($patch) === true) {
- $logger->debug('[UserProfileUpdatedEventListener] No fields to patch on contactpersoon', [
- 'userId' => $userId,
- ]);
+ $logger->debug(
+ '[UserProfileUpdatedEventListener] No fields to patch on contactpersoon',
+ [
+ 'userId' => $userId,
+ ]
+ );
return;
}
- $logger->info('[UserProfileUpdatedEventListener] Patching contactpersoon object', [
- 'userId' => $userId,
- 'contactpersoonId' => $contactpersoon->getUuid(),
- 'patch' => $patch,
- ]);
-
- // Only save the changed fields to avoid property authorization issues
- // with protected fields like 'rollen'.
- $objectService->saveObject(
- register: $contactpersoon->getRegister(),
- schema: $contactpersoon->getSchema(),
- object: $patch,
+ $logger->info(
+ '[UserProfileUpdatedEventListener] Patching contactpersoon object',
+ [
+ 'userId' => $userId,
+ 'contactpersoonId' => $contactpersoon->getUuid(),
+ 'patch' => $patch,
+ ]
+ );
+
+ // Merge the patch into existing data and save directly via mapper to skip schema validation.
+ // Schema validation can reject existing data with legacy values (e.g. notificaties enum).
+ $mergedObject = array_merge($contactData, $patch);
+ $contactpersoon->setObject($mergedObject);
+
+ // Regenerate _name metadata from the schema's objectNameField template.
+ // (e.g. "{{ voornaam }} {{ tussenvoegsel }} {{ achternaam }}").
+ // Without this, _name stays stale after field updates because we bypass the full saveObject flow.
+ $schemaMapper = \OC::$server->get('OCA\OpenRegister\Db\SchemaMapper');
+ $registerMapper = \OC::$server->get('OCA\OpenRegister\Db\RegisterMapper');
+ $metaHydrationHandler = \OC::$server->get('OCA\OpenRegister\Service\Object\SaveObject\MetadataHydrationHandler');
+
+ $schemaEntity = null;
+ $registerEntity = null;
+ try {
+ $schemaEntity = $schemaMapper->find(
+ id: (int) $contactpersoonSchema,
+ _rbac: false,
+ _multitenancy: false
+ );
+ $registerEntity = $registerMapper->find(
+ id: (int) $register,
+ _rbac: false,
+ _multitenancy: false
+ );
+ } catch (\Exception $e) {
+ $logger->warning(
+ '[UserProfileUpdatedEventListener] Could not load schema/register entities for _name hydration',
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }
+
+ if ($schemaEntity !== null) {
+ $metaHydrationHandler->hydrateObjectMetadata(entity: $contactpersoon, schema: $schemaEntity);
+ $logger->debug(
+ '[UserProfileUpdatedEventListener] Regenerated _name metadata',
+ [
+ 'newName' => $contactpersoon->getName(),
+ ]
+ );
+ }
+
+ // Pass register and schema so the magic mapper route is triggered and the.
+ // Per-schema magic table is updated (not just the blob table).
+ $objectMapper = \OC::$server->get('OCA\OpenRegister\Db\ObjectEntityMapper');
+ $objectMapper->update(entity: $contactpersoon, register: $registerEntity, schema: $schemaEntity);
+
+ $logger->info(
+ '[UserProfileUpdatedEventListener] Successfully synced user profile to contactpersoon',
+ [
+ 'userId' => $userId,
+ 'contactpersoonId' => $contactpersoon->getUuid(),
+ 'patchedFields' => array_keys($patch),
+ ]
+ );
+ }//end syncToContactpersoon()
+
+ /**
+ * Find a contactpersoon by username, falling back to a case-insensitive email search.
+ *
+ * @param object $objectService The OpenRegister ObjectService.
+ * @param array $selfQuery The @self register/schema filter.
+ * @param string $userId The Nextcloud user ID.
+ * @param UserProfileUpdatedEvent $event The profile updated event.
+ * @param LoggerInterface $logger The logger.
+ *
+ * @return object|null The contactpersoon entity or null if not found.
+ */
+ private function findContactpersoon(
+ object $objectService,
+ array $selfQuery,
+ string $userId,
+ UserProfileUpdatedEvent $event,
+ LoggerInterface $logger
+ ): ?object {
+ // 1. Search by username = userId, scoped to the user's organisation (multitenancy).
+ // This prevents updating a contactpersoon from a different organisation when.
+ // Multiple records share the same username across orgs.
+ $results = $objectService->searchObjects(
+ query: ['@self' => $selfQuery, 'username' => $userId],
_rbac: false,
- _multitenancy: false,
- uuid: $contactpersoon->getUuid()
+ _multitenancy: true
);
- $logger->info('[UserProfileUpdatedEventListener] Successfully synced user profile to contactpersoon', [
- 'userId' => $userId,
- 'contactpersoonId' => $contactpersoon->getUuid(),
- 'patchedFields' => array_keys($patch),
- ]);
- }
-}
+ if (empty($results) === false && (is_array($results) === false || count($results) > 0)) {
+ if (is_array($results) === true) {
+ return reset($results);
+ }
+
+ return $results;
+ }
+
+ // 2. Fallback: case-insensitive email search using _search (ILIKE).
+ // Try the user's email first, then the userId (which may itself be an email).
+ $emailCandidates = array_filter(
+ array_unique(
+ [
+ $event->getUser()->getEMailAddress(),
+ $userId,
+ ]
+ )
+ );
+
+ foreach ($emailCandidates as $emailCandidate) {
+ if (empty($emailCandidate) === true) {
+ continue;
+ }
+
+ $logger->debug(
+ '[UserProfileUpdatedEventListener] Username lookup failed, trying email fallback',
+ [
+ 'userId' => $userId,
+ 'emailCandidate' => $emailCandidate,
+ ]
+ );
+
+ // Use _search for case-insensitive matching, then verify the email field in PHP.
+ // Scoped to user's organisation via multitenancy to avoid cross-org matches.
+ $results = $objectService->searchObjects(
+ query: ['@self' => $selfQuery, '_search' => $emailCandidate],
+ _rbac: false,
+ _multitenancy: true
+ );
+
+ if (is_array($results) === true) {
+ foreach ($results as $result) {
+ $data = $result->getObject();
+ $storedEmail = $data['e-mailadres'] ?? $data['email'] ?? '';
+ if (strcasecmp($storedEmail, $emailCandidate) === 0) {
+ return $result;
+ }
+ }
+ }
+ }//end foreach
+
+ return null;
+ }//end findContactpersoon()
+}//end class
diff --git a/lib/Examples/ContactpersoonServiceExample.php b/lib/Examples/ContactpersoonServiceExample.php
index 7cb7b67f..cb518438 100644
--- a/lib/Examples/ContactpersoonServiceExample.php
+++ b/lib/Examples/ContactpersoonServiceExample.php
@@ -5,13 +5,13 @@
* This file demonstrates how to use the new getContactPersonsWithUserDetailsForOrganization
* method to retrieve contact persons for an organization with their user details spliced in.
*
- * @category Example
- * @package OCA\SoftwareCatalog\Examples
- * @author Conduction b.v.
+ * @category Example
+ * @package OCA\SoftwareCatalog\Examples
+ * @author Conduction b.v.
* @copyright 2024 Conduction B.V.
- * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
- * @link https://github.com/ConductionNL/SoftwareCatalog
+ * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
*/
declare(strict_types=1);
@@ -22,13 +22,13 @@
use Psr\Log\LoggerInterface;
/**
- * Example class demonstrating ContactpersoonService usage
+ * Example class demonstrating ContactpersoonService usage.
*
* @category Example
* @package OCA\SoftwareCatalog\Examples
* @author Conduction b.v.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
class ContactpersoonServiceExample
@@ -37,64 +37,74 @@ class ContactpersoonServiceExample
* ContactpersoonServiceExample constructor
*
* @param ContactpersoonService $contactpersoonService The contactpersoon service
- * @param LoggerInterface $logger Logger interface
+ * @param LoggerInterface $logger Logger interface
*/
public function __construct(
private readonly ContactpersoonService $contactpersoonService,
private readonly LoggerInterface $logger
) {
- }
+ }//end __construct()
/**
* Example method showing how to get contact persons with user details for an organization
*
* @param string $organizationUuid The organization UUID to get contact persons for
- *
+ *
* @return array Array of contact persons with user details
- *
+ *
* @throws \Exception If contact person retrieval fails
*/
public function getContactPersonsWithUserDetailsExample(string $organizationUuid): array
{
try {
- $this->logger->info('ContactpersoonServiceExample: Getting contact persons with user details', [
- 'organizationUuid' => $organizationUuid
- ]);
+ $this->logger->info(
+ 'ContactpersoonServiceExample: Getting contact persons with user details',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
- // Use the service method to get contact persons with user details
- $contactPersons = $this->contactpersoonService->getContactPersonsWithUserDetailsForOrganization($organizationUuid);
+ // Use the service method to get contact persons with user details.
+ $contactPersons = $this->contactpersoonService->getContactPersonsWithUserDetailsForOrganization(
+ organizationUuid: $organizationUuid
+ );
- $this->logger->info('ContactpersoonServiceExample: Retrieved contact persons', [
- 'organizationUuid' => $organizationUuid,
- 'contactPersonCount' => count($contactPersons)
- ]);
+ $this->logger->info(
+ 'ContactpersoonServiceExample: Retrieved contact persons',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'contactPersonCount' => count($contactPersons),
+ ]
+ );
- // Process the results
+ // Process the results.
$processedResults = [];
foreach ($contactPersons as $contactPerson) {
$contactData = $contactPerson->getObject();
-
+
$processedResults[] = [
- 'contactPersonId' => $contactPerson->getId(),
+ 'contactPersonId' => $contactPerson->getId(),
'contactPersonUuid' => $contactPerson->getUuid(),
- 'name' => $contactData['naam'] ?? 'Unknown',
- 'email' => $contactData['email'] ?? null,
- 'username' => $contactData['username'] ?? null,
- 'hasUserDetails' => $contactData['userDetails'] !== null,
- 'userDetails' => $contactData['userDetails']
+ 'name' => $contactData['naam'] ?? 'Unknown',
+ 'email' => $contactData['email'] ?? null,
+ 'username' => $contactData['username'] ?? null,
+ 'hasUserDetails' => $contactData['userDetails'] !== null,
+ 'userDetails' => $contactData['userDetails'],
];
}
return $processedResults;
-
} catch (\Exception $e) {
- $this->logger->error('ContactpersoonServiceExample: Failed to get contact persons with user details', [
- 'organizationUuid' => $organizationUuid,
- 'error' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'ContactpersoonServiceExample: Failed to get contact persons with user details',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'error' => $e->getMessage(),
+ ]
+ );
throw $e;
- }
- }
+ }//end try
+ }//end getContactPersonsWithUserDetailsExample()
/**
* Example method showing how to use the API endpoint
@@ -103,54 +113,54 @@ public function getContactPersonsWithUserDetailsExample(string $organizationUuid
* or external service.
*
* @param string $organizationUuid The organization UUID
- *
+ *
* @return array Example API call structure
*/
public function getApiCallExample(string $organizationUuid): array
{
return [
- 'method' => 'GET',
- 'url' => '/api/contactpersonen/organisation/' . $organizationUuid . '/with-user-details',
+ 'method' => 'GET',
+ 'url' => '/api/contactpersonen/organisation/'.$organizationUuid.'/with-user-details',
'description' => 'Get all contact persons for an organization with user details spliced in',
- 'parameters' => [
+ 'parameters' => [
'organizationUuid' => [
- 'type' => 'string',
- 'required' => true,
- 'description' => 'The UUID of the organization to get contact persons for'
- ]
+ 'type' => 'string',
+ 'required' => true,
+ 'description' => 'The UUID of the organization to get contact persons for',
+ ],
],
- 'response' => [
- 'success' => true,
- 'data' => [
+ 'response' => [
+ 'success' => true,
+ 'data' => [
[
- 'id' => 'contact_person_id',
- 'uuid' => 'contact_person_uuid',
- 'object' => [
- 'naam' => 'John Doe',
- 'email' => 'john.doe@example.com',
- 'username' => 'john.doe@example.com',
+ 'id' => 'contact_person_id',
+ 'uuid' => 'contact_person_uuid',
+ 'object' => [
+ 'naam' => 'John Doe',
+ 'email' => 'john.doe@example.com',
+ 'username' => 'john.doe@example.com',
'userDetails' => [
- 'uid' => 'john.doe@example.com',
- 'email' => 'john.doe@example.com',
+ 'uid' => 'john.doe@example.com',
+ 'email' => 'john.doe@example.com',
'displayName' => 'John Doe',
- 'enabled' => true,
- 'lastLogin' => 1640995200,
- 'backend' => 'Database',
- 'home' => '/var/www/html/data/john.doe@example.com',
+ 'enabled' => true,
+ 'lastLogin' => 1640995200,
+ 'backend' => 'Database',
+ 'home' => '/var/www/html/data/john.doe@example.com',
'avatarImage' => 'base64_encoded_image_data',
- 'quota' => '1GB',
- 'freeQuota' => '500MB'
- ]
+ 'quota' => '1GB',
+ 'freeQuota' => '500MB',
+ ],
],
'register' => 6,
- 'schema' => 38,
- 'created' => '2024-01-01T00:00:00Z',
- 'modified' => '2024-01-01T00:00:00Z'
- ]
+ 'schema' => 38,
+ 'created' => '2024-01-01T00:00:00Z',
+ 'modified' => '2024-01-01T00:00:00Z',
+ ],
],
- 'count' => 1,
- 'organizationUuid' => $organizationUuid
- ]
+ 'count' => 1,
+ 'organizationUuid' => $organizationUuid,
+ ],
];
- }
-}
+ }//end getApiCallExample()
+}//end class
diff --git a/lib/Repair/InitializeSettings.php b/lib/Repair/InitializeSettings.php
index d586955c..0fd30c13 100644
--- a/lib/Repair/InitializeSettings.php
+++ b/lib/Repair/InitializeSettings.php
@@ -1,4 +1,15 @@
+ * @copyright 2024 Conduction B.V.
+ * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
+ */
declare(strict_types=1);
@@ -17,68 +28,90 @@
* Repair step that initializes SoftwareCatalog settings on install/upgrade.
*
* This runs only during app install or upgrade, not on every request.
+ *
+ * @category Repair
+ * @package OCA\SoftwareCatalog\Repair
*/
class InitializeSettings implements IRepairStep
{
+ /**
+ * Constructor for InitializeSettings.
+ *
+ * @param IAppConfig $config The application configuration
+ * @param IAppManager $appManager The application manager
+ * @param ContainerInterface $container The DI container
+ * @param LoggerInterface $logger The logger instance
+ */
public function __construct(
private readonly IAppConfig $config,
private readonly IAppManager $appManager,
private readonly ContainerInterface $container,
private readonly LoggerInterface $logger
) {
- }
+ }//end __construct()
+ /**
+ * Returns the name of this repair step.
+ *
+ * @return string The repair step name
+ */
public function getName(): string
{
return 'Initialize SoftwareCatalog settings';
- }
+ }//end getName()
+ /**
+ * Run the repair step.
+ *
+ * @param IOutput $output The output interface for progress reporting
+ *
+ * @return void
+ */
public function run(IOutput $output): void
{
$output->startProgress(1);
try {
- $currentAppVersion = $this->appManager->getAppVersion(Application::APP_ID);
+ $currentAppVersion = $this->appManager->getAppVersion(Application::APP_ID);
$lastInitializedVersion = $this->config->getValueString(Application::APP_ID, 'last_initialized_version', '');
- // Only initialize if version changed or never initialized
+ // Only initialize if version changed or never initialized.
if ($lastInitializedVersion === $currentAppVersion) {
- $output->info('Settings already initialized for version ' . $currentAppVersion);
+ $output->info('Settings already initialized for version '.$currentAppVersion);
$output->advance(1);
$output->finishProgress();
return;
}
- $output->info('Initializing settings for version ' . $currentAppVersion);
- $this->logger->info('SoftwareCatalog repair: Starting initialization for version ' . $currentAppVersion);
+ $output->info('Initializing settings for version '.$currentAppVersion);
+ $this->logger->info('SoftwareCatalog repair: Starting initialization for version '.$currentAppVersion);
- // Get the settings service and initialize
+ // Get the settings service and initialize.
$settingsService = $this->container->get(SettingsService::class);
- $result = $settingsService->initialize();
+ $result = $settingsService->initialize();
- // Mark this version as initialized regardless of partial failures
- // This prevents repeated attempts on every request
+ // Mark this version as initialized regardless of partial failures.
+ // This prevents repeated attempts on every request.
$this->config->setValueString(Application::APP_ID, 'last_initialized_version', $currentAppVersion);
- if (!empty($result['errors'])) {
+ if (empty($result['errors']) === false) {
foreach ($result['errors'] as $error) {
- $output->warning('Initialization warning: ' . $error);
- $this->logger->warning('SoftwareCatalog repair: ' . $error);
+ $output->warning('Initialization warning: '.$error);
+ $this->logger->warning('SoftwareCatalog repair: '.$error);
}
}
$output->info('Settings initialization completed');
$this->logger->info('SoftwareCatalog repair: Initialization completed', ['result' => $result]);
-
} catch (\Exception $e) {
- // Still mark as initialized to prevent repeated failures
+ // Still mark as initialized to prevent repeated failures.
$currentAppVersion = $this->appManager->getAppVersion(Application::APP_ID);
$this->config->setValueString(Application::APP_ID, 'last_initialized_version', $currentAppVersion);
- $output->warning('Settings initialization failed: ' . $e->getMessage());
+ $output->warning('Settings initialization failed: '.$e->getMessage());
$this->logger->error('SoftwareCatalog repair: Initialization failed', ['exception' => $e->getMessage()]);
- }
+ }//end try
$output->advance(1);
$output->finishProgress();
- }
-}
+ }//end run()
+}//end class
diff --git a/lib/Sections/SoftwareCatalogAdmin.php b/lib/Sections/SoftwareCatalogAdmin.php
index b8a1d6b9..22070995 100644
--- a/lib/Sections/SoftwareCatalogAdmin.php
+++ b/lib/Sections/SoftwareCatalogAdmin.php
@@ -1,32 +1,89 @@
+ * @copyright 2024 Conduction B.V.
+ * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
+ */
+
namespace OCA\SoftwareCatalog\Sections;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\IIconSection;
-class SoftwareCatalogAdmin implements IIconSection {
+class SoftwareCatalogAdmin implements IIconSection
+{
+
+ /**
+ * The localization service.
+ *
+ * @var IL10N
+ */
private IL10N $l;
+
+ /**
+ * The URL generator service.
+ *
+ * @var IURLGenerator
+ */
private IURLGenerator $urlGenerator;
- public function __construct(IL10N $l, IURLGenerator $urlGenerator) {
- $this->l = $l;
+ /**
+ * Constructor for SoftwareCatalogAdmin section.
+ *
+ * @param IL10N $l The localization service
+ * @param IURLGenerator $urlGenerator The URL generator service
+ */
+ public function __construct(IL10N $l, IURLGenerator $urlGenerator)
+ {
+ $this->l = $l;
$this->urlGenerator = $urlGenerator;
- }
+ }//end __construct()
- public function getIcon(): string {
+ /**
+ * Returns the icon URL for this settings section.
+ *
+ * @return string The icon URL
+ */
+ public function getIcon(): string
+ {
+ // phpcs:ignore -- named parameters unsafe for Nextcloud core methods (param names vary by NC version)
return $this->urlGenerator->imagePath('core', 'actions/settings-dark.svg');
- }
+ }//end getIcon()
- public function getID(): string {
+ /**
+ * Returns the unique identifier for this section.
+ *
+ * @return string The section ID
+ */
+ public function getID(): string
+ {
return 'softwarecatalog';
- }
+ }//end getID()
- public function getName(): string {
+ /**
+ * Returns the human-readable name of this section.
+ *
+ * @return string The translated section name
+ */
+ public function getName(): string
+ {
return $this->l->t('Software Catalog');
- }
+ }//end getName()
- public function getPriority(): int {
+ /**
+ * Returns the priority for ordering this section.
+ *
+ * @return int The priority value
+ */
+ public function getPriority(): int
+ {
return 97;
- }
-}
\ No newline at end of file
+ }//end getPriority()
+}//end class
diff --git a/lib/Service/AanbodService.php b/lib/Service/AanbodService.php
index a307121e..6c69dee7 100644
--- a/lib/Service/AanbodService.php
+++ b/lib/Service/AanbodService.php
@@ -1,23 +1,23 @@
+ * @category Service
+ * @package OCA\SoftwareCatalog\Service
+ * @author Conduction b.v.
* @copyright 2024 Conduction B.V.
- * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- * @version 1.0.0
- * @link https://github.com/ConductionNL/SoftwareCatalog
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
*/
+declare(strict_types=1);
+
namespace OCA\SoftwareCatalog\Service;
use OCA\OpenRegister\Service\ObjectService;
@@ -29,31 +29,31 @@
use Exception;
/**
- * Service for managing aanbod (offers) operations
+ * Service for managing aanbod (offers) operations.
*
* This service provides operations for querying aanbod objects (gebruik, dienst,
* module, koppeling) where the active organization is involved either as the
* afnemer (consumer) or aanbieder (provider), and for accepting or denying these offers.
*
- * @category Service
- * @package OCA\SoftwareCatalog\Service
- * @author Conduction b.v.
+ * @category Service
+ * @package OCA\SoftwareCatalog\Service
+ * @author Conduction b.v.
* @copyright 2024 Conduction B.V.
- * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- * @version 1.0.0
- * @link https://github.com/ConductionNL/SoftwareCatalog
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
*/
class AanbodService
{
/**
- * Constructor for AanbodService
+ * Constructor for AanbodService.
*
- * @param IAppConfig $config Nextcloud app configuration service
- * @param IAppManager $appManager App manager service for checking available apps
- * @param ContainerInterface $container PSR-11 container interface for dependency injection
- * @param LoggerInterface $logger Logger service for debugging and error reporting
- * @param SettingsService $settingsService Settings service for retrieving configuration
- * @param IUserSession $userSession User session service for current user context
+ * @param IAppConfig $config Nextcloud app configuration service
+ * @param IAppManager $appManager App manager service for checking available apps
+ * @param ContainerInterface $container PSR-11 container interface for dependency injection
+ * @param LoggerInterface $logger Logger service for debugging and error reporting
+ * @param SettingsService $settingsService Settings service for retrieving configuration
+ * @param IUserSession $userSession User session service for current user context
*/
public function __construct(
private readonly IAppConfig $config,
@@ -63,10 +63,10 @@ public function __construct(
private readonly SettingsService $settingsService,
private readonly IUserSession $userSession
) {
- }
+ }//end __construct()
/**
- * Get all aanbod objects (modules, diensten, koppelingen, gebruiks)
+ * Get all aanbod objects (modules, diensten, koppelingen, gebruiks).
*
* Returns modules, diensten, and koppelingen where the current organisation
* is in the aanbieder property, or gebruiks where the current organisation
@@ -74,266 +74,309 @@ public function __construct(
* equals the current organisation (already accepted).
*
* @param array $options Additional query options (limit, offset, filters, etc.)
+ *
* @return array Array with success status, aanbod objects data, and metadata
+ *
* @throws Exception When OpenRegister service is not available
*/
- public function getAanbod(array $options = []): array
+ public function getAanbod(array $options=[]): array
{
- $this->logger->info('Getting aanbod objects for active organisation', [
- 'options' => $options
- ]);
+ $this->logger->info(
+ 'Getting aanbod objects for active organisation',
+ [
+ 'options' => $options,
+ ]
+ );
try {
- // Get ObjectService from OpenRegister
+ // Get ObjectService from OpenRegister.
$objectService = $this->getObjectService();
- // Get current organization
+ // Get current organization.
$currentOrg = $this->getCurrentOrganisation();
- if (!$currentOrg) {
+ if ($currentOrg === null) {
$this->logger->warning('No current organization available for aanbod filtering');
return [
'results' => [],
- 'total' => 0,
- 'page' => 1,
- 'pages' => 0,
- 'limit' => 20,
- 'offset' => 0,
- 'message' => 'No current organization available'
+ 'total' => 0,
+ 'page' => 1,
+ 'pages' => 0,
+ 'limit' => 20,
+ 'offset' => 0,
+ 'message' => 'No current organization available',
];
}
- // Get voorzieningen configuration
+ // Get voorzieningen configuration.
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $registerId = $voorzieningenConfig['register'] ?? null;
- $gebruikSchema = $voorzieningenConfig['gebruik_schema'] ?? null;
- $koppelingSchema = $voorzieningenConfig['koppeling_schema'] ?? null;
- $moduleSchema = $voorzieningenConfig['module_schema'] ?? null;
- $dienstSchema = $voorzieningenConfig['dienst_schema'] ?? null;
+ $registerId = $voorzieningenConfig['register'] ?? null;
+ $gebruikSchema = $voorzieningenConfig['gebruik_schema'] ?? null;
+ $koppelingSchema = $voorzieningenConfig['koppeling_schema'] ?? null;
+ $moduleSchema = $voorzieningenConfig['module_schema'] ?? null;
+ $dienstSchema = $voorzieningenConfig['dienst_schema'] ?? null;
- if (!$registerId) {
+ if ($registerId === null) {
throw new Exception('Voorzieningen register not configured');
}
- $allResults = [];
+ $allResults = [];
$schemasToSearch = [];
- // Collect all schemas we need to search
- if ($gebruikSchema) {
+ // Collect all schemas we need to search.
+ if ($gebruikSchema !== null) {
$schemasToSearch[] = ['schema' => $gebruikSchema, 'type' => 'gebruik', 'filter_field' => 'afnemer'];
}
- if ($koppelingSchema) {
+
+ if ($koppelingSchema !== null) {
$schemasToSearch[] = ['schema' => $koppelingSchema, 'type' => 'koppeling', 'filter_field' => 'aanbieder'];
}
- if ($moduleSchema) {
+
+ if ($moduleSchema !== null) {
$schemasToSearch[] = ['schema' => $moduleSchema, 'type' => 'module', 'filter_field' => 'aanbieder'];
}
- if ($dienstSchema) {
+
+ if ($dienstSchema !== null) {
$schemasToSearch[] = ['schema' => $dienstSchema, 'type' => 'dienst', 'filter_field' => 'aanbieder'];
}
- // Search each schema type
+ // Search each schema type.
foreach ($schemasToSearch as $schemaConfig) {
try {
$query = [
- '@self' => [
+ '@self' => [
'register' => $registerId,
- 'schema' => $schemaConfig['schema']
+ 'schema' => $schemaConfig['schema'],
],
- $schemaConfig['filter_field'] => $currentOrg
+ $schemaConfig['filter_field'] => $currentOrg,
];
- // Add pagination and other filters from options
- $query = $this->addQueryFilters($query, $options);
+ // Add pagination and other filters from options.
+ $query = $this->addQueryFilters(baseQuery: $query, options: $options);
- $this->logger->debug('Searching aanbod objects', [
- 'schema' => $schemaConfig['schema'],
- 'type' => $schemaConfig['type'],
- 'filter_field' => $schemaConfig['filter_field'],
- 'current_org' => $currentOrg
- ]);
+ $this->logger->debug(
+ 'Searching aanbod objects',
+ [
+ 'schema' => $schemaConfig['schema'],
+ 'type' => $schemaConfig['type'],
+ 'filter_field' => $schemaConfig['filter_field'],
+ 'current_org' => $currentOrg,
+ ]
+ );
- // Execute search with RBAC and multitenancy disabled to find cross-organisation objects
+ // Execute search with RBAC and multitenancy disabled to find cross-organisation objects.
$searchResult = $objectService->searchObjectsPaginated(
query: $query,
_rbac: false,
_multitenancy: false
);
- // Filter out objects where @self.organisation equals current org
+ // Filter out objects where @self.organisation equals current org.
foreach ($searchResult['results'] ?? [] as $result) {
- // Use jsonSerialize() instead of getObject() to include @self metadata
- // getObject() only returns raw object data without @self.organisation
- $resultData = is_array($result) ? $result : $result->jsonSerialize();
+ // Use jsonSerialize() instead of getObject() to include @self metadata.
+ // GetObject() only returns raw object data without @self.organisation.
+ if (is_array($result) === true) {
+ $resultData = $result;
+ } else {
+ $resultData = $result->jsonSerialize();
+ }
+
$selfOrg = $resultData['@self']['organisation'] ?? null;
- // Only include if @self.organisation is NOT set to the current organisation
+ // Only include if @self.organisation is NOT set to the current organisation.
if ($selfOrg !== $currentOrg) {
- // Add type information to result
+ // Add type information to result.
$resultData['_aanbod_type'] = $schemaConfig['type'];
$allResults[] = $resultData;
}
}
- $this->logger->debug('Found aanbod objects for schema', [
- 'schema' => $schemaConfig['schema'],
- 'type' => $schemaConfig['type'],
- 'count' => count($searchResult['results'] ?? [])
- ]);
-
+ $this->logger->debug(
+ 'Found aanbod objects for schema',
+ [
+ 'schema' => $schemaConfig['schema'],
+ 'type' => $schemaConfig['type'],
+ 'count' => count($searchResult['results'] ?? []),
+ ]
+ );
} catch (Exception $e) {
- $this->logger->warning('Failed to get aanbod objects from schema', [
- 'schema' => $schemaConfig['schema'],
- 'type' => $schemaConfig['type'],
- 'error' => $e->getMessage()
- ]);
- }
- }
-
- // Apply pagination to combined results
+ $this->logger->warning(
+ 'Failed to get aanbod objects from schema',
+ [
+ 'schema' => $schemaConfig['schema'],
+ 'type' => $schemaConfig['type'],
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end foreach
+
+ // Apply pagination to combined results.
$requestedLimit = $options['_limit'] ?? $options['limit'] ?? 20;
- $requestedPage = $options['_page'] ?? 1;
- $requestedOffset = isset($options['_offset']) ? $options['_offset'] : (($requestedPage - 1) * $requestedLimit);
+ $requestedPage = $options['_page'] ?? 1;
+
+ if (isset($options['_offset']) === true) {
+ $requestedOffset = $options['_offset'];
+ } else {
+ $requestedOffset = (($requestedPage - 1) * $requestedLimit);
+ }
- $totalFiltered = count($allResults);
+ $totalFiltered = count($allResults);
$paginatedResults = array_slice($allResults, $requestedOffset, $requestedLimit);
- $totalPages = $requestedLimit > 0
- ? (int) ceil($totalFiltered / $requestedLimit)
- : 1;
+ if ($requestedLimit > 0) {
+ $totalPages = (int) ceil($totalFiltered / $requestedLimit);
+ } else {
+ $totalPages = 1;
+ }
return [
'results' => $paginatedResults,
- 'total' => $totalFiltered,
- 'page' => $requestedPage,
- 'pages' => $totalPages,
- 'limit' => $requestedLimit,
- 'offset' => $requestedOffset
+ 'total' => $totalFiltered,
+ 'page' => $requestedPage,
+ 'pages' => $totalPages,
+ 'limit' => $requestedLimit,
+ 'offset' => $requestedOffset,
];
-
} catch (Exception $e) {
- $this->logger->error('Failed to get aanbod objects', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'Failed to get aanbod objects',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return [
'results' => [],
- 'total' => 0,
- 'page' => 1,
- 'pages' => 0,
- 'limit' => 20,
- 'offset' => 0,
- 'error' => 'Failed to retrieve aanbod: ' . $e->getMessage()
+ 'total' => 0,
+ 'page' => 1,
+ 'pages' => 0,
+ 'limit' => 20,
+ 'offset' => 0,
+ 'error' => 'Failed to retrieve aanbod: '.$e->getMessage(),
];
- }
- }
+ }//end try
+ }//end getAanbod()
/**
- * Accept an aanbod object (set @self.organisation to current organisation)
+ * Accept an aanbod object (set @self.organisation to current organisation).
*
* This method updates the @self.organisation property of an aanbod object
* to the active organization, but only if the active organization is either
* the afnemer (for gebruiks) or aanbieder (for modules, diensten, koppelingen).
*
* @param string $aanbodId The UUID of the aanbod object to accept
- * @param array $options Additional update options
+ * @param array $options Additional update options
+ *
* @return array Result with success status and updated object data
+ *
* @throws Exception When OpenRegister service is not available or operation fails
*/
- public function acceptAanbod(string $aanbodId, array $options = []): array
+ public function acceptAanbod(string $aanbodId, array $options=[]): array
{
- $this->logger->info('Accepting aanbod object', [
- 'aanbod_id' => $aanbodId,
- 'options' => $options
- ]);
+ $this->logger->info(
+ 'Accepting aanbod object',
+ [
+ 'aanbod_id' => $aanbodId,
+ 'options' => $options,
+ ]
+ );
try {
- // Validate input
- if (empty($aanbodId)) {
+ // Validate input.
+ if (empty($aanbodId) === true) {
return [
'success' => false,
- 'error' => 'Aanbod ID is required',
- 'aanbod' => null
+ 'error' => 'Aanbod ID is required',
+ 'aanbod' => null,
];
}
- // Get ObjectService from OpenRegister
+ // Get ObjectService from OpenRegister.
$objectService = $this->getObjectService();
- // Get current organization
+ // Get current organization.
$currentOrg = $this->getCurrentOrganisation();
- if (!$currentOrg) {
+ if ($currentOrg === null) {
return [
'success' => false,
- 'error' => 'No current organization available',
- 'aanbod' => null
+ 'error' => 'No current organization available',
+ 'aanbod' => null,
];
}
- // Find the aanbod object across all possible schemas (gebruik, koppeling, module, dienst)
- $existingAanbod = $this->findAanbodObject($objectService, $aanbodId);
+ // Find the aanbod object across all possible schemas (gebruik, koppeling, module, dienst).
+ $existingAanbod = $this->findAanbodObject(
+ objectService: $objectService,
+ aanbodId: $aanbodId
+ );
- if (!$existingAanbod) {
+ if ($existingAanbod === null) {
return [
'success' => false,
- 'error' => 'Aanbod object not found',
- 'aanbod' => null
+ 'error' => 'Aanbod object not found',
+ 'aanbod' => null,
];
}
- // Verify that the active organization is either afnemer or aanbieder
- $aanbodData = $existingAanbod->getObject();
- $afnemerInfo = $aanbodData['afnemer'] ?? null;
+ // Verify that the active organization is either afnemer or aanbieder.
+ $aanbodData = $existingAanbod->getObject();
+ $afnemerInfo = $aanbodData['afnemer'] ?? null;
$aanbiederInfo = $aanbodData['aanbieder'] ?? null;
- // Check various ways the afnemer might be stored
+ // Check various ways the afnemer might be stored.
$afnemerId = null;
- if (is_array($afnemerInfo) && isset($afnemerInfo['id'])) {
+ if (is_array($afnemerInfo) === true && isset($afnemerInfo['id']) === true) {
$afnemerId = $afnemerInfo['id'];
- } elseif (is_string($afnemerInfo)) {
+ } else if (is_string($afnemerInfo) === true) {
$afnemerId = $afnemerInfo;
}
- // Check various ways the aanbieder might be stored
+ // Check various ways the aanbieder might be stored.
$aanbiederId = null;
- if (is_array($aanbiederInfo) && isset($aanbiederInfo['id'])) {
+ if (is_array($aanbiederInfo) === true && isset($aanbiederInfo['id']) === true) {
$aanbiederId = $aanbiederInfo['id'];
- } elseif (is_string($aanbiederInfo)) {
+ } else if (is_string($aanbiederInfo) === true) {
$aanbiederId = $aanbiederInfo;
}
- // Allow operation if current org is either afnemer or aanbieder
- $isAfnemer = ($afnemerId && $afnemerId === $currentOrg);
- $isAanbieder = ($aanbiederId && $aanbiederId === $currentOrg);
+ // Allow operation if current org is either afnemer or aanbieder.
+ $isAfnemer = ($afnemerId !== null && $afnemerId === $currentOrg);
+ $isAanbieder = ($aanbiederId !== null && $aanbiederId === $currentOrg);
- if (!$isAfnemer && !$isAanbieder) {
+ if ($isAfnemer === false && $isAanbieder === false) {
return [
'success' => false,
- 'error' => 'Operation not allowed: active organization is not the afnemer or aanbieder',
- 'aanbod' => null,
- 'debug' => [
- 'afnemer_in_object' => $afnemerInfo,
- 'resolved_afnemer_id' => $afnemerId,
- 'aanbieder_in_object' => $aanbiederInfo,
+ 'error' => 'Operation not allowed: active organization is not the afnemer or aanbieder',
+ 'aanbod' => null,
+ 'debug' => [
+ 'afnemer_in_object' => $afnemerInfo,
+ 'resolved_afnemer_id' => $afnemerId,
+ 'aanbieder_in_object' => $aanbiederInfo,
'resolved_aanbieder_id' => $aanbiederId,
- 'current_org' => $currentOrg
- ]
+ 'current_org' => $currentOrg,
+ ],
];
}
- // Update the @self.organisation and @self.owner properties
- // Both must be set so the accepting user has permission to read/update the object
+ // Update the @self.organisation and @self.owner properties.
+ // Both must be set so the accepting user has permission to read/update the object.
$currentUser = $this->userSession->getUser();
- $selfData = ['organisation' => $currentOrg];
+ $selfData = ['organisation' => $currentOrg];
if ($currentUser !== null) {
$selfData['owner'] = $currentUser->getUID();
}
+
$aanbodData['@self'] = $selfData;
- // Update geregistreerdDoor based on the accepting organisation's type
- $aanbodData = $this->updateGeregistreerdDoor($objectService, $aanbodData, $currentOrg);
+ // Update geregistreerdDoor based on the accepting organisation's type.
+ $aanbodData = $this->updateGeregistreerdDoor(
+ objectService: $objectService,
+ objectData: $aanbodData,
+ organisationUuid: $currentOrg
+ );
- // Save the updated object with RBAC and multitenancy disabled
+ // Save the updated object with RBAC and multitenancy disabled.
$existingAanbod->setObject($aanbodData);
$updatedAanbod = $objectService->saveObject(
object: $existingAanbod,
@@ -344,138 +387,153 @@ public function acceptAanbod(string $aanbodId, array $options = []): array
_multitenancy: false
);
- $this->logger->info('Successfully accepted aanbod object', [
- 'aanbod_id' => $aanbodId,
- 'organisation' => $currentOrg,
- 'owner' => $currentUser?->getUID(),
- 'is_afnemer' => $isAfnemer,
- 'is_aanbieder' => $isAanbieder
- ]);
+ $this->logger->info(
+ 'Successfully accepted aanbod object',
+ [
+ 'aanbod_id' => $aanbodId,
+ 'organisation' => $currentOrg,
+ 'owner' => $currentUser?->getUID(),
+ 'is_afnemer' => $isAfnemer,
+ 'is_aanbieder' => $isAanbieder,
+ ]
+ );
return [
- 'success' => true,
- 'message' => 'Aanbod object accepted successfully',
- 'aanbod' => $updatedAanbod->getObject(),
- 'updated_fields' => ['@self.organisation', '@self.owner']
+ 'success' => true,
+ 'message' => 'Aanbod object accepted successfully',
+ 'aanbod' => $updatedAanbod->getObject(),
+ 'updated_fields' => ['@self.organisation', '@self.owner'],
];
-
} catch (Exception $e) {
- $this->logger->error('Failed to accept aanbod object', [
- 'aanbod_id' => $aanbodId,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'Failed to accept aanbod object',
+ [
+ 'aanbod_id' => $aanbodId,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return [
'success' => false,
- 'error' => 'Failed to accept aanbod: ' . $e->getMessage(),
- 'aanbod' => null
+ 'error' => 'Failed to accept aanbod: '.$e->getMessage(),
+ 'aanbod' => null,
];
- }
- }
+ }//end try
+ }//end acceptAanbod()
/**
- * Deny an aanbod object (delete it)
+ * Deny an aanbod object (delete it).
*
* This method deletes an aanbod object, but only if the active organization
* is either the afnemer (for gebruiks) or aanbieder (for modules, diensten, koppelingen).
*
* @param string $aanbodId The UUID of the aanbod object to deny
- * @param array $options Additional options for the operation
+ * @param array $options Additional options for the operation
+ *
* @return array Result array with success status and details
*/
- public function denyAanbod(string $aanbodId, array $options = []): array
+ public function denyAanbod(string $aanbodId, array $options=[]): array
{
- $this->logger->info('Denying aanbod object', [
- 'aanbod_id' => $aanbodId,
- 'options' => $options
- ]);
+ $this->logger->info(
+ 'Denying aanbod object',
+ [
+ 'aanbod_id' => $aanbodId,
+ 'options' => $options,
+ ]
+ );
try {
- // Validate input
- if (empty($aanbodId)) {
+ // Validate input.
+ if (empty($aanbodId) === true) {
return [
'success' => false,
- 'error' => 'Aanbod ID is required',
- 'deleted' => false
+ 'error' => 'Aanbod ID is required',
+ 'deleted' => false,
];
}
- // Get ObjectService from OpenRegister
+ // Get ObjectService from OpenRegister.
$objectService = $this->getObjectService();
- // Get current organization
+ // Get current organization.
$currentOrg = $this->getCurrentOrganisation();
- if (!$currentOrg) {
+ if ($currentOrg === null) {
return [
'success' => false,
- 'error' => 'No current organization available',
- 'deleted' => false
+ 'error' => 'No current organization available',
+ 'deleted' => false,
];
}
- // Find the aanbod object across all possible schemas (gebruik, koppeling, module, dienst)
- $existingAanbod = $this->findAanbodObject($objectService, $aanbodId);
+ // Find the aanbod object across all possible schemas (gebruik, koppeling, module, dienst).
+ $existingAanbod = $this->findAanbodObject(
+ objectService: $objectService,
+ aanbodId: $aanbodId
+ );
- if (!$existingAanbod) {
+ if ($existingAanbod === null) {
return [
'success' => false,
- 'error' => 'Aanbod object not found',
- 'deleted' => false
+ 'error' => 'Aanbod object not found',
+ 'deleted' => false,
];
}
$aanbodData = $existingAanbod->getObject();
- // SECURITY CHECK: Verify that the active organization is either afnemer or aanbieder
- $afnemerInfo = $aanbodData['afnemer'] ?? null;
+ // SECURITY CHECK: Verify that the active organization is either afnemer or aanbieder.
+ $afnemerInfo = $aanbodData['afnemer'] ?? null;
$aanbiederInfo = $aanbodData['aanbieder'] ?? null;
- // Check various ways the afnemer might be stored
+ // Check various ways the afnemer might be stored.
$afnemerId = null;
- if (is_array($afnemerInfo) && isset($afnemerInfo['id'])) {
+ if (is_array($afnemerInfo) === true && isset($afnemerInfo['id']) === true) {
$afnemerId = $afnemerInfo['id'];
- } elseif (is_string($afnemerInfo)) {
+ } else if (is_string($afnemerInfo) === true) {
$afnemerId = $afnemerInfo;
}
- // Check various ways the aanbieder might be stored
+ // Check various ways the aanbieder might be stored.
$aanbiederId = null;
- if (is_array($aanbiederInfo) && isset($aanbiederInfo['id'])) {
+ if (is_array($aanbiederInfo) === true && isset($aanbiederInfo['id']) === true) {
$aanbiederId = $aanbiederInfo['id'];
- } elseif (is_string($aanbiederInfo)) {
+ } else if (is_string($aanbiederInfo) === true) {
$aanbiederId = $aanbiederInfo;
}
- // Allow operation if current org is either afnemer or aanbieder
- $isAfnemer = ($afnemerId && $afnemerId === $currentOrg);
- $isAanbieder = ($aanbiederId && $aanbiederId === $currentOrg);
-
- if (!$isAfnemer && !$isAanbieder) {
- $this->logger->warning('Unauthorized delete attempt - user is not afnemer or aanbieder', [
- 'aanbod_id' => $aanbodId,
- 'current_org' => $currentOrg,
- 'afnemer_in_object' => $afnemerInfo,
- 'resolved_afnemer_id' => $afnemerId,
- 'aanbieder_in_object' => $aanbiederInfo,
- 'resolved_aanbieder_id' => $aanbiederId
- ]);
+ // Allow operation if current org is either afnemer or aanbieder.
+ $isAfnemer = ($afnemerId !== null && $afnemerId === $currentOrg);
+ $isAanbieder = ($aanbiederId !== null && $aanbiederId === $currentOrg);
+
+ if ($isAfnemer === false && $isAanbieder === false) {
+ $this->logger->warning(
+ 'Unauthorized delete attempt - user is not afnemer or aanbieder',
+ [
+ 'aanbod_id' => $aanbodId,
+ 'current_org' => $currentOrg,
+ 'afnemer_in_object' => $afnemerInfo,
+ 'resolved_afnemer_id' => $afnemerId,
+ 'aanbieder_in_object' => $aanbiederInfo,
+ 'resolved_aanbieder_id' => $aanbiederId,
+ ]
+ );
return [
'success' => false,
- 'error' => 'Operation not allowed: active organization is not the afnemer or aanbieder',
+ 'error' => 'Operation not allowed: active organization is not the afnemer or aanbieder',
'deleted' => false,
- 'debug' => [
- 'afnemer_in_object' => $afnemerInfo,
- 'resolved_afnemer_id' => $afnemerId,
- 'aanbieder_in_object' => $aanbiederInfo,
+ 'debug' => [
+ 'afnemer_in_object' => $afnemerInfo,
+ 'resolved_afnemer_id' => $afnemerId,
+ 'aanbieder_in_object' => $aanbiederInfo,
'resolved_aanbieder_id' => $aanbiederId,
- 'current_org' => $currentOrg
- ]
+ 'current_org' => $currentOrg,
+ ],
];
- }
+ }//end if
- // Delete the object with RBAC and multitenancy disabled
+ // Delete the object with RBAC and multitenancy disabled.
$objectService->setRegister(register: $existingAanbod->getRegister());
$objectService->setSchema(schema: $existingAanbod->getSchema());
@@ -485,64 +543,74 @@ public function denyAanbod(string $aanbodId, array $options = []): array
_multitenancy: false
);
- $this->logger->info('Successfully denied aanbod object', [
- 'aanbod_id' => $aanbodId,
- 'organisation' => $currentOrg,
- 'is_afnemer' => $isAfnemer,
- 'is_aanbieder' => $isAanbieder
- ]);
+ $this->logger->info(
+ 'Successfully denied aanbod object',
+ [
+ 'aanbod_id' => $aanbodId,
+ 'organisation' => $currentOrg,
+ 'is_afnemer' => $isAfnemer,
+ 'is_aanbieder' => $isAanbieder,
+ ]
+ );
return [
- 'success' => true,
- 'message' => 'Aanbod object denied successfully',
- 'deleted' => true,
- 'aanbod_id' => $aanbodId,
- 'organisation' => $currentOrg
+ 'success' => true,
+ 'message' => 'Aanbod object denied successfully',
+ 'deleted' => true,
+ 'aanbod_id' => $aanbodId,
+ 'organisation' => $currentOrg,
];
-
} catch (Exception $e) {
- $this->logger->error('Failed to deny aanbod object', [
- 'aanbod_id' => $aanbodId,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'Failed to deny aanbod object',
+ [
+ 'aanbod_id' => $aanbodId,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return [
'success' => false,
- 'error' => 'Failed to deny aanbod: ' . $e->getMessage(),
- 'deleted' => false
+ 'error' => 'Failed to deny aanbod: '.$e->getMessage(),
+ 'deleted' => false,
];
- }
- }
+ }//end try
+ }//end denyAanbod()
/**
- * Find an aanbod object by UUID across all possible schemas
+ * Find an aanbod object by UUID across all possible schemas.
*
* Searches gebruik, koppeling, module, and dienst schemas because
* an aanbod can be any of these types. Register/schema context is
* required to find objects stored in magic tables.
*
* @param ObjectService $objectService The OpenRegister object service
- * @param string $aanbodId The UUID of the aanbod object
+ * @param string $aanbodId The UUID of the aanbod object
+ *
* @return \OCA\OpenRegister\Db\ObjectEntity|null The found object or null
*/
- private function findAanbodObject(ObjectService $objectService, string $aanbodId): ?\OCA\OpenRegister\Db\ObjectEntity
- {
+ private function findAanbodObject(
+ ObjectService $objectService,
+ string $aanbodId
+ ): ?\OCA\OpenRegister\Db\ObjectEntity {
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $registerId = $voorzieningenConfig['register'] ?? null;
+ $registerId = $voorzieningenConfig['register'] ?? null;
- if (!$registerId) {
+ if ($registerId === null) {
$this->logger->warning('Cannot find aanbod object: no register configured');
return null;
}
- // Collect all schema IDs to search across
- $schemasToTry = array_filter([
- $voorzieningenConfig['gebruik_schema'] ?? null,
- $voorzieningenConfig['koppeling_schema'] ?? null,
- $voorzieningenConfig['module_schema'] ?? null,
- $voorzieningenConfig['dienst_schema'] ?? null,
- ]);
+ // Collect all schema IDs to search across.
+ $schemasToTry = array_filter(
+ [
+ $voorzieningenConfig['gebruik_schema'] ?? null,
+ $voorzieningenConfig['koppeling_schema'] ?? null,
+ $voorzieningenConfig['module_schema'] ?? null,
+ $voorzieningenConfig['dienst_schema'] ?? null,
+ ]
+ );
foreach ($schemasToTry as $schemaId) {
try {
@@ -553,108 +621,116 @@ private function findAanbodObject(ObjectService $objectService, string $aanbodId
_rbac: false,
_multitenancy: false
);
- if ($object) {
+ if ($object !== null) {
return $object;
}
} catch (Exception $e) {
- // Object not found in this schema, try next
+ // Object not found in this schema, try next.
continue;
}
}
- $this->logger->warning('Aanbod object not found in any schema', [
- 'aanbod_id' => $aanbodId,
- 'schemas_tried' => $schemasToTry
- ]);
+ $this->logger->warning(
+ 'Aanbod object not found in any schema',
+ [
+ 'aanbod_id' => $aanbodId,
+ 'schemas_tried' => $schemasToTry,
+ ]
+ );
return null;
- }
+ }//end findAanbodObject()
/**
- * Get current active organisation for filtering
+ * Get current active organisation for filtering.
*
* @return string|null Current organisation identifier or null if no user session
*/
private function getCurrentOrganisation(): ?string
{
$user = $this->userSession->getUser();
- if (!$user) {
+ if ($user === null) {
return null;
}
try {
- // Get the OpenRegister OrganisationService to get the active organisation
+ // Get the OpenRegister OrganisationService to get the active organisation.
$organisationService = $this->container->get('OCA\OpenRegister\Service\OrganisationService');
- $activeOrg = $organisationService->getActiveOrganisation();
+ $activeOrg = $organisationService->getActiveOrganisation();
- if ($activeOrg) {
+ if ($activeOrg !== null) {
return $activeOrg->getUuid();
}
return null;
} catch (Exception $e) {
- $this->logger->error('Failed to get current organisation from OpenRegister', [
- 'error' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to get current organisation from OpenRegister',
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
return null;
}
- }
+ }//end getCurrentOrganisation()
/**
- * Get ObjectService from OpenRegister app
+ * Get ObjectService from OpenRegister app.
*
* @return ObjectService The OpenRegister object service
+ *
* @throws Exception When OpenRegister service is not available
*/
private function getObjectService(): ObjectService
{
- if (!in_array('openregister', $this->appManager->getInstalledApps())) {
+ if (in_array(needle: 'openregister', haystack: $this->appManager->getInstalledApps()) === false) {
throw new Exception('OpenRegister app is not installed');
}
try {
return $this->container->get('OCA\OpenRegister\Service\ObjectService');
} catch (Exception $e) {
- throw new Exception('Failed to get OpenRegister service: ' . $e->getMessage());
+ throw new Exception('Failed to get OpenRegister service: '.$e->getMessage());
}
- }
+ }//end getObjectService()
/**
- * Add query filters from options to the base query
+ * Add query filters from options to the base query.
*
* @param array $baseQuery The base query to extend
- * @param array $options Filter options to apply
+ * @param array $options Filter options to apply
+ *
* @return array Extended query with additional filters
*/
private function addQueryFilters(array $baseQuery, array $options): array
{
- // Add limit if specified
+ // Add limit if specified.
$limit = $options['_limit'] ?? $options['limit'] ?? null;
- if ($limit !== null && is_numeric($limit)) {
- $baseQuery['_limit'] = (int)$limit;
+ if ($limit !== null && is_numeric($limit) === true) {
+ $baseQuery['_limit'] = (int) $limit;
}
- // Add offset if specified
+ // Add offset if specified.
$offset = $options['_offset'] ?? $options['offset'] ?? null;
- if ($offset !== null && is_numeric($offset)) {
- $baseQuery['_offset'] = (int)$offset;
+ if ($offset !== null && is_numeric($offset) === true) {
+ $baseQuery['_offset'] = (int) $offset;
}
- // Add page if specified
- if (isset($options['_page']) && is_numeric($options['_page'])) {
- $baseQuery['_page'] = (int)$options['_page'];
+ // Add page if specified.
+ if (isset($options['_page']) === true && is_numeric($options['_page']) === true) {
+ $baseQuery['_page'] = (int) $options['_page'];
}
- // Add source parameter if specified
- if (isset($options['_source']) && !empty($options['_source'])) {
+ // Add source parameter if specified.
+ if (isset($options['_source']) === true && empty($options['_source']) === false) {
$baseQuery['_source'] = $options['_source'];
}
return $baseQuery;
- }
+ }//end addQueryFilters()
/**
- * Mapping from organisatie.type to geregistreerdDoor value
+ * Mapping from organisatie.type to geregistreerdDoor value.
*/
private const TYPE_MAP = [
'Gemeente' => 'Gemeente',
@@ -664,14 +740,15 @@ private function addQueryFilters(array $baseQuery, array $options): array
];
/**
- * Update geregistreerdDoor on object data based on the organisation's type
+ * Update geregistreerdDoor on object data based on the organisation's type.
*
* Looks up the organisation object by UUID, reads its type, and maps it
* to the appropriate geregistreerdDoor value using TYPE_MAP.
*
- * @param ObjectService $objectService The OpenRegister object service
- * @param array $objectData The object data to update
- * @param string $organisationUuid The UUID of the organisation to look up
+ * @param ObjectService $objectService The OpenRegister object service
+ * @param array $objectData The object data to update
+ * @param string $organisationUuid The UUID of the organisation to look up
+ *
* @return array The updated object data
*/
private function updateGeregistreerdDoor(
@@ -682,7 +759,7 @@ private function updateGeregistreerdDoor(
try {
$organisatieSchemaId = $this->settingsService->getSchemaIdForObjectType('organisatie');
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $registerId = $voorzieningenConfig['register'] ?? null;
+ $registerId = $voorzieningenConfig['register'] ?? null;
if ($organisatieSchemaId === null || $registerId === null) {
return $objectData;
@@ -701,24 +778,30 @@ private function updateGeregistreerdDoor(
}
$organisatieData = $organisatieObject->getObject();
- $orgType = $organisatieData['type'] ?? null;
+ $orgType = $organisatieData['type'] ?? null;
- if ($orgType !== null && isset(self::TYPE_MAP[$orgType])) {
+ if ($orgType !== null && isset(self::TYPE_MAP[$orgType]) === true) {
$objectData['geregistreerdDoor'] = self::TYPE_MAP[$orgType];
- $this->logger->info('Updated geregistreerdDoor during transfer', [
- 'organisationUuid' => $organisationUuid,
- 'orgType' => $orgType,
- 'geregistreerdDoor' => self::TYPE_MAP[$orgType],
- ]);
+ $this->logger->info(
+ 'Updated geregistreerdDoor during transfer',
+ [
+ 'organisationUuid' => $organisationUuid,
+ 'orgType' => $orgType,
+ 'geregistreerdDoor' => self::TYPE_MAP[$orgType],
+ ]
+ );
}
} catch (Exception $e) {
- $this->logger->warning('Failed to update geregistreerdDoor during transfer', [
- 'organisationUuid' => $organisationUuid,
- 'error' => $e->getMessage(),
- ]);
- }
+ $this->logger->warning(
+ 'Failed to update geregistreerdDoor during transfer',
+ [
+ 'organisationUuid' => $organisationUuid,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
return $objectData;
- }
-}
+ }//end updateGeregistreerdDoor()
+}//end class
diff --git a/lib/Service/AangebodenGebruikService.php b/lib/Service/AangebodenGebruikService.php
index eded68cb..5d2b2101 100644
--- a/lib/Service/AangebodenGebruikService.php
+++ b/lib/Service/AangebodenGebruikService.php
@@ -1,23 +1,22 @@
+ *
+ * @category Service
+ * @package OCA\SoftwareCatalog\Service
+ * @author Conduction b.v.
* @copyright 2024 Conduction B.V.
- * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- * @version 1.0.0
- * @link https://github.com/ConductionNL/SoftwareCatalog
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
*/
+declare(strict_types=1);
+
namespace OCA\SoftwareCatalog\Service;
use OCA\OpenRegister\Service\ObjectService;
@@ -30,30 +29,30 @@
/**
* Service for managing offered usage (aangeboden gebruik) operations
- *
+ *
* This service provides operations for querying gebruiks objects where the active
* organization is involved either as the afnemer (consumer) or in the deelnemers
* (participants) array, and for updating the @self property of gebruiks objects.
- *
- * @category Service
- * @package OCA\SoftwareCatalog\Service
- * @author Conduction b.v.
+ *
+ * @category Service
+ * @package OCA\SoftwareCatalog\Service
+ * @author Conduction b.v.
* @copyright 2024 Conduction B.V.
- * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
- * @version 1.0.0
- * @link https://github.com/ConductionNL/SoftwareCatalog
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
*/
class AangebodenGebruikService
{
/**
* Constructor for AangebodenGebruikService
- *
- * @param IAppConfig $config Nextcloud app configuration service
- * @param IAppManager $appManager App manager service for checking available apps
- * @param ContainerInterface $container PSR-11 container interface for dependency injection
- * @param LoggerInterface $logger Logger service for debugging and error reporting
- * @param SettingsService $settingsService Settings service for retrieving configuration
- * @param IUserSession $userSession User session service for current user context
+ *
+ * @param IAppConfig $config Nextcloud app configuration service
+ * @param IAppManager $appManager App manager service for checking available apps
+ * @param ContainerInterface $container PSR-11 container interface for dependency injection
+ * @param LoggerInterface $logger Logger service for debugging and error reporting
+ * @param SettingsService $settingsService Settings service for retrieving configuration
+ * @param IUserSession $userSession User session service for current user context
*/
public function __construct(
private readonly IAppConfig $config,
@@ -63,338 +62,398 @@ public function __construct(
private readonly SettingsService $settingsService,
private readonly IUserSession $userSession
) {
- }
+ }//end __construct()
/**
* Get all gebruiks, koppelingen, and other objects where the active organization is the afnemer (consumer)
- *
+ *
* This method retrieves all objects (gebruiks, koppelingen, modules, etc.) where the active organization
* appears as the afnemer using standard RBAC filtering. It excludes objects where '@self.organisation'
* equals the currently active organisation, meaning only offered objects that haven't been accepted
* (overnomen) by this organisation yet are returned.
- *
- * @param array $options Additional query options (limit, offset, filters, etc.)
- * @return array Array with success status, objects data, and metadata
- * @throws Exception When OpenRegister service is not available
+ *
+ * @param array $options Additional query options (limit, offset, filters, etc.).
+ *
+ * @return array Array with success status, objects data, and metadata.
+ *
+ * @throws Exception When OpenRegister service is not available.
*/
- public function getGebruiksWhereAfnemer(array $options = []): array
+ public function getGebruiksWhereAfnemer(array $options=[]): array
{
- $this->logger->info('Getting gebruiks objects where active org is afnemer', [
- 'options' => $options
- ]);
+ $this->logger->info(
+ 'Getting gebruiks objects where active org is afnemer',
+ [
+ 'options' => $options,
+ ]
+ );
try {
- // Get ObjectService from OpenRegister
+ // Get ObjectService from OpenRegister.
$objectService = $this->getObjectService();
-
- // Get current organization
+
+ // Get current organization.
$currentOrg = $this->getCurrentOrganisation();
- if (!$currentOrg) {
+ if ($currentOrg === null) {
$this->logger->warning('No current organization available for afnemer filtering');
return [
'results' => [],
- 'total' => 0,
- 'page' => 1,
- 'pages' => 0,
- 'limit' => 20,
- 'offset' => 0,
- 'message' => 'No current organization available'
+ 'total' => 0,
+ 'page' => 1,
+ 'pages' => 0,
+ 'limit' => 20,
+ 'offset' => 0,
+ 'message' => 'No current organization available',
];
}
- // Get configuration for gebruiks register/schema
+ // Get configuration for gebruiks register/schema.
$gebruiksConfig = $this->getGebruiksConfiguration();
-
- // Use the first schema for now (can be extended for multi-schema support)
+
+ // Use the first schema for now (can be extended for multi-schema support).
$schemaId = $gebruiksConfig['schemas'][0] ?? null;
- if (!$schemaId) {
+ if ($schemaId === null) {
throw new Exception('No gebruik schema configured');
}
-
- // Build query for afnemer filtering - search for objects where current org is afnemer
- // Note: We don't filter by organisation in @self since the objects are owned by leveranciers
+
+ // Build query for afnemer filtering - search for objects where current org is afnemer.
+ // Note: We don't filter by organisation in @self since the objects are owned by leveranciers.
$query = [
- '@self' => [
+ '@self' => [
'register' => $gebruiksConfig['register_id'],
- 'schema' => $schemaId
+ 'schema' => $schemaId,
],
- 'afnemer' => $currentOrg // Filter by afnemer field instead of ownership
+ // Filter by afnemer field instead of ownership.
+ 'afnemer' => $currentOrg,
];
-
- // Store original pagination parameters
+
+ // Store original pagination parameters.
$requestedLimit = $options['_limit'] ?? 20;
- $requestedPage = $options['_page'] ?? 1;
-
- // Calculate offset from page or use explicit offset
- if (isset($options['_offset'])) {
+ $requestedPage = $options['_page'] ?? 1;
+
+ // Calculate offset from page or use explicit offset.
+ if (isset($options['_offset']) === true) {
$requestedOffset = $options['_offset'];
} else {
- // Calculate offset from page number
+ // Calculate offset from page number.
$requestedOffset = ($requestedPage - 1) * $requestedLimit;
}
-
- // Fetch a large batch for filtering (since we filter post-fetch)
- // We need to fetch more than requested because some will be filtered out
+
+ // Fetch a large batch for filtering (since we filter post-fetch).
+ // We need to fetch more than requested because some will be filtered out.
$fetchOptions = $options;
- $fetchOptions['_limit'] = 1000; // Fetch a large batch
- $fetchOptions['_offset'] = 0; // Always start from beginning for now
- unset($fetchOptions['_page']); // Remove page parameter
-
- // Add additional filters from options (search, extend, etc.)
- $query = $this->addQueryFilters($query, $fetchOptions);
-
- $this->logger->debug('AangebodenGebruikService: Executing search query', [
- 'query' => $query,
- 'schema_id' => $schemaId,
- 'current_org' => $currentOrg,
- 'fetch_limit' => 1000,
- 'requested_limit' => $requestedLimit,
- 'requested_offset' => $requestedOffset
- ]);
-
- // Execute search with RBAC and multitenancy disabled to find cross-organisation objects
- // Fetch a large batch that we'll filter and paginate afterward
+ // Fetch a large batch.
+ $fetchOptions['_limit'] = 1000;
+ // Always start from beginning for now.
+ $fetchOptions['_offset'] = 0;
+ // Remove page parameter.
+ unset($fetchOptions['_page']);
+ // Add additional filters from options (search, extend, etc.).
+ $query = $this->addQueryFilters(baseQuery: $query, options: $fetchOptions);
+
+ $this->logger->debug(
+ 'AangebodenGebruikService: Executing search query',
+ [
+ 'query' => $query,
+ 'schema_id' => $schemaId,
+ 'current_org' => $currentOrg,
+ 'fetch_limit' => 1000,
+ 'requested_limit' => $requestedLimit,
+ 'requested_offset' => $requestedOffset,
+ ]
+ );
+
+ // Execute search with RBAC and multitenancy disabled to find cross-organisation objects.
+ // Fetch a large batch that we'll filter and paginate afterward.
$searchResult = $objectService->searchObjectsPaginated(
query: $query,
- _rbac: false, // Disable RBAC to find cross-organisation objects
- _multitenancy: false // Disable multitenancy to find objects from other organisations
+ // Disable RBAC to find cross-organisation objects.
+ _rbac: false,
+ // Disable multitenancy to find objects from other organisations.
+ _multitenancy: false
);
-
- $this->logger->debug('AangebodenGebruikService: Search completed before filtering', [
- 'total' => $searchResult['total'] ?? 0,
- 'results_count' => count($searchResult['results'] ?? []),
- 'organisation' => $currentOrg
- ]);
-
- // Filter out objects where @self.organisation equals the currently active organisation
- // Only return objects that are offered TO this org but not yet claimed/accepted BY this org
- // This excludes gebruiks, koppelingen, and other objects that have already been accepted (overnomen)
- // Note: @self.organisation is never empty, so we check if it equals the current org
+
+ $this->logger->debug(
+ 'AangebodenGebruikService: Search completed before filtering',
+ [
+ 'total' => $searchResult['total'] ?? 0,
+ 'results_count' => count($searchResult['results'] ?? []),
+ 'organisation' => $currentOrg,
+ ]
+ );
+
+ // Filter out objects where @self.organisation equals the currently active organisation.
+ // Only return objects that are offered TO this org but not yet claimed/accepted BY this org.
+ // This excludes gebruiks, koppelingen, and other objects that have already been accepted (overnomen).
+ // Note: @self.organisation is never empty, so we check if it equals the current org.
$filteredResults = [];
foreach ($searchResult['results'] ?? [] as $result) {
- // Convert ObjectEntity to array if needed
- $resultData = is_array($result) ? $result : $result->getObject();
+ // Convert ObjectEntity to array if needed.
+ if (is_array(value: $result) === true) {
+ $resultData = $result;
+ } else {
+ $resultData = $result->getObject();
+ }
+
$selfOrg = $resultData['@self']['organisation'] ?? null;
-
- // Only include if @self.organisation is NOT set to the current organisation
- // (meaning it hasn't been accepted by this organisation yet)
+
+ // Only include if @self.organisation is NOT set to the current organisation.
+ // (meaning it hasn't been accepted by this organisation yet).
if ($selfOrg !== $currentOrg) {
$filteredResults[] = $result;
}
}
-
- $this->logger->debug('AangebodenGebruikService: Filtering completed', [
- 'original_count' => count($searchResult['results'] ?? []),
- 'filtered_count' => count($filteredResults),
- 'removed_count' => (count($searchResult['results'] ?? []) - count($filteredResults))
- ]);
-
- // Apply pagination to filtered results
- $totalFiltered = count($filteredResults);
- $paginatedResults = array_slice($filteredResults, $requestedOffset, $requestedLimit);
-
- // Calculate pagination metadata
- $totalPages = $requestedLimit > 0
- ? (int) ceil($totalFiltered / $requestedLimit)
- : 1;
- $currentPage = $requestedOffset > 0
- ? (int) floor($requestedOffset / $requestedLimit) + 1
- : $requestedPage;
-
- // Build next/previous links
- $nextLink = null;
- $prevLink = null;
+
+ $this->logger->debug(
+ 'AangebodenGebruikService: Filtering completed',
+ [
+ 'original_count' => count($searchResult['results'] ?? []),
+ 'filtered_count' => count($filteredResults),
+ 'removed_count' => (count($searchResult['results'] ?? []) - count($filteredResults)),
+ ]
+ );
+
+ // Apply pagination to filtered results.
+ $totalFiltered = count($filteredResults);
+ $paginatedResults = array_slice(array: $filteredResults, offset: $requestedOffset, length: $requestedLimit);
+
+ // Calculate pagination metadata.
+ if ($requestedLimit > 0) {
+ $totalPages = (int) ceil(num: $totalFiltered / $requestedLimit);
+ } else {
+ $totalPages = 1;
+ }
+
+ if ($requestedOffset > 0) {
+ $currentPage = (int) floor(num: $requestedOffset / $requestedLimit) + 1;
+ } else {
+ $currentPage = $requestedPage;
+ }
+
+ // Build next/previous links.
+ $nextLink = null;
+ $prevLink = null;
+ $afnemerPath = '/index.php/apps/softwarecatalog/api/aangeboden-gebruik/afnemer';
if ($currentPage < $totalPages) {
$nextPage = $currentPage + 1;
- $nextLink = "/index.php/apps/softwarecatalog/api/aangeboden-gebruik/afnemer?_limit={$requestedLimit}&_source=database&page={$nextPage}";
+ $nextLink = "{$afnemerPath}?_limit={$requestedLimit}&_source=database&page={$nextPage}";
}
+
if ($currentPage > 1) {
$prevPage = $currentPage - 1;
- $prevLink = "/index.php/apps/softwarecatalog/api/aangeboden-gebruik/afnemer?_limit={$requestedLimit}&_source=database&page={$prevPage}";
+ $prevLink = "{$afnemerPath}?_limit={$requestedLimit}&_source=database&page={$prevPage}";
}
-
- $this->logger->debug('AangebodenGebruikService: Pagination applied', [
- 'total_filtered' => $totalFiltered,
- 'requested_limit' => $requestedLimit,
- 'requested_offset' => $requestedOffset,
- 'current_page' => $currentPage,
- 'total_pages' => $totalPages,
- 'returned_count' => count($paginatedResults)
- ]);
-
- // Update the result with paginated filtered data
+
+ $this->logger->debug(
+ 'AangebodenGebruikService: Pagination applied',
+ [
+ 'total_filtered' => $totalFiltered,
+ 'requested_limit' => $requestedLimit,
+ 'requested_offset' => $requestedOffset,
+ 'current_page' => $currentPage,
+ 'total_pages' => $totalPages,
+ 'returned_count' => count($paginatedResults),
+ ]
+ );
+
+ // Update the result with paginated filtered data.
$searchResult['results'] = $paginatedResults;
- $searchResult['total'] = $totalFiltered;
- $searchResult['pages'] = $totalPages;
- $searchResult['page'] = $currentPage;
- $searchResult['limit'] = $requestedLimit;
- $searchResult['offset'] = $requestedOffset;
- if ($nextLink) {
+ $searchResult['total'] = $totalFiltered;
+ $searchResult['pages'] = $totalPages;
+ $searchResult['page'] = $currentPage;
+ $searchResult['limit'] = $requestedLimit;
+ $searchResult['offset'] = $requestedOffset;
+ if ($nextLink !== null) {
$searchResult['next'] = $nextLink;
} else {
unset($searchResult['next']);
}
- if ($prevLink) {
+
+ if ($prevLink !== null) {
$searchResult['previous'] = $prevLink;
}
-
- return $searchResult;
+ return $searchResult;
} catch (Exception $e) {
- $this->logger->error('Failed to get afnemer gebruiks', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'Failed to get afnemer gebruiks',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return [
'results' => [],
- 'total' => 0,
- 'page' => 1,
- 'pages' => 0,
- 'limit' => 20,
- 'offset' => 0,
- 'error' => 'Failed to retrieve gebruiks: ' . $e->getMessage()
+ 'total' => 0,
+ 'page' => 1,
+ 'pages' => 0,
+ 'limit' => 20,
+ 'offset' => 0,
+ 'error' => 'Failed to retrieve gebruiks: '.$e->getMessage(),
];
- }
- }
+ }//end try
+ }//end getGebruiksWhereAfnemer()
/**
* Get koppelingen and gebruiks for a specific organisation, application, or module UUID
- *
+ *
* This method retrieves koppelingen and gebruiks related to a specific UUID based on:
* - If user has ambtenaar role: return all related objects (optionally filtered by organization)
* - If user's organization owns the application/module: return all related usage
* - Otherwise: return empty result
- *
+ *
* The UUID can be:
* - An organisation UUID: returns all gebruiks/koppelingen for that organisation
* - An application/suite UUID: returns all gebruiks/koppelingen that reference that suite
* - A module UUID: returns all gebruiks/koppelingen that reference that module
- *
- * @param string $uuid The UUID of the organisation, application, or module
- * @param array $options Additional query options (limit, offset, filters, organisation, etc.)
- * @param bool $isAmbtenaar Whether the user has ambtenaar privileges
- * @return array searchObjectsPaginated result with koppelingen and gebruiks for the UUID
- * @throws Exception When OpenRegister service is not available
+ *
+ * @param string $uuid The UUID of the organisation, application, or module.
+ * @param array $options Additional query options (limit, offset, filters, organisation, etc.).
+ * @param bool $isAmbtenaar Whether the user has ambtenaar privileges.
+ *
+ * @return array searchObjectsPaginated result with koppelingen and gebruiks for the UUID.
+ *
+ * @throws Exception When OpenRegister service is not available.
*/
- public function getKoppelingenGebruikByUuid(string $uuid, array $options = [], bool $isAmbtenaar = false): array
+ public function getKoppelingenGebruikByUuid(string $uuid, array $options=[], bool $isAmbtenaar=false): array
{
- $this->logger->info('Getting koppelingen and gebruiks for UUID with extended access', [
- 'uuid' => $uuid,
- 'options' => $options,
- 'isAmbtenaar' => $isAmbtenaar
- ]);
+ $this->logger->info(
+ 'Getting koppelingen and gebruiks for UUID with extended access',
+ [
+ 'uuid' => $uuid,
+ 'options' => $options,
+ 'isAmbtenaar' => $isAmbtenaar,
+ ]
+ );
try {
- // Validate input
- if (empty($uuid)) {
+ // Validate input.
+ if (empty($uuid) === true) {
return [
'results' => [],
- 'total' => 0,
- 'page' => 1,
- 'pages' => 0,
- 'limit' => 20,
- 'offset' => 0,
- 'error' => 'UUID is required'
+ 'total' => 0,
+ 'page' => 1,
+ 'pages' => 0,
+ 'limit' => 20,
+ 'offset' => 0,
+ 'error' => 'UUID is required',
];
}
- // Get ObjectService from OpenRegister
+ // Get ObjectService from OpenRegister.
$objectService = $this->getObjectService();
-
- // Get voorzieningen configuration
+
+ // Get voorzieningen configuration.
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $registerId = $voorzieningenConfig['register'] ?? null;
- $gebruikSchema = $voorzieningenConfig['gebruik_schema'] ?? null;
- $koppeligenSchema = $voorzieningenConfig['koppeling_schema'] ?? null;
-
- if (!$registerId || !$gebruikSchema || !$koppeligenSchema) {
- throw new Exception('Voorzieningen configuration not found. Please configure the schemas in the admin panel.');
+ $registerId = $voorzieningenConfig['register'] ?? null;
+ $gebruikSchema = $voorzieningenConfig['gebruik_schema'] ?? null;
+ $koppeligenSchema = $voorzieningenConfig['koppeling_schema'] ?? null;
+
+ if ($registerId === null || $gebruikSchema === null || $koppeligenSchema === null) {
+ throw new Exception(
+ 'Voorzieningen configuration not found. Please configure the schemas in the admin panel.'
+ );
}
-
- // Check access permissions
+
+ // Check access permissions.
$currentOrg = $this->getCurrentOrganisation();
- $hasAccess = false;
-
- if ($isAmbtenaar) {
- // Ambtenaar always has access
+ $hasAccess = false;
+
+ if ($isAmbtenaar === true) {
+ // Ambtenaar always has access.
$hasAccess = true;
- } else if ($currentOrg) {
- // Check if the application/module is owned by user's organization
+ } else if ($currentOrg !== null) {
+ // Check if the application/module is owned by user's organization.
try {
$appObject = $objectService->find(id: $uuid, _rbac: false, _multitenancy: false);
- if ($appObject) {
- $appData = $appObject->getObject();
- $ownerOrg = $appData['@self']['organisation'] ?? null;
+ if ($appObject !== null) {
+ $appData = $appObject->getObject();
+ $ownerOrg = $appData['@self']['organisation'] ?? null;
$hasAccess = ($ownerOrg === $currentOrg);
}
} catch (Exception $e) {
- $this->logger->warning('Failed to check ownership for UUID', [
- 'uuid' => $uuid,
- 'error' => $e->getMessage()
- ]);
+ $this->logger->warning(
+ 'Failed to check ownership for UUID',
+ [
+ 'uuid' => $uuid,
+ 'error' => $e->getMessage(),
+ ]
+ );
}
- }
-
- if (!$hasAccess) {
+ }//end if
+
+ if ($hasAccess === false) {
return [
'results' => [],
- 'total' => 0,
- 'page' => 1,
- 'pages' => 0,
- 'limit' => 20,
- 'offset' => 0
+ 'total' => 0,
+ 'page' => 1,
+ 'pages' => 0,
+ 'limit' => 20,
+ 'offset' => 0,
];
}
-
- // Get organization filter if provided (for ambtenaar)
- $organisationFilter = ($isAmbtenaar && isset($options['organisation'])) ? $options['organisation'] : null;
-
- // Build search query using ObjectService's buildSearchQuery
+
+ // Get organization filter if provided (for ambtenaar).
+ if ($isAmbtenaar === true && isset($options['organisation']) === true) {
+ $organisationFilter = $options['organisation'];
+ } else {
+ $organisationFilter = null;
+ }
+
+ // Build search query using ObjectService's buildSearchQuery.
$searchQuery = $objectService->buildSearchQuery($options);
-
- // Add register and schema filters
+
+ // Add register and schema filters.
$searchQuery['@self']['register'] = $registerId;
- $searchQuery['@self']['schema'] = [$gebruikSchema, $koppeligenSchema];
-
- // Force database source
+ $searchQuery['@self']['schema'] = [$gebruikSchema, $koppeligenSchema];
+
+ // Force database source.
$searchQuery['_source'] = 'database';
-
- // Check if UUID is an organisation UUID by trying to fetch it and checking its schema
+
+ // Check if UUID is an organisation UUID by trying to fetch it and checking its schema.
$isOrganisationUuid = false;
try {
$uuidObject = $objectService->find(id: $uuid, _rbac: false, _multitenancy: false);
- if ($uuidObject) {
- $uuidData = $uuidObject->getObject();
+ if ($uuidObject !== null) {
+ $uuidData = $uuidObject->getObject();
$uuidSchema = $uuidData['@self']['schema'] ?? null;
$organisationSchemaId = $voorzieningenConfig['organisatie_schema'] ?? '15';
- $isOrganisationUuid = ($uuidSchema == $organisationSchemaId);
+ $isOrganisationUuid = ($uuidSchema === $organisationSchemaId);
}
} catch (Exception $e) {
- $this->logger->debug('Could not fetch UUID object, assuming it is not an organisation', [
- 'uuid' => $uuid
- ]);
+ $this->logger->debug(
+ 'Could not fetch UUID object, assuming it is not an organisation',
+ [
+ 'uuid' => $uuid,
+ ]
+ );
}
-
- // Handle organisation UUID filtering differently from suite/module UUIDs
- if ($isOrganisationUuid) {
- // For organisation UUIDs, filter by @self.organisation
+
+ // Handle organisation UUID filtering differently from suite/module UUIDs.
+ if ($isOrganisationUuid === true) {
+ // For organisation UUIDs, filter by @self.organisation.
$searchQuery['@self']['organisation'] = $uuid;
-
- // Apply additional organisation filter if provided by ambtenaar
- if ($organisationFilter) {
- $this->logger->warning('Organisation filter parameter is ignored when UUID is already an organisation', [
- 'uuid' => $uuid,
- 'filter' => $organisationFilter
- ]);
+
+ // Apply additional organisation filter if provided by ambtenaar.
+ if ($organisationFilter !== null) {
+ $this->logger->warning(
+ 'Organisation filter parameter is ignored when UUID is already an organisation',
+ [
+ 'uuid' => $uuid,
+ 'filter' => $organisationFilter,
+ ]
+ );
}
-
- $this->logger->debug('Executing koppelingen-gebruik by organisation UUID', [
- 'uuid' => $uuid,
- 'query' => $searchQuery
- ]);
-
- // Execute paginated search without 'uses' parameter
+
+ $this->logger->debug(
+ 'Executing koppelingen-gebruik by organisation UUID',
+ [
+ 'uuid' => $uuid,
+ 'query' => $searchQuery,
+ ]
+ );
+
+ // Execute paginated search without 'uses' parameter.
$searchResult = $objectService->searchObjectsPaginated(
query: $searchQuery,
_rbac: false,
@@ -403,18 +462,21 @@ public function getKoppelingenGebruikByUuid(string $uuid, array $options = [], b
deleted: false
);
} else {
- // For suite/module UUIDs, use 'uses' parameter to filter by relations
- // Add organization filter if provided
- if ($organisationFilter) {
+ // For suite/module UUIDs, use 'uses' parameter to filter by relations.
+ // Add organization filter if provided.
+ if ($organisationFilter !== null) {
$searchQuery['@self']['organisation'] = $organisationFilter;
}
-
- $this->logger->debug('Executing koppelingen-gebruik by suite/module UUID', [
- 'uuid' => $uuid,
- 'query' => $searchQuery
- ]);
-
- // Execute paginated search using 'uses' parameter to filter by UUID in relations
+
+ $this->logger->debug(
+ 'Executing koppelingen-gebruik by suite/module UUID',
+ [
+ 'uuid' => $uuid,
+ 'query' => $searchQuery,
+ ]
+ );
+
+ // Execute paginated search using 'uses' parameter to filter by UUID in relations.
$searchResult = $objectService->searchObjectsPaginated(
query: $searchQuery,
_rbac: false,
@@ -423,433 +485,502 @@ public function getKoppelingenGebruikByUuid(string $uuid, array $options = [], b
deleted: false,
uses: $uuid
);
- }
-
- $this->logger->debug('Koppelingen-gebruik by UUID search completed', [
- 'uuid' => $uuid,
- 'total' => $searchResult['total'] ?? 0,
- 'results_count' => count($searchResult['results'] ?? [])
- ]);
-
- return $searchResult;
+ }//end if
+
+ $this->logger->debug(
+ 'Koppelingen-gebruik by UUID search completed',
+ [
+ 'uuid' => $uuid,
+ 'total' => $searchResult['total'] ?? 0,
+ 'results_count' => count($searchResult['results'] ?? []),
+ ]
+ );
+ return $searchResult;
} catch (Exception $e) {
- $this->logger->error('Failed to get koppelingen-gebruik by UUID', [
- 'uuid' => $uuid,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'Failed to get koppelingen-gebruik by UUID',
+ [
+ 'uuid' => $uuid,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return [
'results' => [],
- 'total' => 0,
- 'page' => 1,
- 'pages' => 0,
- 'limit' => 20,
- 'offset' => 0,
- 'error' => 'Failed to retrieve objects: ' . $e->getMessage()
+ 'total' => 0,
+ 'page' => 1,
+ 'pages' => 0,
+ 'limit' => 20,
+ 'offset' => 0,
+ 'error' => 'Failed to retrieve objects: '.$e->getMessage(),
];
- }
- }
+ }//end try
+ }//end getKoppelingenGebruikByUuid()
/**
* Get all gebruiks objects (ignoring RBAC and multitenancy) - restricted to ambtenaar group
- *
+ *
* This method retrieves all gebruiks objects regardless of ownership or organization,
* bypassing normal RBAC and multitenancy restrictions. Access is restricted to users
* with the "ambtenaar" group.
- *
- * @deprecated Use getKoppelingenGebruik() instead
- * @param array $options Additional query options (limit, offset, filters, etc.)
- * @return array searchObjectsPaginated result with all gebruiks
- * @throws Exception When OpenRegister service is not available
+ *
+ * @param array $options Additional query options (limit, offset, filters, etc.).
+ *
+ * @deprecated Use getKoppelingenGebruik() instead.
+ *
+ * @return array searchObjectsPaginated result with all gebruiks.
+ *
+ * @throws Exception When OpenRegister service is not available.
*/
- public function getAllGebruiksForAmbtenaar(array $options = []): array
+ public function getAllGebruiksForAmbtenaar(array $options=[]): array
{
- $this->logger->info('Getting all gebruiks objects for ambtenaar (ignoring RBAC/multitenancy)', [
- 'options' => $options
- ]);
+ $this->logger->info(
+ 'Getting all gebruiks objects for ambtenaar (ignoring RBAC/multitenancy)',
+ [
+ 'options' => $options,
+ ]
+ );
try {
- // Get ObjectService from OpenRegister
+ // Get ObjectService from OpenRegister.
$objectService = $this->getObjectService();
-
- // Get configuration for gebruiks register/schema
+
+ // Get configuration for gebruiks register/schema.
$gebruiksConfig = $this->getGebruiksConfiguration();
-
- // Use the first schema for now (can be extended for multi-schema support)
+
+ // Use the first schema for now (can be extended for multi-schema support).
$schemaId = $gebruiksConfig['schemas'][0] ?? null;
- if (!$schemaId) {
+ if ($schemaId === null) {
throw new Exception('No gebruik schema configured');
}
-
- // Build query for all gebruiks - no organization filtering
+
+ // Build query for all gebruiks - no organization filtering.
$query = [
'@self' => [
'register' => $gebruiksConfig['register_id'],
- 'schema' => $schemaId
- ]
+ 'schema' => $schemaId,
+ ],
];
-
- // Add additional filters from options (pagination, search, etc.)
- $query = $this->addQueryFilters($query, $options);
-
- // Force use of database source (not index/SOLR) like PublicationsController
+
+ // Add additional filters from options (pagination, search, etc.).
+ $query = $this->addQueryFilters(baseQuery: $query, options: $options);
+
+ // Force use of database source (not index/SOLR) like PublicationsController.
$query['_source'] = 'database';
-
- $this->logger->debug('AangebodenGebruikService: Executing ambtenaar search query', [
- 'query' => $query,
- 'schema_id' => $schemaId
- ]);
-
- // Execute search with RBAC and multitenancy disabled to get ALL objects
- // Use database source and include unpublished objects
+
+ $this->logger->debug(
+ 'AangebodenGebruikService: Executing ambtenaar search query',
+ [
+ 'query' => $query,
+ 'schema_id' => $schemaId,
+ ]
+ );
+
+ // Execute search with RBAC and multitenancy disabled to get ALL objects.
+ // Use database source and include unpublished objects.
$searchResult = $objectService->searchObjectsPaginated(
query: $query,
- _rbac: false, // Disable RBAC to access all objects
- _multitenancy: false, // Disable multitenancy to access objects from all organisations
- published: false, // Include unpublished objects
- deleted: false // Exclude deleted objects
+ // Disable RBAC to access all objects.
+ _rbac: false,
+ // Disable multitenancy to access objects from all organisations.
+ _multitenancy: false,
+ // Include unpublished objects.
+ published: false,
+ // Exclude deleted objects.
+ deleted: false
);
-
- $this->logger->debug('AangebodenGebruikService: Ambtenaar search completed', [
- 'total' => $searchResult['total'] ?? 0,
- 'results_count' => count($searchResult['results'] ?? [])
- ]);
-
- return $searchResult;
+ $this->logger->debug(
+ 'AangebodenGebruikService: Ambtenaar search completed',
+ [
+ 'total' => $searchResult['total'] ?? 0,
+ 'results_count' => count($searchResult['results'] ?? []),
+ ]
+ );
+
+ return $searchResult;
} catch (Exception $e) {
- $this->logger->error('Failed to get all gebruiks for ambtenaar', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'Failed to get all gebruiks for ambtenaar',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return [
'results' => [],
- 'total' => 0,
- 'page' => 1,
- 'pages' => 0,
- 'limit' => 20,
- 'offset' => 0,
- 'error' => 'Failed to retrieve gebruiks: ' . $e->getMessage()
+ 'total' => 0,
+ 'page' => 1,
+ 'pages' => 0,
+ 'limit' => 20,
+ 'offset' => 0,
+ 'error' => 'Failed to retrieve gebruiks: '.$e->getMessage(),
];
- }
- }
+ }//end try
+ }//end getAllGebruiksForAmbtenaar()
/**
- * Get all gebruiks objects belonging to a specific suite ID (ignoring RBAC and multitenancy) - restricted to ambtenaar group
- *
- * This method retrieves all gebruiks objects that belong to the specified suite ID,
- * bypassing normal RBAC and multitenancy restrictions. Access is restricted to users
+ * Get all gebruiks objects belonging to a specific suite ID - restricted to ambtenaar group.
+ *
+ * This method retrieves all gebruiks objects that belong to the specified suite ID,
+ * bypassing normal RBAC and multitenancy restrictions. Access is restricted to users
* with the "ambtenaar" group.
- *
+ *
* @param string $suiteId The ID of the suite to get gebruiks for
- * @param array $options Additional query options (extend, fields, etc.)
+ * @param array $options Additional query options (extend, fields, etc.)
+ *
* @return array searchObjectsPaginated result with all gebruiks for the suite
+ *
* @throws Exception When OpenRegister service is not available
*/
- public function getSingleGebruikForAmbtenaar(string $suiteId, array $options = []): array
+ public function getSingleGebruikForAmbtenaar(string $suiteId, array $options=[]): array
{
- $this->logger->info('Getting all gebruiks for suite ID for ambtenaar (ignoring RBAC/multitenancy)', [
- 'suite_id' => $suiteId,
- 'options' => $options
- ]);
+ $this->logger->info(
+ 'Getting all gebruiks for suite ID for ambtenaar (ignoring RBAC/multitenancy)',
+ [
+ 'suite_id' => $suiteId,
+ 'options' => $options,
+ ]
+ );
try {
- // Validate input
- if (empty($suiteId)) {
+ // Validate input.
+ if (empty($suiteId) === true) {
return [
'results' => [],
- 'total' => 0,
- 'page' => 1,
- 'pages' => 0,
- 'limit' => 20,
- 'offset' => 0,
- 'error' => 'Suite ID is required'
+ 'total' => 0,
+ 'page' => 1,
+ 'pages' => 0,
+ 'limit' => 20,
+ 'offset' => 0,
+ 'error' => 'Suite ID is required',
];
}
- // Get ObjectService from OpenRegister
+ // Get ObjectService from OpenRegister.
$objectService = $this->getObjectService();
-
- // Get configuration for gebruiks register/schema
+
+ // Get configuration for gebruiks register/schema.
$gebruiksConfig = $this->getGebruiksConfiguration();
-
- // Use the first schema for now (can be extended for multi-schema support)
+
+ // Use the first schema for now (can be extended for multi-schema support).
$schemaId = $gebruiksConfig['schemas'][0] ?? null;
- if (!$schemaId) {
+ if ($schemaId === null) {
throw new Exception('No gebruik schema configured');
}
-
- // Build query for all gebruiks that reference the specified UUID in their relations
- // Follow the same pattern as PublicationsController.php used() method
+
+ // Build query for all gebruiks that reference the specified UUID in their relations.
+ // Follow the same pattern as PublicationsController.php used() method.
$query = [
'@self' => [
'register' => $gebruiksConfig['register_id'],
- 'schema' => $schemaId
- ]
+ 'schema' => $schemaId,
+ ],
];
-
- // Add additional filters from options (extend, fields, etc.)
- $query = $this->addQueryFilters($query, $options);
-
- // Force use of database source (not index/SOLR) like PublicationsController
+
+ // Add additional filters from options (extend, fields, etc.).
+ $query = $this->addQueryFilters(baseQuery: $query, options: $options);
+
+ // Force use of database source (not index/SOLR) like PublicationsController.
$query['_source'] = 'database';
-
- $this->logger->debug('AangebodenGebruikService: Executing uses-based query for ambtenaar', [
- 'query' => $query,
- 'schema_id' => $schemaId,
- 'uses_uuid' => $suiteId
- ]);
-
- // Execute search following PublicationsController.php used() method pattern
- // Use database source and uses parameter for relationship filtering
+
+ $this->logger->debug(
+ 'AangebodenGebruikService: Executing uses-based query for ambtenaar',
+ [
+ 'query' => $query,
+ 'schema_id' => $schemaId,
+ 'uses_uuid' => $suiteId,
+ ]
+ );
+
+ // Execute search following PublicationsController.php used() method pattern.
+ // Use database source and uses parameter for relationship filtering.
$searchResult = $objectService->searchObjectsPaginated(
query: $query,
- _rbac: false, // Disable RBAC to access any object
- _multitenancy: false, // Disable multitenancy to access objects from any organisation
- published: false, // Include unpublished objects
- deleted: false, // Exclude deleted objects
- uses: $suiteId // Find objects that have this UUID in their relations array
+ _rbac: false,
+ // Disable RBAC to access any object.
+ _multitenancy: false,
+ // Disable multitenancy to access objects from any organisation.
+ published: false,
+ // Include unpublished objects.
+ deleted: false,
+ // Exclude deleted objects.
+ uses: $suiteId
+ // Find objects that have this UUID in their relations array.
);
-
- $this->logger->debug('AangebodenGebruikService: Uses-based query completed', [
- 'total' => $searchResult['total'] ?? 0,
- 'results_count' => count($searchResult['results'] ?? []),
- 'uses_uuid' => $suiteId
- ]);
-
- return $searchResult;
+ $this->logger->debug(
+ 'AangebodenGebruikService: Uses-based query completed',
+ [
+ 'total' => $searchResult['total'] ?? 0,
+ 'results_count' => count($searchResult['results'] ?? []),
+ 'uses_uuid' => $suiteId,
+ ]
+ );
+
+ return $searchResult;
} catch (Exception $e) {
- $this->logger->error('Failed to get gebruiks by uses relationship', [
- 'uses_uuid' => $suiteId,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'Failed to get gebruiks by uses relationship',
+ [
+ 'uses_uuid' => $suiteId,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return [
'results' => [],
- 'total' => 0,
- 'page' => 1,
- 'pages' => 0,
- 'limit' => 20,
- 'offset' => 0,
- 'error' => 'Failed to retrieve gebruik: ' . $e->getMessage()
+ 'total' => 0,
+ 'page' => 1,
+ 'pages' => 0,
+ 'limit' => 20,
+ 'offset' => 0,
+ 'error' => 'Failed to retrieve gebruik: '.$e->getMessage(),
];
- }
- }
+ }//end try
+ }//end getSingleGebruikForAmbtenaar()
/**
* Get all gebruiks objects where the active organization is in deelnemers (participants)
- *
+ *
* This method retrieves all gebruiks objects where the active organization
* appears in the deelnemers array, using RBAC-disabled search.
- *
+ *
* @param array $options Additional query options (limit, offset, filters, etc.)
+ *
* @return array Array with success status, gebruiks data, and metadata
+ *
* @throws Exception When OpenRegister service is not available
*/
- public function getGebruiksWhereDeelnemers(array $options = []): array
+ public function getGebruiksWhereDeelnemers(array $options=[]): array
{
- $this->logger->info('Getting gebruiks objects where active org is in deelnemers', [
- 'options' => $options
- ]);
+ $this->logger->info(
+ 'Getting gebruiks objects where active org is in deelnemers',
+ [
+ 'options' => $options,
+ ]
+ );
try {
- // Get ObjectService from OpenRegister
+ // Get ObjectService from OpenRegister.
$objectService = $this->getObjectService();
-
- // Get current organization
+
+ // Get current organization.
$currentOrg = $this->getCurrentOrganisation();
- if (!$currentOrg) {
+ if ($currentOrg === null) {
$this->logger->warning('No current organization available for deelnemers filtering');
return [
- 'success' => true,
+ 'success' => true,
'gebruiks' => [],
- 'count' => 0,
- 'message' => 'No current organization available'
+ 'count' => 0,
+ 'message' => 'No current organization available',
];
}
- // Get configuration for gebruiks register/schema
+ // Get configuration for gebruiks register/schema.
$gebruiksConfig = $this->getGebruiksConfiguration();
-
+
$allGebruiks = [];
-
- // Search each configured schema for gebruiks where org is in deelnemers
+
+ // Search each configured schema for gebruiks where org is in deelnemers.
foreach ($gebruiksConfig['schemas'] as $schemaId) {
- if (!$schemaId) continue;
-
+ if ($schemaId === null) {
+ continue;
+ }
+
try {
- // Build query for deelnemers filtering
+ // Build query for deelnemers filtering.
$query = [
- '@self' => [
+ '@self' => [
'register' => $gebruiksConfig['register_id'],
- 'schema' => $schemaId
+ 'schema' => $schemaId,
],
- 'deelnemers' => $currentOrg // Search where current org is in deelnemers
+ 'deelnemers' => $currentOrg,
+ // Search where current org is in deelnemers.
];
-
- // Add additional filters from options
- $query = $this->addQueryFilters($query, $options);
-
- // Execute search with RBAC disabled to find deelnemers
+
+ // Add additional filters from options.
+ $query = $this->addQueryFilters(baseQuery: $query, options: $options);
+
+ // Execute search with RBAC disabled to find deelnemers.
$gebruikItems = $objectService->searchObjects($query, _rbac: false);
-
- // Process and add to results
+
+ // Process and add to results.
foreach ($gebruikItems as $gebruik) {
- $gebruik['_filter_type'] = 'deelnemers';
- $gebruik['_schema_id'] = $schemaId;
- $allGebruiks[] = $gebruik;
+ $gebruikData = is_array(value: $gebruik) === true
+ ? $gebruik
+ : $gebruik->jsonSerialize();
+ $gebruikData['_filter_type'] = 'deelnemers';
+ $gebruikData['_schema_id'] = $schemaId;
+ $allGebruiks[] = $gebruikData;
}
-
- $this->logger->debug('Retrieved deelnemers gebruiks from schema', [
- 'schema_id' => $schemaId,
- 'count' => count($gebruikItems),
- 'organisation_in_deelnemers' => $currentOrg
- ]);
-
+
+ $this->logger->debug(
+ 'Retrieved deelnemers gebruiks from schema',
+ [
+ 'schema_id' => $schemaId,
+ 'count' => count($gebruikItems),
+ 'organisation_in_deelnemers' => $currentOrg,
+ ]
+ );
} catch (Exception $e) {
- $this->logger->warning('Failed to get deelnemers gebruiks from schema', [
- 'schema_id' => $schemaId,
- 'error' => $e->getMessage()
- ]);
- }
- }
+ $this->logger->warning(
+ 'Failed to get deelnemers gebruiks from schema',
+ [
+ 'schema_id' => $schemaId,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end foreach
return [
- 'success' => true,
- 'gebruiks' => $allGebruiks,
- 'count' => count($allGebruiks),
- 'filter_type' => 'deelnemers',
- 'organisation' => $currentOrg
+ 'success' => true,
+ 'gebruiks' => $allGebruiks,
+ 'count' => count($allGebruiks),
+ 'filter_type' => 'deelnemers',
+ 'organisation' => $currentOrg,
];
-
} catch (Exception $e) {
- $this->logger->error('Failed to get deelnemers gebruiks', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'Failed to get deelnemers gebruiks',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return [
- 'success' => false,
- 'error' => 'Failed to retrieve gebruiks: ' . $e->getMessage(),
+ 'success' => false,
+ 'error' => 'Failed to retrieve gebruiks: '.$e->getMessage(),
'gebruiks' => [],
- 'count' => 0
+ 'count' => 0,
];
- }
- }
+ }//end try
+ }//end getGebruiksWhereDeelnemers()
/**
* Set the @self property of a gebruik to the active organization
- *
+ *
* This method updates the @self.organisation property of a specific gebruik
* or koppeling object, but only if the active organization is the afnemer
* (consumer) or aanbieder (provider) for that object.
- *
+ *
* @param string $gebruikId The UUID of the gebruik or koppeling object to update
- * @param array $options Additional update options
+ * @param array $options Additional update options
+ *
* @return array Result with success status and updated object data
+ *
* @throws Exception When OpenRegister service is not available or operation fails
*/
- public function setGebruikSelfToActiveOrg(string $gebruikId, array $options = []): array
+ public function setGebruikSelfToActiveOrg(string $gebruikId, array $options=[]): array
{
- $this->logger->info('Setting gebruik @self property to active organisation', [
- 'gebruik_id' => $gebruikId,
- 'options' => $options
- ]);
+ $this->logger->info(
+ 'Setting gebruik @self property to active organisation',
+ [
+ 'gebruik_id' => $gebruikId,
+ 'options' => $options,
+ ]
+ );
try {
- // Validate input
- if (empty($gebruikId)) {
+ // Validate input.
+ if (empty($gebruikId) === true) {
return [
'success' => false,
- 'error' => 'Gebruik ID is required',
- 'gebruik' => null
+ 'error' => 'Gebruik ID is required',
+ 'gebruik' => null,
];
}
- // Get ObjectService from OpenRegister
+ // Get ObjectService from OpenRegister.
$objectService = $this->getObjectService();
-
- // Get current organization
+
+ // Get current organization.
$currentOrg = $this->getCurrentOrganisation();
- if (!$currentOrg) {
+ if ($currentOrg === null) {
return [
'success' => false,
- 'error' => 'No current organization available',
- 'gebruik' => null
+ 'error' => 'No current organization available',
+ 'gebruik' => null,
];
}
- // Find the gebruik or koppeling object across possible schemas
- // Register/schema context is required to search magic tables
- $existingGebruik = $this->findGebruikOrKoppeling($objectService, $gebruikId);
+ // Find the gebruik or koppeling object across possible schemas.
+ // Register/schema context is required to search magic tables.
+ $existingGebruik = $this->findGebruikOrKoppeling(objectService: $objectService, objectId: $gebruikId);
- if (!$existingGebruik) {
+ if ($existingGebruik === null) {
return [
'success' => false,
- 'error' => 'Gebruik object not found',
- 'gebruik' => null
+ 'error' => 'Gebruik object not found',
+ 'gebruik' => null,
];
}
- // Verify that the active organization is either the afnemer or aanbieder
- $gebruikData = $existingGebruik->getObject();
- $afnemerInfo = $gebruikData['afnemer'] ?? null;
+ // Verify that the active organization is either the afnemer or aanbieder.
+ $gebruikData = $existingGebruik->getObject();
+ $afnemerInfo = $gebruikData['afnemer'] ?? null;
$aanbiederInfo = $gebruikData['aanbieder'] ?? null;
- // Check various ways the afnemer might be stored (UUID, object, or string)
+ // Check various ways the afnemer might be stored (UUID, object, or string).
$afnemerId = null;
- if (is_array($afnemerInfo) && isset($afnemerInfo['id'])) {
+ if (is_array(value: $afnemerInfo) === true && isset($afnemerInfo['id']) === true) {
$afnemerId = $afnemerInfo['id'];
- } elseif (is_string($afnemerInfo)) {
+ } else if (is_string(value: $afnemerInfo) === true) {
$afnemerId = $afnemerInfo;
}
- // Check various ways the aanbieder might be stored (UUID, object, or string)
+ // Check various ways the aanbieder might be stored (UUID, object, or string).
$aanbiederId = null;
- if (is_array($aanbiederInfo) && isset($aanbiederInfo['id'])) {
+ if (is_array(value: $aanbiederInfo) === true && isset($aanbiederInfo['id']) === true) {
$aanbiederId = $aanbiederInfo['id'];
- } elseif (is_string($aanbiederInfo)) {
+ } else if (is_string(value: $aanbiederInfo) === true) {
$aanbiederId = $aanbiederInfo;
}
- // Allow operation if current org is either afnemer or aanbieder
- $isAfnemer = ($afnemerId && $afnemerId === $currentOrg);
- $isAanbieder = ($aanbiederId && $aanbiederId === $currentOrg);
+ // Allow operation if current org is either afnemer or aanbieder.
+ $isAfnemer = ($afnemerId !== null && $afnemerId === $currentOrg);
+ $isAanbieder = ($aanbiederId !== null && $aanbiederId === $currentOrg);
- if (!$isAfnemer && !$isAanbieder) {
+ if ($isAfnemer === false && $isAanbieder === false) {
return [
'success' => false,
- 'error' => 'Operation not allowed: active organization is not the afnemer or aanbieder',
+ 'error' => 'Operation not allowed: active organization is not the afnemer or aanbieder',
'gebruik' => null,
- 'debug' => [
- 'afnemer_in_object' => $afnemerInfo,
- 'resolved_afnemer_id' => $afnemerId,
- 'aanbieder_in_object' => $aanbiederInfo,
+ 'debug' => [
+ 'afnemer_in_object' => $afnemerInfo,
+ 'resolved_afnemer_id' => $afnemerId,
+ 'aanbieder_in_object' => $aanbiederInfo,
'resolved_aanbieder_id' => $aanbiederId,
- 'current_org' => $currentOrg
- ]
+ 'current_org' => $currentOrg,
+ ],
];
}
- // Update the @self.organisation and @self.owner properties
- // Both must be set so the accepting user has permission to read/update the object
+ // Update the @self.organisation and @self.owner properties.
+ // Both must be set so the accepting user has permission to read/update the object.
$currentUser = $this->userSession->getUser();
- $selfData = ['organisation' => $currentOrg];
+ $selfData = ['organisation' => $currentOrg];
if ($currentUser !== null) {
$selfData['owner'] = $currentUser->getUID();
}
+
$gebruikData['@self'] = $selfData;
- // Update geregistreerdDoor based on the accepting organisation's type
- $gebruikData = $this->updateGeregistreerdDoor($objectService, $gebruikData, $currentOrg);
+ // Update geregistreerdDoor based on the accepting organisation's type.
+ $gebruikData = $this->updateGeregistreerdDoor(
+ objectService: $objectService,
+ objectData: $gebruikData,
+ organisationUuid: $currentOrg
+ );
- // Save the updated object with RBAC and multitenancy disabled
- // Use register/schema from the found entity for correct table routing
+ // Save the updated object with RBAC and multitenancy disabled.
+ // Use register/schema from the found entity for correct table routing.
$existingGebruik->setObject($gebruikData);
$updatedGebruik = $objectService->saveObject(
object: $existingGebruik,
@@ -860,37 +991,42 @@ public function setGebruikSelfToActiveOrg(string $gebruikId, array $options = []
_multitenancy: false
);
- $this->logger->info('Successfully updated gebruik @self property', [
- 'gebruik_id' => $gebruikId,
- 'organisation' => $currentOrg,
- 'owner' => $currentUser?->getUID(),
- 'is_afnemer' => $isAfnemer,
- 'is_aanbieder' => $isAanbieder,
- 'afnemer_id' => $afnemerId,
- 'aanbieder_id' => $aanbiederId
- ]);
+ $this->logger->info(
+ 'Successfully updated gebruik @self property',
+ [
+ 'gebruik_id' => $gebruikId,
+ 'organisation' => $currentOrg,
+ 'owner' => $currentUser?->getUID(),
+ 'is_afnemer' => $isAfnemer,
+ 'is_aanbieder' => $isAanbieder,
+ 'afnemer_id' => $afnemerId,
+ 'aanbieder_id' => $aanbiederId,
+ ]
+ );
return [
- 'success' => true,
- 'message' => 'Gebruik @self property updated successfully',
- 'gebruik' => $updatedGebruik->getObject(),
- 'updated_fields' => ['@self.organisation', '@self.owner']
+ 'success' => true,
+ 'message' => 'Gebruik @self property updated successfully',
+ 'gebruik' => $updatedGebruik->getObject(),
+ 'updated_fields' => ['@self.organisation', '@self.owner'],
];
-
} catch (Exception $e) {
- $this->logger->error('Failed to update gebruik @self property', [
- 'gebruik_id' => $gebruikId,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'Failed to update gebruik @self property',
+ [
+ 'gebruik_id' => $gebruikId,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return [
'success' => false,
- 'error' => 'Failed to update gebruik: ' . $e->getMessage(),
- 'gebruik' => null
+ 'error' => 'Failed to update gebruik: '.$e->getMessage(),
+ 'gebruik' => null,
];
- }
- }
+ }//end try
+ }//end setGebruikSelfToActiveOrg()
/**
* Find a gebruik or koppeling object by UUID across possible schemas
@@ -900,24 +1036,29 @@ public function setGebruikSelfToActiveOrg(string $gebruikId, array $options = []
* stored in magic tables.
*
* @param ObjectService $objectService The OpenRegister object service
- * @param string $objectId The UUID of the object to find
+ * @param string $objectId The UUID of the object to find
+ *
* @return \OCA\OpenRegister\Db\ObjectEntity|null The found object or null
*/
- private function findGebruikOrKoppeling(ObjectService $objectService, string $objectId): ?\OCA\OpenRegister\Db\ObjectEntity
- {
+ private function findGebruikOrKoppeling(
+ ObjectService $objectService,
+ string $objectId
+ ): ?\OCA\OpenRegister\Db\ObjectEntity {
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $registerId = $voorzieningenConfig['register'] ?? null;
+ $registerId = $voorzieningenConfig['register'] ?? null;
- if (!$registerId) {
+ if ($registerId === null) {
$this->logger->warning('Cannot find object: no register configured');
return null;
}
- // Search across gebruik and koppeling schemas
- $schemasToTry = array_filter([
- $voorzieningenConfig['gebruik_schema'] ?? null,
- $voorzieningenConfig['koppeling_schema'] ?? null,
- ]);
+ // Search across gebruik and koppeling schemas.
+ $schemasToTry = array_filter(
+ [
+ $voorzieningenConfig['gebruik_schema'] ?? null,
+ $voorzieningenConfig['koppeling_schema'] ?? null,
+ ]
+ );
foreach ($schemasToTry as $schemaId) {
try {
@@ -928,22 +1069,25 @@ private function findGebruikOrKoppeling(ObjectService $objectService, string $ob
_rbac: false,
_multitenancy: false
);
- if ($object) {
+ if ($object !== null) {
return $object;
}
} catch (Exception $e) {
- // Object not found in this schema, try next
+ // Object not found in this schema, try next.
continue;
}
}
- $this->logger->warning('Object not found in any gebruik/koppeling schema', [
- 'object_id' => $objectId,
- 'schemas_tried' => $schemasToTry
- ]);
+ $this->logger->warning(
+ 'Object not found in any gebruik/koppeling schema',
+ [
+ 'object_id' => $objectId,
+ 'schemas_tried' => $schemasToTry,
+ ]
+ );
return null;
- }
+ }//end findGebruikOrKoppeling()
/**
* Get current active organisation for filtering
@@ -953,174 +1097,200 @@ private function findGebruikOrKoppeling(ObjectService $objectService, string $ob
private function getCurrentOrganisation(): ?string
{
$user = $this->userSession->getUser();
- if (!$user) {
+ if ($user === null) {
return null;
}
-
+
try {
- // Get the OpenRegister OrganisationService to get the active organisation
+ // Get the OpenRegister OrganisationService to get the active organisation.
$organisationService = $this->container->get('OCA\OpenRegister\Service\OrganisationService');
- $activeOrg = $organisationService->getActiveOrganisation();
-
- if ($activeOrg) {
+ $activeOrg = $organisationService->getActiveOrganisation();
+
+ if ($activeOrg !== null) {
return $activeOrg->getUuid();
}
-
+
return null;
} catch (Exception $e) {
- $this->logger->error('Failed to get current organisation from OpenRegister', [
- 'error' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to get current organisation from OpenRegister',
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
return null;
}
- }
+ }//end getCurrentOrganisation()
/**
* Get ObjectService from OpenRegister app
- *
+ *
* @return ObjectService The OpenRegister object service
* @throws Exception When OpenRegister service is not available
*/
private function getObjectService(): ObjectService
{
- if (!in_array('openregister', $this->appManager->getInstalledApps())) {
+ if (in_array(needle: 'openregister', haystack: $this->appManager->getInstalledApps()) === false) {
throw new Exception('OpenRegister app is not installed');
}
try {
return $this->container->get('OCA\OpenRegister\Service\ObjectService');
} catch (Exception $e) {
- throw new Exception('Failed to get OpenRegister service: ' . $e->getMessage());
+ throw new Exception('Failed to get OpenRegister service: '.$e->getMessage());
}
- }
+ }//end getObjectService()
/**
* Get configuration for gebruiks objects (register ID and schema IDs)
- *
+ *
* @return array Configuration with register_id and schemas array
* @throws Exception When configuration is not available
*/
private function getGebruiksConfiguration(): array
{
- // Try to get voorzieningen configuration from SettingsService
+ // Try to get voorzieningen configuration from SettingsService.
try {
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
-
- $this->logger->debug('Retrieved voorzieningen configuration', [
- 'config' => $voorzieningenConfig
- ]);
-
- $registerId = $voorzieningenConfig['register'] ?? null;
+
+ $this->logger->debug(
+ 'Retrieved voorzieningen configuration',
+ [
+ 'config' => $voorzieningenConfig,
+ ]
+ );
+
+ $registerId = $voorzieningenConfig['register'] ?? null;
$gebruikSchema = $voorzieningenConfig['gebruik_schema'] ?? null;
-
- // If configuration is available, use it
- if ($registerId && $gebruikSchema) {
+
+ // If configuration is available, use it.
+ if ($registerId !== null && $gebruikSchema !== null) {
return [
'register_id' => $registerId,
- 'schemas' => [$gebruikSchema]
+ 'schemas' => [$gebruikSchema],
];
}
} catch (Exception $e) {
- $this->logger->warning('Failed to get voorzieningen configuration from SettingsService', [
- 'error' => $e->getMessage()
- ]);
- }
-
- // No hardcoded fallback - configuration must be properly set
- $this->logger->error('Failed to get voorzieningen configuration - no fallback provided', [
- 'registerId' => $registerId ?? 'null',
- 'gebruikSchema' => $gebruikSchema ?? 'null',
- 'voorzieningenConfig' => $voorzieningenConfig ?? 'null'
- ]);
-
+ $this->logger->warning(
+ 'Failed to get voorzieningen configuration from SettingsService',
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+
+ // No hardcoded fallback - configuration must be properly set.
+ $this->logger->error(
+ 'Failed to get voorzieningen configuration - no fallback provided',
+ [
+ 'registerId' => $registerId ?? 'null',
+ 'gebruikSchema' => $gebruikSchema ?? 'null',
+ 'voorzieningenConfig' => $voorzieningenConfig ?? 'null',
+ ]
+ );
+
throw new Exception('Voorzieningen configuration not found. Please configure the schemas in the admin panel.');
- }
+ }//end getGebruiksConfiguration()
/**
* Get configuration for koppelingen objects (register ID and schema IDs)
- *
+ *
* @return array Configuration with register_id and schemas array
* @throws Exception When configuration is not available
*/
private function getKoppelingenConfiguration(): array
{
- // Try to get voorzieningen configuration from SettingsService
+ // Try to get voorzieningen configuration from SettingsService.
try {
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
-
- $this->logger->debug('Retrieved voorzieningen configuration for koppelingen', [
- 'config' => $voorzieningenConfig
- ]);
-
- $registerId = $voorzieningenConfig['register'] ?? null;
+
+ $this->logger->debug(
+ 'Retrieved voorzieningen configuration for koppelingen',
+ [
+ 'config' => $voorzieningenConfig,
+ ]
+ );
+
+ $registerId = $voorzieningenConfig['register'] ?? null;
$koppeligenSchema = $voorzieningenConfig['koppeling_schema'] ?? null;
-
- // If configuration is available, use it
- if ($registerId && $koppeligenSchema) {
+
+ // If configuration is available, use it.
+ if ($registerId !== null && $koppeligenSchema !== null) {
return [
'register_id' => $registerId,
- 'schemas' => [$koppeligenSchema]
+ 'schemas' => [$koppeligenSchema],
];
}
} catch (Exception $e) {
- $this->logger->warning('Failed to get koppelingen configuration from SettingsService', [
- 'error' => $e->getMessage()
- ]);
- }
-
- // No hardcoded fallback - configuration must be properly set
- $this->logger->error('Failed to get koppelingen configuration - no fallback provided', [
- 'registerId' => $registerId ?? 'null',
- 'koppeligenSchema' => $koppeligenSchema ?? 'null',
- 'voorzieningenConfig' => $voorzieningenConfig ?? 'null'
- ]);
-
+ $this->logger->warning(
+ 'Failed to get koppelingen configuration from SettingsService',
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+
+ // No hardcoded fallback - configuration must be properly set.
+ $this->logger->error(
+ 'Failed to get koppelingen configuration - no fallback provided',
+ [
+ 'registerId' => $registerId ?? 'null',
+ 'koppeligenSchema' => $koppeligenSchema ?? 'null',
+ 'voorzieningenConfig' => $voorzieningenConfig ?? 'null',
+ ]
+ );
+
throw new Exception('Koppelingen configuration not found. Please configure the schemas in the admin panel.');
- }
+ }//end getKoppelingenConfiguration()
/**
* Get all objects for a specific schema using paginated search, optionally filtered by organization
- *
+ *
* Uses ObjectService's buildSearchQuery() for proper query construction
- *
- * @param ObjectService $objectService The OpenRegister object service
- * @param string $registerId The register ID
- * @param string $schemaId The schema ID
- * @param array $options Query options (includes request parameters for buildSearchQuery)
- * @param string|null $organisationFilter Optional organization UUID to filter by
+ *
+ * @param ObjectService $objectService The OpenRegister object service
+ * @param string $registerId The register ID
+ * @param string $schemaId The schema ID
+ * @param array $options Query options (includes request parameters for buildSearchQuery)
+ * @param string|null $organisationFilter Optional organization UUID to filter by
+ *
* @return array Paginated result from searchObjectsPaginated
+ *
* @throws Exception When query fails
*/
private function getAllObjectsForSchema(
ObjectService $objectService,
string $registerId,
string $schemaId,
- array $options = [],
- ?string $organisationFilter = null
+ array $options=[],
+ ?string $organisationFilter=null
): array {
- // Use ObjectService's buildSearchQuery to properly handle request parameters
+ // Use ObjectService's buildSearchQuery to properly handle request parameters.
$searchQuery = $objectService->buildSearchQuery($options);
-
- // Add schema and register filters
- $searchQuery['@self']['schema'] = $schemaId;
+
+ // Add schema and register filters.
+ $searchQuery['@self']['schema'] = $schemaId;
$searchQuery['@self']['register'] = $registerId;
-
- // Add organization filter if provided
- if ($organisationFilter) {
+
+ // Add organization filter if provided.
+ if ($organisationFilter !== null) {
$searchQuery['@self']['organisation'] = $organisationFilter;
}
-
- // Force database source for real-time data
+
+ // Force database source for real-time data.
$searchQuery['_source'] = 'database';
-
- $this->logger->debug('Getting all objects for schema (paginated)', [
- 'register' => $registerId,
- 'schema' => $schemaId,
- 'organisation_filter' => $organisationFilter,
- 'query' => $searchQuery
- ]);
-
- // Execute search with RBAC and multitenancy disabled using paginated search
+
+ $this->logger->debug(
+ 'Getting all objects for schema (paginated)',
+ [
+ 'register' => $registerId,
+ 'schema' => $schemaId,
+ 'organisation_filter' => $organisationFilter,
+ 'query' => $searchQuery,
+ ]
+ );
+
+ // Execute search with RBAC and multitenancy disabled using paginated search.
$searchResult = $objectService->searchObjectsPaginated(
query: $searchQuery,
_rbac: false,
@@ -1128,16 +1298,18 @@ private function getAllObjectsForSchema(
published: false,
deleted: false
);
-
+
return $searchResult;
- }
+ }//end getAllObjectsForSchema()
/**
* Get all applications/modules owned by an organization
- *
- * @param ObjectService $objectService The OpenRegister object service
- * @param string $organisationUuid The organization UUID
+ *
+ * @param ObjectService $objectService The OpenRegister object service
+ * @param string $organisationUuid The organization UUID
+ *
* @return array Array of application/module UUIDs
+ *
* @throws Exception When query fails
*/
private function getApplicationsOwnedByOrganisation(
@@ -1146,91 +1318,108 @@ private function getApplicationsOwnedByOrganisation(
): array {
try {
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $registerId = $voorzieningenConfig['register'] ?? null;
-
- if (!$registerId) {
+ $registerId = $voorzieningenConfig['register'] ?? null;
+
+ if ($registerId === null) {
return [];
}
-
+
$appUuids = [];
-
- // Check suite schema (applications)
- if (isset($voorzieningenConfig['suite_schema'])) {
+
+ // Check suite schema (applications).
+ if (isset($voorzieningenConfig['suite_schema']) === true) {
$suiteQuery = [
- '@self' => [
- 'register' => $registerId,
- 'schema' => $voorzieningenConfig['suite_schema'],
- 'organisation' => $organisationUuid
+ '@self' => [
+ 'register' => $registerId,
+ 'schema' => $voorzieningenConfig['suite_schema'],
+ 'organisation' => $organisationUuid,
],
- '_source' => 'database'
+ '_source' => 'database',
];
-
+
$suites = $objectService->searchObjects(
query: $suiteQuery,
_rbac: false,
_multitenancy: false
);
-
+
foreach ($suites as $suite) {
- $suiteData = is_array($suite) ? $suite : $suite->getObject();
+ if (is_array(value: $suite) === true) {
+ $suiteData = $suite;
+ } else {
+ $suiteData = $suite->getObject();
+ }
+
$appUuids[] = $suiteData['uuid'] ?? $suiteData['id'] ?? null;
}
- }
-
- // Check module schema
- if (isset($voorzieningenConfig['module_schema'])) {
+ }//end if
+
+ // Check module schema.
+ if (isset($voorzieningenConfig['module_schema']) === true) {
$moduleQuery = [
- '@self' => [
- 'register' => $registerId,
- 'schema' => $voorzieningenConfig['module_schema'],
- 'organisation' => $organisationUuid
+ '@self' => [
+ 'register' => $registerId,
+ 'schema' => $voorzieningenConfig['module_schema'],
+ 'organisation' => $organisationUuid,
],
- '_source' => 'database'
+ '_source' => 'database',
];
-
+
$modules = $objectService->searchObjects(
query: $moduleQuery,
_rbac: false,
_multitenancy: false
);
-
+
foreach ($modules as $module) {
- $moduleData = is_array($module) ? $module : $module->getObject();
+ if (is_array(value: $module) === true) {
+ $moduleData = $module;
+ } else {
+ $moduleData = $module->getObject();
+ }
+
$appUuids[] = $moduleData['uuid'] ?? $moduleData['id'] ?? null;
}
- }
-
- // Filter out nulls
+ }//end if
+
+ // Filter out nulls.
$appUuids = array_filter($appUuids);
-
- $this->logger->debug('Found applications owned by organization', [
- 'organisation' => $organisationUuid,
- 'count' => count($appUuids)
- ]);
-
+
+ $this->logger->debug(
+ 'Found applications owned by organization',
+ [
+ 'organisation' => $organisationUuid,
+ 'count' => count($appUuids),
+ ]
+ );
+
return $appUuids;
-
} catch (Exception $e) {
- $this->logger->error('Failed to get applications owned by organization', [
- 'organisation' => $organisationUuid,
- 'error' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to get applications owned by organization',
+ [
+ 'organisation' => $organisationUuid,
+ 'error' => $e->getMessage(),
+ ]
+ );
return [];
- }
- }
+ }//end try
+ }//end getApplicationsOwnedByOrganisation()
/**
* Get objects related to a specific UUID via the uses relationship using paginated search
- *
+ *
* Uses ObjectService's buildSearchQuery() for proper query construction
- *
- * @param ObjectService $objectService The OpenRegister object service
- * @param string $registerId The register ID
- * @param string $schemaId The schema ID
- * @param string $relatedUuid The UUID to find relationships for
- * @param array $options Query options (includes request parameters for buildSearchQuery)
- * @param string|null $organisationFilter Optional organization UUID to filter by
+ *
+ * @param ObjectService $objectService The OpenRegister object service
+ * @param string $registerId The register ID
+ * @param string $schemaId The schema ID
+ * @param string $relatedUuid The UUID to find relationships for
+ * @param array $options Query options (includes request parameters for buildSearchQuery)
+ * @param string|null $organisationFilter Optional organization UUID to filter by
+ *
* @return array Paginated result from searchObjectsPaginated
+ *
* @throws Exception When query fails
*/
private function getObjectsRelatedToUuid(
@@ -1238,33 +1427,36 @@ private function getObjectsRelatedToUuid(
string $registerId,
string $schemaId,
string $relatedUuid,
- array $options = [],
- ?string $organisationFilter = null
+ array $options=[],
+ ?string $organisationFilter=null
): array {
- // Use ObjectService's buildSearchQuery to properly handle request parameters
+ // Use ObjectService's buildSearchQuery to properly handle request parameters.
$searchQuery = $objectService->buildSearchQuery($options);
-
- // Add schema and register filters
- $searchQuery['@self']['schema'] = $schemaId;
+
+ // Add schema and register filters.
+ $searchQuery['@self']['schema'] = $schemaId;
$searchQuery['@self']['register'] = $registerId;
-
- // Add organization filter if provided
- if ($organisationFilter) {
+
+ // Add organization filter if provided.
+ if ($organisationFilter !== null) {
$searchQuery['@self']['organisation'] = $organisationFilter;
}
-
- // Force database source for real-time data
+
+ // Force database source for real-time data.
$searchQuery['_source'] = 'database';
-
- $this->logger->debug('Getting objects related to UUID (paginated)', [
- 'register' => $registerId,
- 'schema' => $schemaId,
- 'related_uuid' => $relatedUuid,
- 'organisation_filter' => $organisationFilter,
- 'query' => $searchQuery
- ]);
-
- // Execute search using the uses parameter to find relationships with pagination
+
+ $this->logger->debug(
+ 'Getting objects related to UUID (paginated)',
+ [
+ 'register' => $registerId,
+ 'schema' => $schemaId,
+ 'related_uuid' => $relatedUuid,
+ 'organisation_filter' => $organisationFilter,
+ 'query' => $searchQuery,
+ ]
+ );
+
+ // Execute search using the uses parameter to find relationships with pagination.
$searchResult = $objectService->searchObjectsPaginated(
query: $searchQuery,
_rbac: false,
@@ -1273,16 +1465,17 @@ private function getObjectsRelatedToUuid(
deleted: false,
uses: $relatedUuid
);
-
+
return $searchResult;
- }
+ }//end getObjectsRelatedToUuid()
/**
* Check if an organization owns a specific application/module
- *
- * @param ObjectService $objectService The OpenRegister object service
- * @param string $appUuid The application/module UUID
- * @param string $organisationUuid The organization UUID
+ *
+ * @param ObjectService $objectService The OpenRegister object service
+ * @param string $appUuid The application/module UUID
+ * @param string $organisationUuid The organization UUID
+ *
* @return bool True if organization owns the application/module
*/
private function checkOrganisationOwnership(
@@ -1291,41 +1484,46 @@ private function checkOrganisationOwnership(
string $organisationUuid
): bool {
try {
- // Get the application/module object
+ // Get the application/module object.
$appObject = $objectService->find(
id: $appUuid,
_rbac: false,
_multitenancy: false
);
-
- if (!$appObject) {
+
+ if ($appObject === null) {
return false;
}
-
- // Check if the organization owns it
- $appData = $appObject->getObject();
+
+ // Check if the organization owns it.
+ $appData = $appObject->getObject();
$ownerOrg = $appData['@self']['organisation'] ?? null;
-
+
$isOwner = ($ownerOrg === $organisationUuid);
-
- $this->logger->debug('Checked organization ownership', [
- 'app_uuid' => $appUuid,
- 'organisation' => $organisationUuid,
- 'owner_org' => $ownerOrg,
- 'is_owner' => $isOwner
- ]);
-
+
+ $this->logger->debug(
+ 'Checked organization ownership',
+ [
+ 'app_uuid' => $appUuid,
+ 'organisation' => $organisationUuid,
+ 'owner_org' => $ownerOrg,
+ 'is_owner' => $isOwner,
+ ]
+ );
+
return $isOwner;
-
} catch (Exception $e) {
- $this->logger->error('Failed to check organization ownership', [
- 'app_uuid' => $appUuid,
- 'organisation' => $organisationUuid,
- 'error' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to check organization ownership',
+ [
+ 'app_uuid' => $appUuid,
+ 'organisation' => $organisationUuid,
+ 'error' => $e->getMessage(),
+ ]
+ );
return false;
- }
- }
+ }//end try
+ }//end checkOrganisationOwnership()
/**
* Mapping from organisatie.type to geregistreerdDoor value
@@ -1343,9 +1541,10 @@ private function checkOrganisationOwnership(
* Looks up the organisation object by UUID, reads its type, and maps it
* to the appropriate geregistreerdDoor value using TYPE_MAP.
*
- * @param ObjectService $objectService The OpenRegister object service
- * @param array $objectData The object data to update
- * @param string $organisationUuid The UUID of the organisation to look up
+ * @param ObjectService $objectService The OpenRegister object service
+ * @param array $objectData The object data to update
+ * @param string $organisationUuid The UUID of the organisation to look up
+ *
* @return array The updated object data
*/
private function updateGeregistreerdDoor(
@@ -1356,7 +1555,7 @@ private function updateGeregistreerdDoor(
try {
$organisatieSchemaId = $this->settingsService->getSchemaIdForObjectType('organisatie');
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $registerId = $voorzieningenConfig['register'] ?? null;
+ $registerId = $voorzieningenConfig['register'] ?? null;
if ($organisatieSchemaId === null || $registerId === null) {
return $objectData;
@@ -1375,26 +1574,32 @@ private function updateGeregistreerdDoor(
}
$organisatieData = $organisatieObject->getObject();
- $orgType = $organisatieData['type'] ?? null;
+ $orgType = $organisatieData['type'] ?? null;
- if ($orgType !== null && isset(self::TYPE_MAP[$orgType])) {
+ if ($orgType !== null && isset(self::TYPE_MAP[$orgType]) === true) {
$objectData['geregistreerdDoor'] = self::TYPE_MAP[$orgType];
- $this->logger->info('Updated geregistreerdDoor during transfer', [
- 'organisationUuid' => $organisationUuid,
- 'orgType' => $orgType,
- 'geregistreerdDoor' => self::TYPE_MAP[$orgType],
- ]);
+ $this->logger->info(
+ 'Updated geregistreerdDoor during transfer',
+ [
+ 'organisationUuid' => $organisationUuid,
+ 'orgType' => $orgType,
+ 'geregistreerdDoor' => self::TYPE_MAP[$orgType],
+ ]
+ );
}
} catch (Exception $e) {
- $this->logger->warning('Failed to update geregistreerdDoor during transfer', [
- 'organisationUuid' => $organisationUuid,
- 'error' => $e->getMessage(),
- ]);
- }
+ $this->logger->warning(
+ 'Failed to update geregistreerdDoor during transfer',
+ [
+ 'organisationUuid' => $organisationUuid,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
return $objectData;
- }
+ }//end updateGeregistreerdDoor()
/**
* Add query filters from options to the base query
@@ -1403,206 +1608,219 @@ private function updateGeregistreerdDoor(
* Supported filters: limit, offset, status, suite, etc.
*
* @param array $baseQuery The base query to extend
- * @param array $options Filter options to apply
+ * @param array $options Filter options to apply
+ *
* @return array Extended query with additional filters
*/
private function addQueryFilters(array $baseQuery, array $options): array
{
- // Add limit if specified (handle both 'limit' and '_limit')
+ // Add limit if specified (handle both 'limit' and '_limit').
$limit = $options['_limit'] ?? $options['limit'] ?? null;
- if ($limit !== null && is_numeric($limit)) {
- $baseQuery['_limit'] = (int)$limit;
+ if ($limit !== null && is_numeric(value: $limit) === true) {
+ $baseQuery['_limit'] = (int) $limit;
}
-
- // Add offset if specified (handle both 'offset' and '_offset')
+
+ // Add offset if specified (handle both 'offset' and '_offset').
$offset = $options['_offset'] ?? $options['offset'] ?? null;
- if ($offset !== null && is_numeric($offset)) {
- $baseQuery['_offset'] = (int)$offset;
+ if ($offset !== null && is_numeric(value: $offset) === true) {
+ $baseQuery['_offset'] = (int) $offset;
}
-
- // Add page if specified (alternative to offset)
- if (isset($options['_page']) && is_numeric($options['_page'])) {
- $baseQuery['_page'] = (int)$options['_page'];
+
+ // Add page if specified (alternative to offset).
+ if (isset($options['_page']) === true && is_numeric(value: $options['_page']) === true) {
+ $baseQuery['_page'] = (int) $options['_page'];
}
-
- // Add source parameter if specified (for forcing database access)
- if (isset($options['_source']) && !empty($options['_source'])) {
+
+ // Add source parameter if specified (for forcing database access).
+ if (isset($options['_source']) === true && empty($options['_source']) === false) {
$baseQuery['_source'] = $options['_source'];
}
-
- // Add status filter if specified
- if (isset($options['status']) && !empty($options['status'])) {
+
+ // Add status filter if specified.
+ if (isset($options['status']) === true && empty($options['status']) === false) {
$baseQuery['status'] = $options['status'];
}
-
- // Add suite filter if specified
- if (isset($options['suite']) && !empty($options['suite'])) {
+
+ // Add suite filter if specified.
+ if (isset($options['suite']) === true && empty($options['suite']) === false) {
$baseQuery['suite'] = $options['suite'];
}
-
- // Add date filters if specified
- if (isset($options['startDate']) && !empty($options['startDate'])) {
+
+ // Add date filters if specified.
+ if (isset($options['startDate']) === true && empty($options['startDate']) === false) {
$baseQuery['startDate'] = $options['startDate'];
}
-
- if (isset($options['endDate']) && !empty($options['endDate'])) {
+
+ if (isset($options['endDate']) === true && empty($options['endDate']) === false) {
$baseQuery['endDate'] = $options['endDate'];
}
-
+
return $baseQuery;
- }
+ }//end addQueryFilters()
/**
* Delete (deny) a gebruik or koppeling object with security validation
- *
+ *
* This method allows deleting a gebruik or koppeling object, but only if the active
* organization is the afnemer (consumer) or aanbieder (provider) for that object.
* This implements the "deny" workflow where a gemeente can reject a suggestion from
* a leverancier, or a leverancier can reject/delete their own koppelingen.
- *
+ *
* Security: Since we disable RBAC to access cross-organisation objects, we must
* implement our own security checks to ensure only the afnemer or aanbieder can delete.
- *
+ *
* @param string $gebruikId The UUID of the gebruik or koppeling object to delete
- * @param array $options Additional options for the operation
+ * @param array $options Additional options for the operation
+ *
* @return array Result array with success status and details
*/
- public function deleteGebruikAsAfnemer(string $gebruikId, array $options = []): array
+ public function deleteGebruikAsAfnemer(string $gebruikId, array $options=[]): array
{
- $this->logger->info('Deleting gebruik object as afnemer or aanbieder', [
- 'gebruik_id' => $gebruikId,
- 'options' => $options
- ]);
+ $this->logger->info(
+ 'Deleting gebruik object as afnemer or aanbieder',
+ [
+ 'gebruik_id' => $gebruikId,
+ 'options' => $options,
+ ]
+ );
try {
- // Validate input
- if (empty($gebruikId)) {
+ // Validate input.
+ if (empty($gebruikId) === true) {
return [
'success' => false,
- 'error' => 'Gebruik ID is required',
- 'deleted' => false
+ 'error' => 'Gebruik ID is required',
+ 'deleted' => false,
];
}
- // Get ObjectService from OpenRegister
+ // Get ObjectService from OpenRegister.
$objectService = $this->getObjectService();
-
- // Get current organization
+
+ // Get current organization.
$currentOrg = $this->getCurrentOrganisation();
- if (!$currentOrg) {
+ if ($currentOrg === null) {
return [
'success' => false,
- 'error' => 'No current organization available',
- 'deleted' => false
+ 'error' => 'No current organization available',
+ 'deleted' => false,
];
}
- // Find the gebruik or koppeling object across possible schemas
- // Register/schema context is required to search magic tables
- $existingGebruik = $this->findGebruikOrKoppeling($objectService, $gebruikId);
+ // Find the gebruik or koppeling object across possible schemas.
+ // Register/schema context is required to search magic tables.
+ $existingGebruik = $this->findGebruikOrKoppeling(objectService: $objectService, objectId: $gebruikId);
- if (!$existingGebruik) {
+ if ($existingGebruik === null) {
return [
'success' => false,
- 'error' => 'Gebruik object not found',
- 'deleted' => false
+ 'error' => 'Gebruik object not found',
+ 'deleted' => false,
];
}
$gebruikData = $existingGebruik->getObject();
- // SECURITY CHECK: Verify that the active organization is either the afnemer or aanbieder
- // This is critical since we're bypassing RBAC
- $afnemerInfo = $gebruikData['afnemer'] ?? null;
+ // SECURITY CHECK: Verify that the active organization is either the afnemer or aanbieder.
+ // This is critical since we're bypassing RBAC.
+ $afnemerInfo = $gebruikData['afnemer'] ?? null;
$aanbiederInfo = $gebruikData['aanbieder'] ?? null;
-
- // Check various ways the afnemer might be stored (UUID, object, or string)
+
+ // Check various ways the afnemer might be stored (UUID, object, or string).
$afnemerId = null;
- if (is_array($afnemerInfo) && isset($afnemerInfo['id'])) {
+ if (is_array(value: $afnemerInfo) === true && isset($afnemerInfo['id']) === true) {
$afnemerId = $afnemerInfo['id'];
- } elseif (is_string($afnemerInfo)) {
+ } else if (is_string(value: $afnemerInfo) === true) {
$afnemerId = $afnemerInfo;
}
-
- // Check various ways the aanbieder might be stored (UUID, object, or string)
+
+ // Check various ways the aanbieder might be stored (UUID, object, or string).
$aanbiederId = null;
- if (is_array($aanbiederInfo) && isset($aanbiederInfo['id'])) {
+ if (is_array(value: $aanbiederInfo) === true && isset($aanbiederInfo['id']) === true) {
$aanbiederId = $aanbiederInfo['id'];
- } elseif (is_string($aanbiederInfo)) {
+ } else if (is_string(value: $aanbiederInfo) === true) {
$aanbiederId = $aanbiederInfo;
}
-
- // Allow operation if current org is either afnemer or aanbieder
- $isAfnemer = ($afnemerId && $afnemerId === $currentOrg);
- $isAanbieder = ($aanbiederId && $aanbiederId === $currentOrg);
- if (!$isAfnemer && !$isAanbieder) {
- $this->logger->warning('Unauthorized delete attempt - user is not afnemer or aanbieder', [
- 'gebruik_id' => $gebruikId,
- 'current_org' => $currentOrg,
- 'afnemer_in_object' => $afnemerInfo,
- 'resolved_afnemer_id' => $afnemerId,
- 'aanbieder_in_object' => $aanbiederInfo,
- 'resolved_aanbieder_id' => $aanbiederId
- ]);
-
+ // Allow operation if current org is either afnemer or aanbieder.
+ $isAfnemer = ($afnemerId !== null && $afnemerId === $currentOrg);
+ $isAanbieder = ($aanbiederId !== null && $aanbiederId === $currentOrg);
+
+ if ($isAfnemer === false && $isAanbieder === false) {
+ $this->logger->warning(
+ 'Unauthorized delete attempt - user is not afnemer or aanbieder',
+ [
+ 'gebruik_id' => $gebruikId,
+ 'current_org' => $currentOrg,
+ 'afnemer_in_object' => $afnemerInfo,
+ 'resolved_afnemer_id' => $afnemerId,
+ 'aanbieder_in_object' => $aanbiederInfo,
+ 'resolved_aanbieder_id' => $aanbiederId,
+ ]
+ );
+
return [
'success' => false,
- 'error' => 'Operation not allowed: active organization is not the afnemer or aanbieder',
+ 'error' => 'Operation not allowed: active organization is not the afnemer or aanbieder',
'deleted' => false,
- 'debug' => [
- 'afnemer_in_object' => $afnemerInfo,
- 'resolved_afnemer_id' => $afnemerId,
- 'aanbieder_in_object' => $aanbiederInfo,
+ 'debug' => [
+ 'afnemer_in_object' => $afnemerInfo,
+ 'resolved_afnemer_id' => $afnemerId,
+ 'aanbieder_in_object' => $aanbiederInfo,
'resolved_aanbieder_id' => $aanbiederId,
- 'current_org' => $currentOrg
- ]
+ 'current_org' => $currentOrg,
+ ],
];
- }
+ }//end if
- // Delete the object with RBAC and multitenancy disabled
- // Use register/schema from the found entity for correct table routing
+ // Delete the object with RBAC and multitenancy disabled.
+ // Use register/schema from the found entity for correct table routing.
$objectService->setRegister($existingGebruik->getRegister());
$objectService->setSchema($existingGebruik->getSchema());
-
+
$deleteResult = $objectService->deleteObject(
uuid: $gebruikId,
- _rbac: false, // Disable RBAC to allow cross-organisation deletion
- _multitenancy: false // Disable multitenancy to allow deletion from different organisations
+ _rbac: false,
+ // Disable RBAC to allow cross-organisation deletion.
+ _multitenancy: false
+ // Disable multitenancy to allow deletion from different organisations.
);
- $this->logger->info('Successfully deleted gebruik object', [
- 'gebruik_id' => $gebruikId,
- 'organisation' => $currentOrg,
- 'is_afnemer' => $isAfnemer,
- 'is_aanbieder' => $isAanbieder,
- 'afnemer_id' => $afnemerId,
- 'aanbieder_id' => $aanbiederId,
- 'delete_result' => $deleteResult
- ]);
+ $this->logger->info(
+ 'Successfully deleted gebruik object',
+ [
+ 'gebruik_id' => $gebruikId,
+ 'organisation' => $currentOrg,
+ 'is_afnemer' => $isAfnemer,
+ 'is_aanbieder' => $isAanbieder,
+ 'afnemer_id' => $afnemerId,
+ 'aanbieder_id' => $aanbiederId,
+ 'delete_result' => $deleteResult,
+ ]
+ );
return [
- 'success' => true,
- 'message' => 'Gebruik object deleted successfully',
- 'deleted' => true,
- 'gebruik_id' => $gebruikId,
- 'organisation' => $currentOrg
+ 'success' => true,
+ 'message' => 'Gebruik object deleted successfully',
+ 'deleted' => true,
+ 'gebruik_id' => $gebruikId,
+ 'organisation' => $currentOrg,
];
-
} catch (Exception $e) {
- $this->logger->error('Failed to delete gebruik object', [
- 'gebruik_id' => $gebruikId,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'Failed to delete gebruik object',
+ [
+ 'gebruik_id' => $gebruikId,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return [
'success' => false,
- 'error' => 'Failed to delete gebruik: ' . $e->getMessage(),
- 'deleted' => false
+ 'error' => 'Failed to delete gebruik: '.$e->getMessage(),
+ 'deleted' => false,
];
- }
- }
-}
-
-
+ }//end try
+ }//end deleteGebruikAsAfnemer()
+}//end class
diff --git a/lib/Service/ArchiMateExportService.php b/lib/Service/ArchiMateExportService.php
index 9a51f2f6..0c36aacf 100644
--- a/lib/Service/ArchiMateExportService.php
+++ b/lib/Service/ArchiMateExportService.php
@@ -18,7 +18,7 @@ class ArchiMateExportService
public function __construct(
private readonly LoggerInterface $logger
) {
- }
+ }//end __construct()
/**
* Convert an associative array into a SimpleXMLElement tree.
@@ -33,42 +33,42 @@ public function __construct(
*/
public function arrayToXml(array $data, \SimpleXMLElement $xml): \SimpleXMLElement
{
- // First pass: attributes and text content
- $addedAttributes = []; // Track attributes to avoid duplicates
-
+ // First pass: attributes and text content.
+ $addedAttributes = [];
+ // Track attributes to avoid duplicates.
foreach ($data as $key => $value) {
if ($key === '_value' || $key === '_text') {
$xml[0] = (string) $value;
continue;
}
- if (is_string($key) && str_starts_with($key, '_') && $key !== '_attributes') {
- // Skip legacy _attributes bag, handle individual underscored keys as attributes
+ if (is_string($key) === true && str_starts_with($key, '_') === true && $key !== '_attributes') {
+ // Skip legacy _attributes bag, handle individual underscored keys as attributes.
$attrKey = substr($key, 1);
-
- // Skip malformed attribute keys that would create invalid XML (e.g., __propertyDefinitionRef -> :propertyDefinitionRef)
- if (str_starts_with($attrKey, '__') || $attrKey === '') {
+
+ // Skip malformed attribute keys that would create invalid XML (e.g., __propertyDefinitionRef -> :propertyDefinitionRef).
+ if (str_starts_with($attrKey, '__') === true || $attrKey === '') {
continue;
}
-
- // Fix double underscores to colons (e.g., xml__lang -> xml:lang)
+
+ // Fix double underscores to colons (e.g., xml__lang -> xml:lang).
$attrKey = str_replace('__', ':', $attrKey);
-
- // Skip if this attribute was already added
- if (in_array($attrKey, $addedAttributes)) {
+
+ // Skip if this attribute was already added.
+ if (in_array($attrKey, $addedAttributes) === true) {
continue;
}
-
- [$nsPrefix, $local] = $this->splitNamespacedKey($attrKey);
+
+ [$nsPrefix, $local] = $this->splitNamespacedKey(key: $attrKey);
if ($nsPrefix !== null) {
- // Namespaced attribute, ensure namespace is declared on element
- $nsUri = $this->getNamespaceUri($xml, $nsPrefix);
- if ($nsUri) {
- $xml->addAttribute($nsPrefix . ':' . $local, (string) $value, $nsUri);
- $addedAttributes[] = $nsPrefix . ':' . $local;
+ // Namespaced attribute, ensure namespace is declared on element.
+ $nsUri = $this->getNamespaceUri(xml: $xml, prefix: $nsPrefix);
+ if (empty($nsUri) === false) {
+ $xml->addAttribute($nsPrefix.':'.$local, (string) $value, $nsUri);
+ $addedAttributes[] = $nsPrefix.':'.$local;
} else {
- // Fallback to non-namespaced if namespace not found
+ // Fallback to non-namespaced if namespace not found.
$xml->addAttribute($attrKey, (string) $value);
$addedAttributes[] = $attrKey;
}
@@ -76,205 +76,219 @@ public function arrayToXml(array $data, \SimpleXMLElement $xml): \SimpleXMLEleme
$xml->addAttribute($local, (string) $value);
$addedAttributes[] = $local;
}
- }
- }
+ }//end if
+ }//end foreach
- // Handle legacy _attributes array with duplicate filtering
- if (isset($data['_attributes']) && is_array($data['_attributes'])) {
+ // Handle legacy _attributes array with duplicate filtering.
+ if (isset($data['_attributes']) === true && is_array($data['_attributes']) === true) {
foreach ($data['_attributes'] as $attrKey => $attrValue) {
- // Skip duplicate attributes with colon prefix or already added attributes
- if (str_starts_with($attrKey, ':') || in_array($attrKey, $addedAttributes)) {
+ // Skip duplicate attributes with colon prefix or already added attributes.
+ if (str_starts_with($attrKey, ':') === true || in_array($attrKey, $addedAttributes) === true) {
continue;
}
- // Fix double underscores to colons
+ // Fix double underscores to colons.
$cleanAttrKey = str_replace('__', ':', $attrKey);
- // Skip if cleaned version was already added
- if (in_array($cleanAttrKey, $addedAttributes)) {
+ // Skip if cleaned version was already added.
+ if (in_array($cleanAttrKey, $addedAttributes) === true) {
continue;
}
- // Handle namespaced attributes (e.g., xml:lang, xsi:type)
- [$nsPrefix, $local] = $this->splitNamespacedKey($cleanAttrKey);
+ // Handle namespaced attributes (e.g., xml:lang, xsi:type).
+ [$nsPrefix, $local] = $this->splitNamespacedKey(key: $cleanAttrKey);
if ($nsPrefix !== null) {
- $nsUri = $this->getNamespaceUri($xml, $nsPrefix);
- if ($nsUri) {
- $xml->addAttribute($nsPrefix . ':' . $local, (string) $attrValue, $nsUri);
- $addedAttributes[] = $nsPrefix . ':' . $local;
+ $nsUri = $this->getNamespaceUri(xml: $xml, prefix: $nsPrefix);
+ if (empty($nsUri) === false) {
+ $xml->addAttribute($nsPrefix.':'.$local, (string) $attrValue, $nsUri);
+ $addedAttributes[] = $nsPrefix.':'.$local;
continue;
}
}
$xml->addAttribute($cleanAttrKey, (string) $attrValue);
$addedAttributes[] = $cleanAttrKey;
- }
- }
+ }//end foreach
+ }//end if
- // Second pass: children
+ // Second pass: children.
foreach ($data as $key => $value) {
if ($key === '_value' || $key === '_text' || $key === '_attributes') {
continue;
}
- if (is_string($key) && str_starts_with($key, '_')) {
- // Already handled as attribute
+
+ if (is_string($key) === true && str_starts_with($key, '_') === true) {
+ // Already handled as attribute.
continue;
}
- // Skip colon-prefixed duplicate keys (artifacts from XML-to-JSON parsing)
- if (is_string($key) && str_starts_with($key, ':')) {
+ // Skip colon-prefixed duplicate keys (artifacts from XML-to-JSON parsing).
+ if (is_string($key) === true && str_starts_with($key, ':') === true) {
continue;
}
- // Skip numeric keys - they indicate array items that should be handled differently
- if (is_int($key)) {
+ // Skip numeric keys - they indicate array items that should be handled differently.
+ if (is_int($key) === true) {
continue;
}
- // Skip property-like fields that should be handled by specialized property methods
- // These fields often appear as direct data but should only be in structure
+ // Skip property-like fields that should be handled by specialized property methods.
+ // These fields often appear as direct data but should only be in structure.
$propertyLikeFields = [
- 'beschikbaarheid', 'integriteit', 'vertrouwelijkheid', 'gemmaType',
- 'objectId', 'bivScoreBbn', 'belangrijksteReden'
+ 'beschikbaarheid',
+ 'integriteit',
+ 'vertrouwelijkheid',
+ 'gemmaType',
+ 'objectId',
+ 'bivScoreBbn',
+ 'belangrijksteReden',
];
- if (in_array($key, $propertyLikeFields, true)) {
- continue; // Skip these - they should only appear in proper structure
+ if (in_array($key, $propertyLikeFields, true) === true) {
+ continue;
+ // Skip these - they should only appear in proper structure.
}
-
- // Special handling for elementProperties and other nested structures - filter out problematic fields
- if (($key === 'elementProperties' || $key === 'properties' || $key === 'viewNodes') && is_array($value)) {
- $value = $this->filterProblematicFields($value, $propertyLikeFields);
+
+ // Special handling for elementProperties and other nested structures - filter out problematic fields.
+ if (($key === 'elementProperties' || $key === 'properties' || $key === 'viewNodes') && is_array($value) === true) {
+ $value = $this->filterProblematicFields(data: $value, fieldsToRemove: $propertyLikeFields);
}
- // Ensure key is always a string for XML tag names
+ // Ensure key is always a string for XML tag names.
$tagName = (string) $key;
- if (is_array($value)) {
- // Handle list of children
- if ($this->isList($value)) {
+ if (is_array($value) === true) {
+ // Handle list of children.
+ if ($this->isList(arr: $value) === true) {
foreach ($value as $item) {
$child = $xml->addChild($tagName);
- if (is_array($item)) {
- $this->arrayToXml($item, $child);
+ if (is_array($item) === true) {
+ $this->arrayToXml(data: $item, xml: $child);
} else {
$child[0] = (string) $item;
}
}
} else {
$child = $xml->addChild($tagName);
- $this->arrayToXml($value, $child);
+ $this->arrayToXml(data: $value, xml: $child);
}
} else {
- // Scalar child node
- $child = $xml->addChild($tagName);
+ // Scalar child node.
+ $child = $xml->addChild($tagName);
$child[0] = (string) $value;
}
- }
+ }//end foreach
return $xml;
- }
+ }//end arrayToXml()
private function isList(array $arr): bool
{
return $arr === [] || array_keys($arr) === range(0, count($arr) - 1);
- }
+ }//end isList()
private function splitNamespacedKey(string $key): array
{
- // Convert `xsi__type` to ['xsi', 'type']
- if (str_contains($key, '__')) {
+ // Convert `xsi__type` to ['xsi', 'type'].
+ if (str_contains($key, '__') === true) {
$parts = explode('__', $key, 2);
if (count($parts) === 2 && $parts[0] !== '' && $parts[1] !== '') {
return [$parts[0], $parts[1]];
}
}
- // Also handle already-converted colon notation (e.g., 'xml:lang')
- if (str_contains($key, ':')) {
+
+ // Also handle already-converted colon notation (e.g., 'xml:lang').
+ if (str_contains($key, ':') === true) {
$parts = explode(':', $key, 2);
if (count($parts) === 2 && $parts[0] !== '' && $parts[1] !== '') {
return [$parts[0], $parts[1]];
}
}
+
return [null, $key];
- }
+ }//end splitNamespacedKey()
/**
* Recursively filter out problematic fields from nested data structures
- *
- * @param array $data The data structure to filter
- * @param array $fieldsToRemove List of field names to remove
+ *
+ * @param array $data The data structure to filter
+ * @param array $fieldsToRemove List of field names to remove
* @return array Filtered data structure
*/
private function filterProblematicFields(array $data, array $fieldsToRemove): array
{
$filtered = [];
-
+
foreach ($data as $key => $value) {
if (is_string($key) === false) {
continue;
}
$shouldSkip = false;
-
- // Skip exact matches
- if (in_array($key, $fieldsToRemove, true)) {
+
+ // Skip exact matches.
+ if (in_array($key, $fieldsToRemove, true) === true) {
$shouldSkip = true;
}
-
- // Skip fields that start with problematic patterns (e.g., "beschikbaarheid(belangrijksteReden)")
+
+ // Skip fields that start with problematic patterns (e.g., "beschikbaarheid(belangrijksteReden)").
foreach ($fieldsToRemove as $fieldPattern) {
- if (str_starts_with($key, $fieldPattern)) {
+ if (str_starts_with($key, $fieldPattern) === true) {
$shouldSkip = true;
break;
}
}
-
- // Skip fields with invalid XML tag name characters (parentheses, etc.)
- if (preg_match('/[(),<>\/\\\]/', $key)) {
+
+ // Skip fields with invalid XML tag name characters (parentheses, etc.).
+ if (preg_match('/[(),<>\/\\\]/', $key) === 1) {
$shouldSkip = true;
}
-
- if ($shouldSkip) {
+
+ if (empty($shouldSkip) === false) {
continue;
}
-
- // Recursively filter nested arrays
- if (is_array($value)) {
- $filtered[$key] = $this->filterProblematicFields($value, $fieldsToRemove);
+
+ // Recursively filter nested arrays.
+ if (is_array($value) === true) {
+ $filtered[$key] = $this->filterProblematicFields(data: $value, fieldsToRemove: $fieldsToRemove);
} else {
$filtered[$key] = $value;
}
- }
-
+ }//end foreach
+
return $filtered;
- }
+ }//end filterProblematicFields()
private function getNamespaceUri(\SimpleXMLElement $xml, string $prefix): string
{
- // Well-known namespaces — check first to avoid expensive getDocNamespaces calls
+ // Well-known namespaces — check first to avoid expensive getDocNamespaces calls.
static $wellKnown = [
'xml' => 'http://www.w3.org/XML/1998/namespace',
'xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
];
- if (isset($wellKnown[$prefix])) {
+ if (isset($wellKnown[$prefix]) === true) {
return $wellKnown[$prefix];
}
- $namespaces = $xml->getDocNamespaces(true) ?: [];
+ if ($xml->getDocNamespaces(true)) {
+ $namespaces = $xml->getDocNamespaces(true);
+ } else {
+ $namespaces = [];
+ }
+
return $namespaces[$prefix] ?? '';
- }
+ }//end getNamespaceUri()
/**
* Create a clean ArchiMate XML structure with proper namespaces
*
- * @param array $modelMetadata Model metadata from the database
+ * @param array $modelMetadata Model metadata from the database
* @return \SimpleXMLElement Root XML element ready for population
*/
public function createCleanArchiMateXml(array $modelMetadata): \SimpleXMLElement
{
$modelName = $modelMetadata['name'] ?? 'ArchiMate Model';
- $modelId = $modelMetadata['identifier'] ?? 'model-' . uniqid();
+ $modelId = $modelMetadata['identifier'] ?? 'model-'.uniqid();
$xmlString = <<
@@ -287,33 +301,33 @@ public function createCleanArchiMateXml(array $modelMetadata): \SimpleXMLElement
XML;
$xml = simplexml_load_string($xmlString);
- if (!$xml) {
+ if ($xml === null) {
throw new \RuntimeException('Failed to create base ArchiMate XML structure');
}
return $xml;
- }
+ }//end createCleanArchiMateXml()
/**
* Generic method to add any collection of objects to XML
*
- * @param \SimpleXMLElement $xml Root XML element
- * @param array $objects Array of objects from database
- * @param string $folderName Name for the folder
- * @param string $folderId ID for the folder
- * @param string $folderType Type attribute for the folder
- * @param string $childTagName Tag name for child elements (default: 'element')
+ * @param \SimpleXMLElement $xml Root XML element
+ * @param array $objects Array of objects from database
+ * @param string $folderName Name for the folder
+ * @param string $folderId ID for the folder
+ * @param string $folderType Type attribute for the folder
+ * @param string $childTagName Tag name for child elements (default: 'element')
* @return void
*/
public function addObjectsToXml(
- \SimpleXMLElement $xml,
- array $objects,
- string $folderName,
- string $folderId,
- string $folderType,
- string $childTagName = 'element'
+ \SimpleXMLElement $xml,
+ array $objects,
+ string $folderName,
+ string $folderId,
+ string $folderType,
+ string $childTagName='element'
): void {
- if (empty($objects)) {
+ if (empty($objects) === true) {
return;
}
@@ -323,37 +337,40 @@ public function addObjectsToXml(
$folder->addAttribute('type', $folderType);
foreach ($objects as $object) {
- $this->addObjectToFolder($folder, $object, $childTagName);
+ $this->addObjectToFolder(folder: $folder, object: $object, childTagName: $childTagName);
}
- }
+ }//end addObjectsToXml()
/**
* Convenience method for elements
*/
public function addElementsToXml(\SimpleXMLElement $xml, array $elements): void
{
- $this->addObjectsToXml($xml, $elements, 'Application', 'folder-elements', 'application', 'element');
- }
+ $this->addObjectsToXml(xml: $xml, objects: $elements, folderName: 'Application', folderId: 'folder-elements', folderType: 'application', childTagName: 'element');
+ }//end addElementsToXml()
/**
* Convenience method for relationships
*/
public function addRelationshipsToXml(\SimpleXMLElement $xml, array $relationships): void
{
- $this->addObjectsToXml($xml, $relationships, 'Relations', 'folder-relations', 'relations', 'element');
- }
+ $this->addObjectsToXml(xml: $xml, objects: $relationships, folderName: 'Relations', folderId: 'folder-relations', folderType: 'relations', childTagName: 'element');
+ }//end addRelationshipsToXml()
/**
* Specialized method for views with custom node handling
*/
public function addViewsToXml(\SimpleXMLElement $xml, array $views): void
{
- $this->logger->debug('Adding views to XML', [
- 'view_count' => count($views),
- 'view_keys' => array_keys($views)
- ]);
-
- if (empty($views)) {
+ $this->logger->debug(
+ 'Adding views to XML',
+ [
+ 'view_count' => count($views),
+ 'view_keys' => array_keys($views),
+ ]
+ );
+
+ if (empty($views) === true) {
$this->logger->warning('No views to process');
return;
}
@@ -364,17 +381,17 @@ public function addViewsToXml(\SimpleXMLElement $xml, array $views): void
$folder->addAttribute('type', 'diagrams');
foreach ($views as $view) {
- $this->addViewToFolder($folder, $view);
+ $this->addViewToFolder(folder: $folder, view: $view);
}
- }
+ }//end addViewsToXml()
/**
* Convenience method for organizations
*/
public function addOrganizationsToXml(\SimpleXMLElement $xml, array $organizations): void
{
- $this->addObjectsToXml($xml, $organizations, 'Organizations', 'folder-organizations', 'business', 'item');
- }
+ $this->addObjectsToXml(xml: $xml, objects: $organizations, folderName: 'Organizations', folderId: 'folder-organizations', folderType: 'business', childTagName: 'item');
+ }//end addOrganizationsToXml()
/**
* Specialized method to add a view to the views folder with custom node handling
@@ -383,111 +400,160 @@ private function addViewToFolder(\SimpleXMLElement $folder, array $view): void
{
$viewNode = $folder->addChild('view');
- // Extract view data from different formats
- $viewData = $this->extractViewData($view);
-
- if (!$viewData) {
- $this->logger->warning('No valid view data found', [
- 'view_keys' => array_keys($view),
- 'view_structure' => $view
- ]);
+ // Extract view data from different formats.
+ $viewData = $this->extractViewData(view: $view);
+
+ if ($viewData === null) {
+ $this->logger->warning(
+ 'No valid view data found',
+ [
+ 'view_keys' => array_keys($view),
+ 'view_structure' => $view,
+ ]
+ );
return;
}
- // DEBUG: Check if this is our target view with nodes
- if (isset($viewData['_identifier']) && $viewData['_identifier'] === 'id-1c197dc3-71e5-40dc-8f5d-a96e983b41af') {
- $this->logger->debug('Found target view with specific ID', [
- 'identifier' => $viewData['_identifier'],
- 'raw_view' => $view,
- 'extracted_view_data' => $viewData,
- 'node_analysis' => [
- 'has_node' => isset($viewData['node']),
- 'node_count' => is_array($viewData['node'] ?? null) ? count($viewData['node']) : 0,
- 'node_sample' => isset($viewData['node'][0]) ? $viewData['node'][0] : 'NO FIRST NODE'
- ]
- ]);
+ // DEBUG: Check if this is our target view with nodes.
+ if (isset($viewData['_identifier']) === true && $viewData['_identifier'] === 'id-1c197dc3-71e5-40dc-8f5d-a96e983b41af') {
+ if (is_array($viewData['node'] ?? null) === true) {
+ $node_countValue = count($viewData['node']);
+ } else {
+ $node_countValue = 0;
+ }
+
+ if (isset($viewData['node'][0]) === true) {
+ $node_sampleValue = $viewData['node'][0];
+ } else {
+ $node_sampleValue = 'NO FIRST NODE';
+ }
+
+ $this->logger->debug(
+ 'Found target view with specific ID',
+ [
+ 'identifier' => $viewData['_identifier'],
+ 'raw_view' => $view,
+ 'extracted_view_data' => $viewData,
+ 'node_analysis' => [
+ 'has_node' => isset($viewData['node']) === true,
+ 'node_count' => $node_countValue,
+ 'node_sample' => $node_sampleValue,
+ ],
+ ]
+ );
+ }//end if
+
+ if (is_array($viewData['node'] ?? null) === true) {
+ $node_countValue = count($viewData['node']);
+ } else {
+ $node_countValue = 0;
}
- $this->logger->debug('Processing view with custom logic', [
- 'has_node' => isset($viewData['node']),
- 'node_count' => is_array($viewData['node'] ?? null) ? count($viewData['node']) : 0,
- 'has_connection' => isset($viewData['connection']),
- 'connection_count' => is_array($viewData['connection'] ?? null) ? count($viewData['connection']) : 0
- ]);
+ if (is_array($viewData['connection'] ?? null) === true) {
+ $connection_countValue = count($viewData['connection']);
+ } else {
+ $connection_countValue = 0;
+ }
+
+ $this->logger->debug(
+ 'Processing view with custom logic',
+ [
+ 'has_node' => isset($viewData['node']) === true,
+ 'node_count' => $node_countValue,
+ 'has_connection' => isset($viewData['connection']) === true,
+ 'connection_count' => $connection_countValue,
+ ]
+ );
- // Process view attributes and basic properties
- $this->addViewBasicData($viewNode, $viewData);
+ // Process view attributes and basic properties.
+ $this->addViewBasicData(viewNode: $viewNode, viewData: $viewData);
- // Process nodes with special handling
- if (isset($viewData['node']) && is_array($viewData['node'])) {
- $this->addViewNodes($viewNode, $viewData['node']);
+ // Process nodes with special handling.
+ if (isset($viewData['node']) === true && is_array($viewData['node']) === true) {
+ $this->addViewNodes(viewNode: $viewNode, nodes: $viewData['node']);
}
- // Process connections with special handling
- if (isset($viewData['connection']) && is_array($viewData['connection'])) {
- $this->addViewConnections($viewNode, $viewData['connection']);
+ // Process connections with special handling.
+ if (isset($viewData['connection']) === true && is_array($viewData['connection']) === true) {
+ $this->addViewConnections(viewNode: $viewNode, connections: $viewData['connection']);
}
- }
+ }//end addViewToFolder()
/**
* Extract view data from different possible formats
*/
private function extractViewData(array $view): ?array
{
- // Format 1: OpenRegister object format with properties.xml_data
- if (isset($view['properties']['xml_data'])) {
- $xmlData = is_string($view['properties']['xml_data']) ?
- json_decode($view['properties']['xml_data'], true) :
- $view['properties']['xml_data'];
- return is_array($xmlData) ? $xmlData : null;
- }
-
- // Format 2: Object with xml_data field (from database)
- if (isset($view['xml_data'])) {
- $xmlData = is_string($view['xml_data']) ?
- json_decode($view['xml_data'], true) :
- $view['xml_data'];
- return is_array($xmlData) ? $xmlData : null;
- }
-
- // Format 3: Direct XML data (from convertFromOpenRegisterObjects)
+ // Format 1: OpenRegister object format with properties.xml_data.
+ if (isset($view['properties']['xml_data']) === true) {
+ if (is_string($view['properties']['xml_data']) === true) {
+ $xmlData = json_decode($view['properties']['xml_data'], true);
+ } else {
+ $xmlData = $view['properties']['xml_data'];
+ }
+
+ if (is_array($xmlData) === true) {
+ return $xmlData;
+ } else {
+ return null;
+ }
+ }
+
+ // Format 2: Object with xml_data field (from database).
+ if (isset($view['xml_data']) === true) {
+ if (is_string($view['xml_data']) === true) {
+ $xmlData = json_decode($view['xml_data'], true);
+ } else {
+ $xmlData = $view['xml_data'];
+ }
+
+ if (is_array($xmlData) === true) {
+ return $xmlData;
+ } else {
+ return null;
+ }
+ }
+
+ // Format 3: Direct XML data (from convertFromOpenRegisterObjects).
return $view;
- }
+ }//end extractViewData()
/**
* Add basic view data (attributes, name, documentation, properties) to view node
*/
private function addViewBasicData(\SimpleXMLElement $viewNode, array $viewData): void
{
- // Add attributes
- if (isset($viewData['_attributes'])) {
+ // Add attributes.
+ if (isset($viewData['_attributes']) === true) {
foreach ($viewData['_attributes'] as $attrKey => $attrValue) {
- if (str_starts_with($attrKey, ':')) {
- continue; // Skip duplicate attributes with colon prefix
+ if (str_starts_with($attrKey, ':') === true) {
+ continue;
+ // Skip duplicate attributes with colon prefix.
}
- $viewNode->addAttribute($attrKey, (string)$attrValue);
+
+ $viewNode->addAttribute($attrKey, (string) $attrValue);
}
}
- // Add identifier directly if present
- if (isset($viewData['_identifier']) || isset($viewData['identifier'])) {
+ // Add identifier directly if present.
+ if (isset($viewData['_identifier']) === true || isset($viewData['identifier']) === true) {
$identifier = $viewData['_identifier'] ?? $viewData['identifier'];
- $viewNode->addAttribute('identifier', (string)$identifier);
+ $viewNode->addAttribute('identifier', (string) $identifier);
}
- // Add xsi:type if present
+ // Add xsi:type if present.
foreach (['_xsi__type', 'xsi:type', '_xsi:type'] as $typeKey) {
- if (isset($viewData[$typeKey])) {
- $viewNode->addAttribute('xsi:type', (string)$viewData[$typeKey]);
+ if (isset($viewData[$typeKey]) === true) {
+ $viewNode->addAttribute('xsi:type', (string) $viewData[$typeKey]);
break;
}
}
- // Add name, documentation, and properties using the generic arrayToXml method
- // but exclude node and connection arrays to handle them separately
+ // Add name, documentation, and properties using the generic arrayToXml method.
+ // but exclude node and connection arrays to handle them separately.
$basicData = array_diff_key($viewData, ['node' => true, 'connection' => true]);
- $this->arrayToXml($basicData, $viewNode);
- }
+ $this->arrayToXml(data: $basicData, xml: $viewNode);
+ }//end addViewBasicData()
/**
* Add view nodes with proper nested structure handling
@@ -496,9 +562,9 @@ private function addViewNodes(\SimpleXMLElement $viewNode, array $nodes): void
{
foreach ($nodes as $nodeData) {
$node = $viewNode->addChild('node');
- $this->arrayToXml($nodeData, $node);
+ $this->arrayToXml(data: $nodeData, xml: $node);
}
- }
+ }//end addViewNodes()
/**
* Add view connections with proper nested structure handling
@@ -507,46 +573,49 @@ private function addViewConnections(\SimpleXMLElement $viewNode, array $connecti
{
foreach ($connections as $connectionData) {
$connection = $viewNode->addChild('connection');
- $this->arrayToXml($connectionData, $connection);
+ $this->arrayToXml(data: $connectionData, xml: $connection);
}
- }
+ }//end addViewConnections()
/**
* Generic method to add any object to a folder - determines everything from the JSON data
*/
- private function addObjectToFolder(\SimpleXMLElement $folder, array $object, string $childTagName = 'element'): void
+ private function addObjectToFolder(\SimpleXMLElement $folder, array $object, string $childTagName='element'): void
{
$objectNode = $folder->addChild($childTagName);
-
- // Handle different data formats:
- // 1. OpenRegister object format with properties.xml_data
- // 2. Direct XML data from convertFromOpenRegisterObjects
- // 3. Raw object data as fallback
-
- if (isset($object['properties']['xml_data'])) {
- // Format 1: OpenRegister object format
- $xmlData = is_string($object['properties']['xml_data']) ?
- json_decode($object['properties']['xml_data'], true) :
- $object['properties']['xml_data'];
-
- if (is_array($xmlData)) {
- $this->arrayToXml($xmlData, $objectNode);
- }
- } elseif (isset($object['xml_data'])) {
- // Format 2: Object with xml_data field (from database)
- $xmlData = is_string($object['xml_data']) ?
- json_decode($object['xml_data'], true) :
- $object['xml_data'];
-
- if (is_array($xmlData)) {
- $this->arrayToXml($xmlData, $objectNode);
+
+ // Handle different data formats:.
+ // 1. OpenRegister object format with properties.xml_data.
+ // 2. Direct XML data from convertFromOpenRegisterObjects.
+ // 3. Raw object data as fallback.
+ if (isset($object['properties']['xml_data']) === true) {
+ // Format 1: OpenRegister object format.
+ if (is_string($object['properties']['xml_data']) === true) {
+ $xmlData = json_decode($object['properties']['xml_data'], true);
+ } else {
+ $xmlData = $object['properties']['xml_data'];
+ }
+
+ if (is_array($xmlData) === true) {
+ $this->arrayToXml(data: $xmlData, xml: $objectNode);
+ }
+ } else if (isset($object['xml_data']) === true) {
+ // Format 2: Object with xml_data field (from database).
+ if (is_string($object['xml_data']) === true) {
+ $xmlData = json_decode($object['xml_data'], true);
+ } else {
+ $xmlData = $object['xml_data'];
+ }
+
+ if (is_array($xmlData) === true) {
+ $this->arrayToXml(data: $xmlData, xml: $objectNode);
}
} else {
- // Format 3: Direct XML data (from convertFromOpenRegisterObjects)
- // This handles the case where $archiMateData['views'][$identifier] = $xmlData
- $this->arrayToXml($object, $objectNode);
- }
- }
+ // Format 3: Direct XML data (from convertFromOpenRegisterObjects).
+ // This handles the case where $archiMateData['views'][$identifier] = $xmlData.
+ $this->arrayToXml(data: $object, xml: $objectNode);
+ }//end if
+ }//end addObjectToFolder()
/**
* Get all objects from the AMEF register
@@ -555,321 +624,358 @@ private function addObjectToFolder(\SimpleXMLElement $folder, array $object, str
* requires both register AND schema in the query. Without schema, the query
* falls back to the generic objects table (which is empty for magic-table registers).
*
- * @param \OCA\OpenRegister\Service\ObjectService $objectService OpenRegister ObjectService
- * @param int $registerId AMEF register ID
- * @param array $schemaIdMap Mapping of schema IDs to schema types
+ * @param \OCA\OpenRegister\Service\ObjectService $objectService OpenRegister ObjectService
+ * @param int $registerId AMEF register ID
+ * @param array $schemaIdMap Mapping of schema IDs to schema types
* @return array Array of objects from all schemas in the register
* @throws \RuntimeException If retrieval fails
*/
- public function getObjectsFromDatabase(\OCA\OpenRegister\Service\ObjectService $objectService, int $registerId, array $schemaIdMap = []): array
+ public function getObjectsFromDatabase(\OCA\OpenRegister\Service\ObjectService $objectService, int $registerId, array $schemaIdMap=[]): array
{
- $this->logger->info('Retrieving all objects from AMEF register', [
- 'register_id' => $registerId,
- 'schema_count' => count($schemaIdMap)
- ]);
+ $this->logger->info(
+ 'Retrieving all objects from AMEF register',
+ [
+ 'register_id' => $registerId,
+ 'schema_count' => count($schemaIdMap),
+ ]
+ );
$allObjects = [];
- // Map schema types (singular) to section names used in XML generation
+ // Map schema types (singular) to section names used in XML generation.
$sectionNameMap = [
- 'element' => 'element',
- 'relationship' => 'relationship',
- 'view' => 'view',
- 'organization' => 'organization',
+ 'element' => 'element',
+ 'relationship' => 'relationship',
+ 'view' => 'view',
+ 'organization' => 'organization',
'property_definition' => 'property_definition',
- 'model' => 'model'
+ 'model' => 'model',
];
- // Query each schema separately (required for magic table routing)
+ // Query each schema separately (required for magic table routing).
foreach ($schemaIdMap as $schemaId => $schemaType) {
$query = [
- '@self' => [
+ '@self' => [
'register' => $registerId,
- 'schema' => (int) $schemaId
+ 'schema' => (int) $schemaId,
],
- '_limit' => 10000
+ '_limit' => 10000,
];
try {
$schemaObjects = $objectService->searchObjects(query: $query, _rbac: false, _multitenancy: false);
- // Inject 'section' field if missing (magic table objects don't store it)
+ // Inject 'section' field if missing (magic table objects don't store it).
$sectionName = $sectionNameMap[$schemaType] ?? $schemaType;
foreach ($schemaObjects as &$obj) {
- if (is_object($obj) && method_exists($obj, 'jsonSerialize')) {
+ if (is_object($obj) === true && method_exists($obj, 'jsonSerialize') === true) {
$obj = $obj->jsonSerialize();
}
- if (!isset($obj['section'])) {
+
+ if (isset($obj['section']) === false) {
$obj['section'] = $sectionName;
}
}
+
unset($obj);
- $this->logger->info("Objects retrieved for schema", [
- 'schema_id' => $schemaId,
- 'schema_type' => $schemaType,
- 'count' => count($schemaObjects)
- ]);
+ $this->logger->info(
+ "Objects retrieved for schema",
+ [
+ 'schema_id' => $schemaId,
+ 'schema_type' => $schemaType,
+ 'count' => count($schemaObjects),
+ ]
+ );
$allObjects = array_merge($allObjects, $schemaObjects);
} catch (\Exception $e) {
- $this->logger->warning("Failed to retrieve objects for schema", [
- 'schema_id' => $schemaId,
- 'schema_type' => $schemaType,
- 'error' => $e->getMessage()
- ]);
- }
- }
+ $this->logger->warning(
+ "Failed to retrieve objects for schema",
+ [
+ 'schema_id' => $schemaId,
+ 'schema_type' => $schemaType,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end foreach
- // Fallback: if no schemaIdMap provided, try querying register directly
- if (empty($schemaIdMap)) {
+ // Fallback: if no schemaIdMap provided, try querying register directly.
+ if (empty($schemaIdMap) === true) {
$query = [
- '@self' => ['register' => $registerId],
- '_limit' => 10000
+ '@self' => ['register' => $registerId],
+ '_limit' => 10000,
];
try {
$allObjects = $objectService->searchObjects(query: $query, _rbac: false, _multitenancy: false);
} catch (\Exception $e) {
- $this->logger->error("Failed to retrieve objects from AMEF register", [
- 'register_id' => $registerId,
- 'error' => $e->getMessage()
- ]);
- throw new \RuntimeException("Failed to retrieve objects from database: " . $e->getMessage());
+ $this->logger->error(
+ "Failed to retrieve objects from AMEF register",
+ [
+ 'register_id' => $registerId,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ throw new \RuntimeException("Failed to retrieve objects from database: ".$e->getMessage());
}
}
- $this->logger->info('Objects retrieved successfully from AMEF register', [
- 'total_retrieved_count' => count($allObjects),
- 'register_id' => $registerId
- ]);
+ $this->logger->info(
+ 'Objects retrieved successfully from AMEF register',
+ [
+ 'total_retrieved_count' => count($allObjects),
+ 'register_id' => $registerId,
+ ]
+ );
return $allObjects;
- }
-
-
+ }//end getObjectsFromDatabase()
/**
* Add property definitions to XML
*/
public function addPropertyDefinitionsToXml(\SimpleXMLElement $xml, array $propertyDefinitions): void
{
- $this->addObjectsToXml($xml, $propertyDefinitions, 'Property Definitions', 'folder-property-definitions', 'other', 'propertyDefinition');
- }
+ $this->addObjectsToXml(xml: $xml, objects: $propertyDefinitions, folderName: 'Property Definitions', folderId: 'folder-property-definitions', folderType: 'other', childTagName: 'propertyDefinition');
+ }//end addPropertyDefinitionsToXml()
/**
* Complete export process: get all objects from database and render XML in one go
- *
+ *
* OPTIMIZED VERSION: This method processes 8000+ objects efficiently by:
* 1. Single database query to get all objects
* 2. Single pass through objects using section property directly
* 3. Direct XML generation without intermediate arrays
* 4. No JSON serialization overhead
- *
- * @param \OCA\OpenRegister\Service\ObjectService $objectService OpenRegister ObjectService
- * @param int $registerId AMEF register ID
- * @param array $schemaIdMap Mapping of schema IDs to schema types (unused, kept for compatibility)
- * @param string|null $organization Organization filter (optional)
+ *
+ * @param \OCA\OpenRegister\Service\ObjectService $objectService OpenRegister ObjectService
+ * @param int $registerId AMEF register ID
+ * @param array $schemaIdMap Mapping of schema IDs to schema types (unused, kept for compatibility)
+ * @param string|null $organization Organization filter (optional)
* @return string Generated XML
*/
public function exportArchiMateXml(
- \OCA\OpenRegister\Service\ObjectService $objectService,
- int $registerId,
- array $schemaIdMap,
- ?string $organization = null
+ \OCA\OpenRegister\Service\ObjectService $objectService,
+ int $registerId,
+ array $schemaIdMap,
+ ?string $organization=null
): string {
$startTime = microtime(true);
- $this->logger->info('Starting OPTIMIZED ArchiMate XML export process (using section property)', [
- 'register_id' => $registerId,
- 'organization_filter' => $organization
- ]);
-
- // Step 1: Get all objects from database (queries each schema separately for magic table support)
- $objects = $this->getObjectsFromDatabase($objectService, $registerId, $schemaIdMap);
- $dbTime = microtime(true) - $startTime;
-
- // Step 2: Process and generate XML in single optimized pass (no schema mapping needed)
- $xml = $this->generateXmlDirectly($objects, []);
-
- // Step 3: Run Quality Assurance checks on generated XML
- $this->runQualityAssuranceChecks($xml, $objects);
-
+ $this->logger->info(
+ 'Starting OPTIMIZED ArchiMate XML export process (using section property)',
+ [
+ 'register_id' => $registerId,
+ 'organization_filter' => $organization,
+ ]
+ );
+
+ // Step 1: Get all objects from database (queries each schema separately for magic table support).
+ $objects = $this->getObjectsFromDatabase(objectService: $objectService, registerId: $registerId, schemaIdMap: $schemaIdMap);
+ $dbTime = microtime(true) - $startTime;
+
+ // Step 2: Process and generate XML in single optimized pass (no schema mapping needed).
+ $xml = $this->generateXmlDirectly(objects: $objects, schemaIdMap: []);
+
+ // Step 3: Run Quality Assurance checks on generated XML.
+ $this->runQualityAssuranceChecks(xmlString: $xml, sourceData: $objects);
+
$totalTime = microtime(true) - $startTime;
-
- $this->logger->info('OPTIMIZED ArchiMate XML export completed', [
- 'total_objects' => count($objects),
- 'xml_length' => strlen($xml),
- 'db_time_seconds' => round($dbTime, 3),
- 'total_time_seconds' => round($totalTime, 3),
- 'objects_per_second' => round(count($objects) / $totalTime, 0)
- ]);
+
+ $this->logger->info(
+ 'OPTIMIZED ArchiMate XML export completed',
+ [
+ 'total_objects' => count($objects),
+ 'xml_length' => strlen($xml),
+ 'db_time_seconds' => round($dbTime, 3),
+ 'total_time_seconds' => round($totalTime, 3),
+ 'objects_per_second' => round(count($objects) / $totalTime, 0),
+ ]
+ );
return $xml;
- }
+ }//end exportArchiMateXml()
/**
* Generate XML directly from objects using section-based organization
- *
- * ULTRA-OPTIMIZED VERSION:
+ *
+ * ULTRA-OPTIMIZED VERSION:
* - Single pass to organize objects by section
* - Direct XML generation per section
* - No unnecessary loops or checks
- *
- * @param array $objects Raw objects from database
- * @param array $schemaIdMap Schema ID to type mapping (unused)
+ *
+ * @param array $objects Raw objects from database
+ * @param array $schemaIdMap Schema ID to type mapping (unused)
* @return string Generated XML
*/
private function generateXmlDirectly(array $objects, array $schemaIdMap): string
{
- $this->logger->info('Starting section-based XML generation from objects', [
- 'object_count' => count($objects)
- ]);
+ $this->logger->info(
+ 'Starting section-based XML generation from objects',
+ [
+ 'object_count' => count($objects),
+ ]
+ );
- // Create base XML structure with model metadata
- $modelMetadata = $this->extractModelMetadata($objects);
+ // Create base XML structure with model metadata.
+ $modelMetadata = $this->extractModelMetadata(objects: $objects);
$propertyDefinitionMap = $modelMetadata['propertyDefinitionMap'] ?? [];
- $xml = $this->createCleanArchiMateXml($modelMetadata);
-
- // Add model name and properties if available
- if (!empty($modelMetadata)) {
- $this->addModelMetadataToXml($xml, $modelMetadata);
- }
-
- // Step 1: Organize objects by section in single pass
+ $xml = $this->createCleanArchiMateXml(modelMetadata: $modelMetadata);
+
+ // Add model name and properties if available.
+ if (empty($modelMetadata) === false) {
+ $this->addModelMetadataToXml(xml: $xml, modelMetadata: $modelMetadata);
+ }
+
+ // Step 1: Organize objects by section in single pass.
$objectsBySection = [];
- $unmatchedCount = 0;
-
+ $unmatchedCount = 0;
+
foreach ($objects as $object) {
- // Serialize object if needed
- if (is_object($object) && method_exists($object, 'jsonSerialize')) {
+ // Serialize object if needed.
+ if (is_object($object) === true && method_exists($object, 'jsonSerialize') === true) {
$object = $object->jsonSerialize();
}
$sectionName = $object['section'] ?? null;
-
- if ($sectionName) {
- if (!isset($objectsBySection[$sectionName])) {
+
+ if (empty($sectionName) === false) {
+ if (isset($objectsBySection[$sectionName]) === false) {
$objectsBySection[$sectionName] = [];
}
+
$objectsBySection[$sectionName][] = $object;
} else {
$unmatchedCount++;
}
}
-
- // Step 2: Generate XML sections directly
+
+ // Step 2: Generate XML sections directly.
$validSections = ['elements', 'relationships', 'organizations', 'property_definitions', 'views'];
$sectionCounts = [];
-
- // Map singular section names to plural for XML generation
+
+ // Map singular section names to plural for XML generation.
$sectionMapping = [
- 'element' => 'elements',
- 'relationship' => 'relationships',
- 'view' => 'views',
- 'organization' => 'organizations',
- 'property_definition' => 'property_definitions'
+ 'element' => 'elements',
+ 'relationship' => 'relationships',
+ 'view' => 'views',
+ 'organization' => 'organizations',
+ 'property_definition' => 'property_definitions',
];
-
+
foreach ($validSections as $sectionName) {
- // Check both singular and plural section names
+ // Check both singular and plural section names.
$sectionObjects = [];
foreach ($objectsBySection as $dbSection => $objects) {
- if (isset($sectionMapping[$dbSection]) && $sectionMapping[$dbSection] === $sectionName) {
+ if (isset($sectionMapping[$dbSection]) === true && $sectionMapping[$dbSection] === $sectionName) {
$sectionObjects = array_merge($sectionObjects, $objects);
}
}
-
- if (!empty($sectionObjects)) {
+
+ if (empty($sectionObjects) === false) {
$sectionCounts[$sectionName] = count($sectionObjects);
- // Organizations are stored as a single tree object with the full hierarchy
+ // Organizations are stored as a single tree object with the full hierarchy.
// in the xml field. Write items directly as children of .
if ($sectionName === 'organizations') {
- $orgFolder = $this->createSectionFolder($xml, $sectionName);
+ $orgFolder = $this->createSectionFolder(xml: $xml, sectionName: $sectionName);
foreach ($sectionObjects as $object) {
- if (is_object($object) && method_exists($object, 'jsonSerialize')) {
+ if (is_object($object) === true && method_exists($object, 'jsonSerialize') === true) {
$object = $object->jsonSerialize();
}
+
$xmlField = $object['xml'] ?? [];
- // The xml field contains the raw organizations data with 'item' array
- if (isset($xmlField['item'])) {
+ // The xml field contains the raw organizations data with 'item' array.
+ if (isset($xmlField['item']) === true) {
$items = $xmlField['item'];
- // Ensure items is a list (could be single assoc array for one top-level folder)
- if (!isset($items[0])) {
+ // Ensure items is a list (could be single assoc array for one top-level folder).
+ if (isset($items[0]) === false) {
$items = [$items];
}
+
foreach ($items as $itemData) {
- if (is_array($itemData)) {
+ if (is_array($itemData) === true) {
$itemNode = $orgFolder->addChild('item');
- $this->addOrganizationItemToXml($itemNode, $itemData);
+ $this->addOrganizationItemToXml(itemNode: $itemNode, itemData: $itemData);
}
}
}
- }
- $this->logger->debug("Generated XML section: {$sectionName} (tree mode)", [
- 'object_count' => count($sectionObjects)
- ]);
+ }//end foreach
+
+ $this->logger->debug(
+ "Generated XML section: {$sectionName} (tree mode)",
+ [
+ 'object_count' => count($sectionObjects),
+ ]
+ );
continue;
- }
+ }//end if
- // Create section folder
- $sectionFolder = $this->createSectionFolder($xml, $sectionName);
+ // Create section folder.
+ $sectionFolder = $this->createSectionFolder(xml: $xml, sectionName: $sectionName);
- // Add all objects in this section
+ // Add all objects in this section.
foreach ($sectionObjects as $object) {
- $this->addObjectDirectlyToXmlWithProperties($sectionFolder, $object, $sectionName, $propertyDefinitionMap);
+ $this->addObjectDirectlyToXmlWithProperties(folder: $sectionFolder, object: $object, sectionName: $sectionName, propertyDefinitionMap: $propertyDefinitionMap);
}
- $this->logger->debug("Generated XML section: {$sectionName}", [
- 'object_count' => count($sectionObjects)
- ]);
- }
- }
-
- // Debug logging
- $this->logger->info('Section-based XML generation completed', [
- 'sections_found' => array_keys($objectsBySection),
- 'section_counts' => $sectionCounts,
- 'unmatched_objects' => $unmatchedCount,
- 'total_objects_processed' => count($objects),
- 'sections_with_data' => array_keys($sectionCounts)
- ]);
+ $this->logger->debug(
+ "Generated XML section: {$sectionName}",
+ [
+ 'object_count' => count($sectionObjects),
+ ]
+ );
+ }//end if
+ }//end foreach
+
+ // Debug logging.
+ $this->logger->info(
+ 'Section-based XML generation completed',
+ [
+ 'sections_found' => array_keys($objectsBySection),
+ 'section_counts' => $sectionCounts,
+ 'unmatched_objects' => $unmatchedCount,
+ 'total_objects_processed' => count($objects),
+ 'sections_with_data' => array_keys($sectionCounts),
+ ]
+ );
- return $this->formatXmlOutput($xml->asXML());
- }
+ return $this->formatXmlOutput(xmlString: $xml->asXML());
+ }//end generateXmlDirectly()
/**
* Create section element in XML (matching original ArchiMate structure)
*/
- private function createSectionFolder(\SimpleXMLElement $xml, string $sectionName): \SimpleXMLElement
+ private function createSectionFolder(\SimpleXMLElement $xml, string $sectionName): ?\SimpleXMLElement
{
- // Map our section names to proper ArchiMate XML elements
+ // Map our section names to proper ArchiMate XML elements.
$sectionMapping = [
- 'elements' => 'elements',
- 'relationships' => 'relationships',
- 'views' => 'views',
- 'organizations' => 'organizations',
- 'property_definitions' => 'propertyDefinitions'
+ 'elements' => 'elements',
+ 'relationships' => 'relationships',
+ 'views' => 'views',
+ 'organizations' => 'organizations',
+ 'property_definitions' => 'propertyDefinitions',
];
$xmlElementName = $sectionMapping[$sectionName] ?? $sectionName;
$sectionElement = $xml->addChild($xmlElementName);
-
- // Views need a wrapper element according to ArchiMate standard
+
+ // Views need a wrapper element according to ArchiMate standard.
if ($sectionName === 'views') {
return $sectionElement->addChild('diagrams');
}
-
+
return $sectionElement;
- }
+ }//end createSectionFolder()
/**
* Add object directly to XML with properties from root fields
*/
private function addObjectDirectlyToXmlWithProperties(\SimpleXMLElement $folder, array $object, string $sectionName, array $propertyDefinitionMap): void
{
- $tagName = match($sectionName) {
+ $tagName = match ($sectionName) {
'organizations' => 'item',
'property_definitions' => 'propertyDefinition',
'views' => 'view',
@@ -877,380 +983,401 @@ private function addObjectDirectlyToXmlWithProperties(\SimpleXMLElement $folder,
'elements' => 'element',
default => 'element'
};
+
$objectNode = $folder->addChild($tagName);
- // Prefer the 'xml' field (original ArchiMate structure preserved during import)
- // over cleanObjectDataForXml which loses array structure for name/documentation
- if (isset($object['xml']) && is_array($object['xml']) && !empty($object['xml'])) {
+ // Prefer the 'xml' field (original ArchiMate structure preserved during import).
+ // over cleanObjectDataForXml which loses array structure for name/documentation.
+ if (isset($object['xml']) === true && is_array($object['xml']) === true && empty($object['xml']) === false) {
$xmlData = $object['xml'];
unset($xmlData['_essential_data']);
} else {
- $xmlData = $this->cleanObjectDataForXml($object, $propertyDefinitionMap);
+ $xmlData = $this->cleanObjectDataForXml(object: $object, propertyDefinitionMap: $propertyDefinitionMap);
}
- if (is_array($xmlData) && !empty($xmlData)) {
+
+ if (is_array($xmlData) === true && empty($xmlData) === false) {
if ($sectionName === 'views') {
- $this->addViewDataToXmlNode($objectNode, $xmlData);
+ $this->addViewDataToXmlNode(viewNode: $objectNode, viewData: $xmlData);
} else {
- $this->addCleanDataToXmlNode($objectNode, $xmlData, $sectionName, $propertyDefinitionMap);
+ $this->addCleanDataToXmlNode(node: $objectNode, data: $xmlData, sectionName: $sectionName, propertyDefinitionMap: $propertyDefinitionMap);
}
}
- }
+ }//end addObjectDirectlyToXmlWithProperties()
/**
* Add view data to XML node with specialized handling for nodes and connections
*/
private function addViewDataToXmlNode(\SimpleXMLElement $viewNode, array $viewData): void
{
- // Add attributes first
- if (isset($viewData['_attributes'])) {
+ // Add attributes first.
+ if (isset($viewData['_attributes']) === true) {
foreach ($viewData['_attributes'] as $attrKey => $attrValue) {
- if (str_starts_with($attrKey, ':')) {
- continue; // Skip duplicate attributes with colon prefix
+ if (str_starts_with($attrKey, ':') === true) {
+ continue;
+ // Skip duplicate attributes with colon prefix.
}
- // Handle namespaced attributes (e.g., xsi:type)
- [$nsPrefix, $local] = $this->splitNamespacedKey($attrKey);
+
+ // Handle namespaced attributes (e.g., xsi:type).
+ [$nsPrefix, $local] = $this->splitNamespacedKey(key: $attrKey);
if ($nsPrefix !== null) {
- $nsUri = $this->getNamespaceUri($viewNode, $nsPrefix);
- if ($nsUri) {
- $viewNode->addAttribute($nsPrefix . ':' . $local, (string)$attrValue, $nsUri);
+ $nsUri = $this->getNamespaceUri(xml: $viewNode, prefix: $nsPrefix);
+ if (empty($nsUri) === false) {
+ $viewNode->addAttribute($nsPrefix.':'.$local, (string) $attrValue, $nsUri);
continue;
}
}
- $viewNode->addAttribute($attrKey, (string)$attrValue);
+
+ $viewNode->addAttribute($attrKey, (string) $attrValue);
}
}
- // Add identifier directly if present
- if (isset($viewData['_identifier']) || isset($viewData['identifier'])) {
+ // Add identifier directly if present.
+ if (isset($viewData['_identifier']) === true || isset($viewData['identifier']) === true) {
$identifier = $viewData['_identifier'] ?? $viewData['identifier'];
- $viewNode->addAttribute('identifier', (string)$identifier);
+ $viewNode->addAttribute('identifier', (string) $identifier);
}
- // Add xsi:type if present
+ // Add xsi:type if present.
foreach (['_xsi__type', 'xsi:type', '_xsi:type'] as $typeKey) {
- if (isset($viewData[$typeKey])) {
- $viewNode->addAttribute('xsi:type', (string)$viewData[$typeKey], 'http://www.w3.org/2001/XMLSchema-instance');
+ if (isset($viewData[$typeKey]) === true) {
+ $viewNode->addAttribute('xsi:type', (string) $viewData[$typeKey], 'http://www.w3.org/2001/XMLSchema-instance');
break;
}
}
- // XSD-required order for ViewType (Diagram): name → documentation → properties → node → connection
- $this->addLangTextChild($viewNode, 'name', $viewData['name'] ?? null);
- $this->addLangTextChild($viewNode, 'documentation', $viewData['documentation'] ?? null);
- if (isset($viewData['properties']) && is_array($viewData['properties'])) {
- $this->addPropertiesToXml($viewNode, $viewData['properties']);
+ // XSD-required order for ViewType (Diagram): name → documentation → properties → node → connection.
+ $this->addLangTextChild(parent: $viewNode, tagName: 'name', data: $viewData['name'] ?? null);
+ $this->addLangTextChild(parent: $viewNode, tagName: 'documentation', data: $viewData['documentation'] ?? null);
+ if (isset($viewData['properties']) === true && is_array($viewData['properties']) === true) {
+ $this->addPropertiesToXml(node: $viewNode, properties: $viewData['properties']);
}
- // Nodes
- if (isset($viewData['node']) && is_array($viewData['node'])) {
+ // Nodes.
+ if (isset($viewData['node']) === true && is_array($viewData['node']) === true) {
$nodes = $viewData['node'];
- if (!$this->isList($nodes)) {
+ if ($this->isLis === falset(arr: $nodes)) {
$nodes = [$nodes];
}
+
foreach ($nodes as $nodeData) {
- if (is_array($nodeData)) {
+ if (is_array($nodeData) === true) {
$nodeElement = $viewNode->addChild('node');
- $this->addNodeDataToXmlElement($nodeElement, $nodeData);
+ $this->addNodeDataToXmlElement(nodeElement: $nodeElement, nodeData: $nodeData);
}
}
}
- // Connections
- if (isset($viewData['connection']) && is_array($viewData['connection'])) {
+ // Connections.
+ if (isset($viewData['connection']) === true && is_array($viewData['connection']) === true) {
$connections = $viewData['connection'];
- if (!$this->isList($connections)) {
+ if ($this->isLis === falset(arr: $connections)) {
$connections = [$connections];
}
+
foreach ($connections as $connectionData) {
- if (is_array($connectionData)) {
+ if (is_array($connectionData) === true) {
$connectionElement = $viewNode->addChild('connection');
- $this->arrayToXml($connectionData, $connectionElement);
+ $this->arrayToXml(data: $connectionData, xml: $connectionElement);
}
}
}
- }
+ }//end addViewDataToXmlNode()
/**
* Add node data to XML element with specialized handling for node attributes and nested elements
*/
private function addNodeDataToXmlElement(\SimpleXMLElement $nodeElement, array $nodeData): void
{
- // Add node attributes first - these are the positioning and identification attributes
+ // Add node attributes first - these are the positioning and identification attributes.
$nodeAttributes = [
'_identifier' => 'identifier',
- '_x' => 'x',
- '_y' => 'y',
- '_w' => 'w',
- '_h' => 'h',
+ '_x' => 'x',
+ '_y' => 'y',
+ '_w' => 'w',
+ '_h' => 'h',
'_elementRef' => 'elementRef',
- '_xsi__type' => 'xsi:type'
+ '_xsi__type' => 'xsi:type',
];
-
+
$addedNodeAttrs = [];
foreach ($nodeAttributes as $dataKey => $xmlAttr) {
- if (isset($nodeData[$dataKey])) {
- // Handle namespaced attributes like xsi:type
- [$nsPrefix, $local] = $this->splitNamespacedKey($xmlAttr);
+ if (isset($nodeData[$dataKey]) === true) {
+ // Handle namespaced attributes like xsi:type.
+ [$nsPrefix, $local] = $this->splitNamespacedKey(key: $xmlAttr);
if ($nsPrefix !== null) {
- $nsUri = $this->getNamespaceUri($nodeElement, $nsPrefix);
- if ($nsUri) {
- $nodeElement->addAttribute($nsPrefix . ':' . $local, (string)$nodeData[$dataKey], $nsUri);
- $addedNodeAttrs[] = $nsPrefix . ':' . $local;
+ $nsUri = $this->getNamespaceUri(xml: $nodeElement, prefix: $nsPrefix);
+ if (empty($nsUri) === false) {
+ $nodeElement->addAttribute($nsPrefix.':'.$local, (string) $nodeData[$dataKey], $nsUri);
+ $addedNodeAttrs[] = $nsPrefix.':'.$local;
continue;
}
}
- $nodeElement->addAttribute($xmlAttr, (string)$nodeData[$dataKey]);
+
+ $nodeElement->addAttribute($xmlAttr, (string) $nodeData[$dataKey]);
$addedNodeAttrs[] = $xmlAttr;
}
}
- // Also check regular attributes array
- if (isset($nodeData['_attributes'])) {
+ // Also check regular attributes array.
+ if (isset($nodeData['_attributes']) === true) {
foreach ($nodeData['_attributes'] as $attrKey => $attrValue) {
- if (str_starts_with($attrKey, ':')) {
- continue; // Skip duplicate attributes with colon prefix
+ if (str_starts_with($attrKey, ':') === true) {
+ continue;
+ // Skip duplicate attributes with colon prefix.
}
- // Skip if we already added this attribute from the direct keys
- if (in_array($attrKey, ['identifier', 'x', 'y', 'w', 'h', 'elementRef', 'xsi:type']) || in_array($attrKey, $addedNodeAttrs)) {
+
+ // Skip if we already added this attribute from the direct keys.
+ if (in_array($attrKey, ['identifier', 'x', 'y', 'w', 'h', 'elementRef', 'xsi:type']) === true || in_array($attrKey, $addedNodeAttrs) === true) {
continue;
}
- // Handle namespaced attributes
- [$nsPrefix, $local] = $this->splitNamespacedKey($attrKey);
+
+ // Handle namespaced attributes.
+ [$nsPrefix, $local] = $this->splitNamespacedKey(key: $attrKey);
if ($nsPrefix !== null) {
- $nsUri = $this->getNamespaceUri($nodeElement, $nsPrefix);
- if ($nsUri) {
- $nodeElement->addAttribute($nsPrefix . ':' . $local, (string)$attrValue, $nsUri);
+ $nsUri = $this->getNamespaceUri(xml: $nodeElement, prefix: $nsPrefix);
+ if (empty($nsUri) === false) {
+ $nodeElement->addAttribute($nsPrefix.':'.$local, (string) $attrValue, $nsUri);
continue;
}
}
- $nodeElement->addAttribute($attrKey, (string)$attrValue);
- }
- }
-
- // XSD-required order for ViewNodeType: label → style → viewRef → node (nested)
- // Label (used in Label nodes)
- if (isset($nodeData['label'])) {
+
+ $nodeElement->addAttribute($attrKey, (string) $attrValue);
+ }//end foreach
+ }//end if
+
+ // XSD-required order for ViewNodeType: label → style → viewRef → node (nested).
+ // Label (used in Label nodes).
+ if (isset($nodeData['label']) === true) {
$labelData = $nodeData['label'];
- if (is_array($labelData)) {
+ if (is_array($labelData) === true) {
$labelElement = $nodeElement->addChild('label');
- $this->arrayToXml($labelData, $labelElement);
+ $this->arrayToXml(data: $labelData, xml: $labelElement);
} else {
- $labelElement = $nodeElement->addChild('label');
- $labelElement[0] = (string)$labelData;
+ $labelElement = $nodeElement->addChild('label');
+ $labelElement[0] = (string) $labelData;
}
}
- // Style (lineColor → fillColor → font per XSD StyleType order)
- if (isset($nodeData['style']) && is_array($nodeData['style'])) {
+ // Style (lineColor → fillColor → font per XSD StyleType order).
+ if (isset($nodeData['style']) === true && is_array($nodeData['style']) === true) {
$styleElement = $nodeElement->addChild('style');
- $styleData = $nodeData['style'];
- // Enforce StyleType order: lineColor → fillColor → font
+ $styleData = $nodeData['style'];
+ // Enforce StyleType order: lineColor → fillColor → font.
foreach (['lineColor', 'fillColor', 'font'] as $styleKey) {
- if (isset($styleData[$styleKey]) && is_array($styleData[$styleKey])) {
+ if (isset($styleData[$styleKey]) === true && is_array($styleData[$styleKey]) === true) {
$child = $styleElement->addChild($styleKey);
- $this->arrayToXml($styleData[$styleKey], $child);
+ $this->arrayToXml(data: $styleData[$styleKey], xml: $child);
}
}
}
- // viewRef
- if (isset($nodeData['viewRef'])) {
+ // viewRef.
+ if (isset($nodeData['viewRef']) === true) {
$viewRefData = $nodeData['viewRef'];
- if (is_array($viewRefData)) {
+ if (is_array($viewRefData) === true) {
$vrElement = $nodeElement->addChild('viewRef');
- $this->arrayToXml($viewRefData, $vrElement);
+ $this->arrayToXml(data: $viewRefData, xml: $vrElement);
}
}
- // Nested nodes (Container/Element type)
- if (isset($nodeData['node']) && is_array($nodeData['node'])) {
+ // Nested nodes (Container/Element type).
+ if (isset($nodeData['node']) === true && is_array($nodeData['node']) === true) {
$nestedNodes = $nodeData['node'];
- if (!$this->isList($nestedNodes)) {
+ if ($this->isLis === falset(arr: $nestedNodes)) {
$nestedNodes = [$nestedNodes];
}
+
foreach ($nestedNodes as $nestedNodeData) {
- if (is_array($nestedNodeData)) {
+ if (is_array($nestedNodeData) === true) {
$nestedNodeElement = $nodeElement->addChild('node');
- $this->addNodeDataToXmlElement($nestedNodeElement, $nestedNodeData);
+ $this->addNodeDataToXmlElement(nodeElement: $nestedNodeElement, nodeData: $nestedNodeData);
}
}
}
- }
+ }//end addNodeDataToXmlElement()
/**
* Add organization item to XML with XSD-required child order: label → documentation → item
*/
private function addOrganizationItemToXml(\SimpleXMLElement $itemNode, array $itemData): void
{
- // Add identifierRef attribute if present
- if (isset($itemData['_identifierRef'])) {
- $itemNode->addAttribute('identifierRef', (string)$itemData['_identifierRef']);
- } elseif (isset($itemData['_attributes']['identifierRef'])) {
- $itemNode->addAttribute('identifierRef', (string)$itemData['_attributes']['identifierRef']);
+ // Add identifierRef attribute if present.
+ if (isset($itemData['_identifierRef']) === true) {
+ $itemNode->addAttribute('identifierRef', (string) $itemData['_identifierRef']);
+ } else if (isset($itemData['_attributes']['identifierRef']) === true) {
+ $itemNode->addAttribute('identifierRef', (string) $itemData['_attributes']['identifierRef']);
}
- // XSD order: label → documentation → item
- // Labels first
- if (isset($itemData['label'])) {
+ // XSD order: label → documentation → item.
+ // Labels first.
+ if (isset($itemData['label']) === true) {
$labels = $itemData['label'];
- if (is_array($labels) && !$this->isList($labels)) {
- $labels = [$labels]; // Single label → list
+ if (is_array($labels) === true && $this->isLis === falset(arr: $labels)) {
+ $labels = [$labels];
+ // Single label → list.
}
- if (is_array($labels)) {
+
+ if (is_array($labels) === true) {
foreach ($labels as $labelData) {
- if (is_array($labelData)) {
- $labelElement = $itemNode->addChild('label');
- $this->arrayToXml($labelData, $labelElement);
- } elseif (is_string($labelData)) {
+ if (is_array($labelData) === true) {
$labelElement = $itemNode->addChild('label');
+ $this->arrayToXml(data: $labelData, xml: $labelElement);
+ } else if (is_string($labelData) === true) {
+ $labelElement = $itemNode->addChild('label');
$labelElement[0] = $labelData;
}
}
- } elseif (is_string($labels)) {
- $labelElement = $itemNode->addChild('label');
+ } else if (is_string($labels) === true) {
+ $labelElement = $itemNode->addChild('label');
$labelElement[0] = $labels;
}
- }
+ }//end if
- // Documentation
- $this->addLangTextChild($itemNode, 'documentation', $itemData['documentation'] ?? null);
+ // Documentation.
+ $this->addLangTextChild(parent: $itemNode, tagName: 'documentation', data: $itemData['documentation'] ?? null);
- // Nested items
- if (isset($itemData['item'])) {
+ // Nested items.
+ if (isset($itemData['item']) === true) {
$items = $itemData['item'];
- if (is_array($items) && !$this->isList($items)) {
+ if (is_array($items) === true && $this->isLis === falset(arr: $items)) {
$items = [$items];
}
- if (is_array($items)) {
+
+ if (is_array($items) === true) {
foreach ($items as $childItemData) {
- if (is_array($childItemData)) {
+ if (is_array($childItemData) === true) {
$childNode = $itemNode->addChild('item');
- $this->addOrganizationItemToXml($childNode, $childItemData);
+ $this->addOrganizationItemToXml(itemNode: $childNode, itemData: $childItemData);
}
}
}
}
- }
+ }//end addOrganizationItemToXml()
/**
* Format XML output with proper indentation and line breaks for readability
*/
private function formatXmlOutput(string $xmlString): string
{
- // Use DOMDocument to format the XML with proper indentation
+ // Use DOMDocument to format the XML with proper indentation.
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->preserveWhiteSpace = false;
- $dom->formatOutput = true;
-
- // Load the XML string
- if ($dom->loadXML($xmlString)) {
+ $dom->formatOutput = true;
+
+ // Load the XML string.
+ if ($dom->loadXML($xmlString) === true) {
return $dom->saveXML();
}
-
- // If formatting fails, return original string
+
+ // If formatting fails, return original string.
return $xmlString;
- }
+ }//end formatXmlOutput()
/**
* Clean object data for XML export - remove metadata and duplicate attributes
*/
- private function cleanObjectDataForXml(array $object, array $propertyDefinitionMap = []): array
+ private function cleanObjectDataForXml(array $object, array $propertyDefinitionMap=[]): array
{
- // Remove our metadata fields
- $cleanData = $object;
+ // Remove our metadata fields.
+ $cleanData = $object;
$metadataFields = ['section', 'identifier', 'model_identifier', '@self', 'extracted_at'];
foreach ($metadataFields as $field) {
unset($cleanData[$field]);
}
-
- // Remove duplicate underscore fields that were created during parsing
- // Keep only the clean attribute names
+
+ // Remove duplicate underscore fields that were created during parsing.
+ // Keep only the clean attribute names.
$fieldsToRemove = [];
foreach ($cleanData as $key => $value) {
- // Remove fields that start with multiple underscores (___identifier, etc)
- if (is_string($key) && preg_match('/^_{2,}/', $key)) {
+ // Remove fields that start with multiple underscores (___identifier, etc).
+ if (is_string($key) === true && preg_match('/^_{2,}/', $key) === true) {
$fieldsToRemove[] = $key;
}
- // Remove single underscore fields that have clean equivalents
- elseif (is_string($key) && str_starts_with($key, '_') && $key !== '_attributes' && $key !== '_value' && $key !== '_text' && $key !== '_xsi__type') {
+ // Remove single underscore fields that have clean equivalents.
+ else if (is_string($key) === true && str_starts_with($key, '_') === true && $key !== '_attributes' && $key !== '_value' && $key !== '_text' && $key !== '_xsi__type') {
$cleanKey = substr($key, 1);
- if (isset($cleanData[$cleanKey])) {
+ if (isset($cleanData[$cleanKey]) === true) {
$fieldsToRemove[] = $key;
}
}
}
-
+
foreach ($fieldsToRemove as $field) {
unset($cleanData[$field]);
}
-
- // Remove flattened properties that will be reconstructed separately
- if (!empty($propertyDefinitionMap)) {
+
+ // Remove flattened properties that will be reconstructed separately.
+ if (empty($propertyDefinitionMap) === false) {
foreach ($propertyDefinitionMap as $propRef => $propName) {
unset($cleanData[$propName]);
}
}
-
+
return $cleanData;
- }
+ }//end cleanObjectDataForXml()
/**
* Add clean data to XML node with proper ArchiMate structure
*/
- private function addCleanDataToXmlNode(\SimpleXMLElement $node, array $data, ?string $sectionName = null, array $propertyDefinitionMap = []): void
+ private function addCleanDataToXmlNode(\SimpleXMLElement $node, array $data, ?string $sectionName=null, array $propertyDefinitionMap=[]): void
{
- // Extract attributes from various possible locations
+ // Extract attributes from various possible locations.
$attributes = [];
- if (isset($data['identifier'])) {
- $attributes['identifier'] = (string)$data['identifier'];
+ if (isset($data['identifier']) === true) {
+ $attributes['identifier'] = (string) $data['identifier'];
}
- if (isset($data['_attributes']) && is_array($data['_attributes'])) {
+
+ if (isset($data['_attributes']) === true && is_array($data['_attributes']) === true) {
foreach ($data['_attributes'] as $attrKey => $attrValue) {
- // Skip colon-prefixed duplicate keys
- if (str_starts_with($attrKey, ':')) {
+ // Skip colon-prefixed duplicate keys.
+ if (str_starts_with($attrKey, ':') === true) {
continue;
}
+
$isPropertyDefinition = ($sectionName === 'property_definitions');
if ($attrKey === 'xsi:type') {
- if ($isPropertyDefinition) {
- $attributes['type'] = (string)$attrValue;
+ if (empty($isPropertyDefinition) === false) {
+ $attributes['type'] = (string) $attrValue;
} else {
- $attributes['xsi:type'] = (string)$attrValue;
+ $attributes['xsi:type'] = (string) $attrValue;
}
- } elseif (in_array($attrKey, ['identifier', 'source', 'target', 'accessType', 'isDirected', 'type'])) {
- if ($attrKey === 'type' && !$isPropertyDefinition) {
- $attributes['xsi:type'] = (string)$attrValue;
+ } else if (in_array($attrKey, ['identifier', 'source', 'target', 'accessType', 'isDirected', 'type']) === true) {
+ if ($attrKey === 'type' && $isPropertyDefinition === false) {
+ $attributes['xsi:type'] = (string) $attrValue;
} else {
- $attributes[$attrKey] = (string)$attrValue;
+ $attributes[$attrKey] = (string) $attrValue;
}
}
- }
- }
+ }//end foreach
+ }//end if
+
foreach (['xsi:type', 'xsi_type', '_xsi:type', '_xsi__type', '_type'] as $typeKey) {
- if (isset($data[$typeKey])) {
+ if (isset($data[$typeKey]) === true) {
$isPropertyDefinition = ($sectionName === 'property_definitions');
- if ($typeKey === '_type' && $isPropertyDefinition && !isset($attributes['type'])) {
- $attributes['type'] = (string)$data[$typeKey];
+ if ($typeKey === '_type' && $isPropertyDefinition === true && isset($attributes['type']) === false) {
+ $attributes['type'] = (string) $data[$typeKey];
break;
- } elseif (in_array($typeKey, ['xsi:type', 'xsi_type', '_xsi:type', '_xsi__type']) && !isset($attributes['xsi:type'])) {
- $attributes['xsi:type'] = (string)$data[$typeKey];
+ } else if (in_array($typeKey, ['xsi:type', 'xsi_type', '_xsi:type', '_xsi__type']) === true && isset($attributes['xsi:type']) === false) {
+ $attributes['xsi:type'] = (string) $data[$typeKey];
break;
}
}
}
+
foreach (['source', 'target', 'accessType', 'isDirected', 'type'] as $attrName) {
- if (isset($data[$attrName]) && !isset($attributes[$attrName])) {
+ if (isset($data[$attrName]) === true && isset($attributes[$attrName]) === false) {
$isPropertyDefinition = ($sectionName === 'property_definitions');
if ($attrName === 'type') {
- if ($isPropertyDefinition) {
- $attributes['type'] = (string)$data[$attrName];
- } elseif (!isset($attributes['xsi:type'])) {
- $attributes['xsi:type'] = (string)$data[$attrName];
+ if (empty($isPropertyDefinition) === false) {
+ $attributes['type'] = (string) $data[$attrName];
+ } else if (isset($attributes['xsi:type']) === false) {
+ $attributes['xsi:type'] = (string) $data[$attrName];
}
} else {
- $attributes[$attrName] = (string)$data[$attrName];
+ $attributes[$attrName] = (string) $data[$attrName];
}
}
}
+
foreach ($attributes as $attrName => $attrValue) {
if ($attrName === 'xsi:type') {
$node->addAttribute('xsi:type', $attrValue, 'http://www.w3.org/2001/XMLSchema-instance');
@@ -1258,147 +1385,152 @@ private function addCleanDataToXmlNode(\SimpleXMLElement $node, array $data, ?st
$node->addAttribute($attrName, $attrValue);
}
}
- // Handle child elements in XSD-required order (xs:sequence):
- // NamedReferenceableType: name → documentation → properties
- $this->addLangTextChild($node, 'name', $data['name'] ?? null);
- $this->addLangTextChild($node, 'documentation', $data['documentation'] ?? null);
- if (isset($data['properties']) && is_array($data['properties'])) {
- $this->addPropertiesToXml($node, $data['properties']);
+
+ // Handle child elements in XSD-required order (xs:sequence):.
+ // NamedReferenceableType: name → documentation → properties.
+ $this->addLangTextChild(parent: $node, tagName: 'name', data: $data['name'] ?? null);
+ $this->addLangTextChild(parent: $node, tagName: 'documentation', data: $data['documentation'] ?? null);
+ if (isset($data['properties']) === true && is_array($data['properties']) === true) {
+ $this->addPropertiesToXml(node: $node, properties: $data['properties']);
}
- // Add properties from root fields using propertyDefinitionMap ONLY if no properties were already processed
- if (!empty($propertyDefinitionMap) && !isset($data['properties'])) {
- $this->addPropertiesFromRootFields($node, $data, $propertyDefinitionMap);
+
+ // Add properties from root fields using propertyDefinitionMap ONLY if no properties were already processed.
+ if (empty($propertyDefinitionMap) === false && isset($data['properties']) === false) {
+ $this->addPropertiesFromRootFields(node: $node, object: $data, propertyDefinitionMap: $propertyDefinitionMap);
}
- }
+ }//end addCleanDataToXmlNode()
/**
* Add properties to XML node using propertyDefinitionMap from model
*
- * @param \SimpleXMLElement $node XML node to add properties to
- * @param array $object The object with root-level properties
- * @param array $propertyDefinitionMap Map of property name => propertyDefinitionRef
+ * @param \SimpleXMLElement $node XML node to add properties to
+ * @param array $object The object with root-level properties
+ * @param array $propertyDefinitionMap Map of property name => propertyDefinitionRef
*/
private function addPropertiesFromRootFields(
\SimpleXMLElement $node,
array $object,
array $propertyDefinitionMap
): void {
- // Find all root-level fields that match a propertyDefinitionMap entry
+ // Find all root-level fields that match a propertyDefinitionMap entry.
$properties = [];
foreach ($propertyDefinitionMap as $propRef => $propName) {
- if (isset($object[$propName])) {
+ if (isset($object[$propName]) === true) {
$properties[] = [
'propertyDefinitionRef' => $propRef,
- 'value' => $object[$propName],
+ 'value' => $object[$propName],
];
}
}
- if (!empty($properties)) {
+
+ if (empty($properties) === false) {
$propertiesNode = $node->addChild('properties');
foreach ($properties as $property) {
$propertyNode = $propertiesNode->addChild('property');
$propertyNode->addAttribute('propertyDefinitionRef', $property['propertyDefinitionRef']);
- $valueNode = $propertyNode->addChild('value');
- $valueNode[0] = (string)$property['value'];
+ $valueNode = $propertyNode->addChild('value');
+ $valueNode[0] = (string) $property['value'];
}
}
- }
+ }//end addPropertiesFromRootFields()
/**
* Add properties section to XML
*/
private function addPropertiesToXml(\SimpleXMLElement $node, array $properties): void
{
- if (empty($properties)) {
+ if (empty($properties) === true) {
return;
}
$propertiesNode = $node->addChild('properties');
-
- // Handle the nested structure from import service: properties.property[]
+
+ // Handle the nested structure from import service: properties.property[].
$propList = [];
- if (isset($properties['property'])) {
- // Structure from import: properties.property (single object or array)
- if ($this->isList($properties['property'])) {
- // Multiple properties as array
+ if (isset($properties['property']) === true) {
+ // Structure from import: properties.property (single object or array).
+ if ($this->isList(arr: $properties['property']) === true) {
+ // Multiple properties as array.
$propList = $properties['property'];
} else {
- // Single property as object - wrap in array
+ // Single property as object - wrap in array.
$propList = [$properties['property']];
}
- } elseif ($this->isList($properties)) {
- // Direct array of properties
+ } else if ($this->isList(arr: $properties) === true) {
+ // Direct array of properties.
$propList = $properties;
} else {
- // Single property object
+ // Single property object.
$propList = [$properties];
}
-
+
foreach ($propList as $property) {
- if (!is_array($property)) {
+ if (is_array($property) === false) {
continue;
}
-
+
$propertyNode = $propertiesNode->addChild('property');
-
- // Look for propertyDefinitionRef in various forms (including double underscore from import service)
+
+ // Look for propertyDefinitionRef in various forms (including double underscore from import service).
$propDefRef = null;
foreach (['propertyDefinitionRef', '_propertyDefinitionRef', '___propertyDefinitionRef'] as $refKey) {
- if (isset($property[$refKey])) {
- $propDefRef = (string)$property[$refKey];
+ if (isset($property[$refKey]) === true) {
+ $propDefRef = (string) $property[$refKey];
break;
}
}
-
- // Also check in _attributes, but avoid duplicate if we already found one
- if (!$propDefRef && isset($property['_attributes']['propertyDefinitionRef'])) {
- $propDefRef = (string)$property['_attributes']['propertyDefinitionRef'];
+
+ // Also check in _attributes, but avoid duplicate if we already found one.
+ if ($propDefRef === null && isset($property['_attributes']['propertyDefinitionRef']) === true) {
+ $propDefRef = (string) $property['_attributes']['propertyDefinitionRef'];
}
-
- // Skip and clean up malformed attributes that would create invalid XML
- if (isset($property['_attributes'][':propertyDefinitionRef'])) {
+
+ // Skip and clean up malformed attributes that would create invalid XML.
+ if (isset($property['_attributes'][':propertyDefinitionRef']) === true) {
unset($property['_attributes'][':propertyDefinitionRef']);
}
- // Also check for other malformed attribute patterns
+
+ // Also check for other malformed attribute patterns.
$badAttrs = [];
foreach ($property['_attributes'] ?? [] as $attrName => $attrValue) {
- if (str_starts_with($attrName, ':')) {
+ if (str_starts_with($attrName, ':') === true) {
$badAttrs[] = $attrName;
}
}
+
foreach ($badAttrs as $badAttr) {
unset($property['_attributes'][$badAttr]);
}
-
- if ($propDefRef) {
+
+ if (empty($propDefRef) === false) {
$propertyNode->addAttribute('propertyDefinitionRef', $propDefRef);
}
-
- // Handle value in various forms
- if (isset($property['value'])) {
- if (is_array($property['value'])) {
+
+ // Handle value in various forms.
+ if (isset($property['value']) === true) {
+ if (is_array($property['value']) === true) {
$valueNode = $propertyNode->addChild('value');
- if (isset($property['value']['_value'])) {
- $valueNode[0] = (string)$property['value']['_value'];
- } elseif (isset($property['value']['value'])) {
- $valueNode[0] = (string)$property['value']['value'];
+ if (isset($property['value']['_value']) === true) {
+ $valueNode[0] = (string) $property['value']['_value'];
+ } else if (isset($property['value']['value']) === true) {
+ $valueNode[0] = (string) $property['value']['value'];
}
-
- // Add xml:lang if present in various forms (including double underscore from import service)
+
+ // Add xml:lang if present in various forms (including double underscore from import service).
foreach (['xml:lang', '_xml:lang', '_xml__lang', 'xml_lang'] as $langKey) {
- if (isset($property['value'][$langKey])) {
+ if (isset($property['value'][$langKey]) === true) {
$valueNode->addAttribute('xml:lang', $property['value'][$langKey], 'http://www.w3.org/XML/1998/namespace');
break;
}
}
} else {
- // Simple string value
- $valueNode = $propertyNode->addChild('value');
- $valueNode[0] = (string)$property['value'];
+ // Simple string value.
+ $valueNode = $propertyNode->addChild('value');
+ $valueNode[0] = (string) $property['value'];
}
- }
- }
- }
+ }//end if
+ }//end foreach
+ }//end addPropertiesToXml()
/**
* Add a child element with text content and optional xml:lang attribute
@@ -1408,22 +1540,24 @@ private function addLangTextChild(\SimpleXMLElement $parent, string $tagName, $d
if ($data === null) {
return;
}
- if (is_array($data)) {
+
+ if (is_array($data) === true) {
$childNode = $parent->addChild($tagName);
- if (isset($data['_value'])) {
- $childNode[0] = (string)$data['_value'];
+ if (isset($data['_value']) === true) {
+ $childNode[0] = (string) $data['_value'];
}
+
foreach (['xml:lang', '_xml:lang', '_xml__lang', 'xml_lang'] as $langKey) {
- if (isset($data[$langKey])) {
+ if (isset($data[$langKey]) === true) {
$childNode->addAttribute('xml:lang', $data[$langKey], 'http://www.w3.org/XML/1998/namespace');
break;
}
}
- } elseif (is_string($data) && $data !== '') {
- $childNode = $parent->addChild($tagName);
+ } else if (is_string($data) === true && $data !== '') {
+ $childNode = $parent->addChild($tagName);
$childNode[0] = $data;
}
- }
+ }//end addLangTextChild()
/**
* Extract model metadata from objects
@@ -1431,379 +1565,395 @@ private function addLangTextChild(\SimpleXMLElement $parent, string $tagName, $d
private function extractModelMetadata(array $objects): array
{
foreach ($objects as $object) {
- if (is_object($object) && method_exists($object, 'jsonSerialize')) {
+ if (is_object($object) === true && method_exists($object, 'jsonSerialize') === true) {
$object = $object->jsonSerialize();
}
-
- if (isset($object['section']) && $object['section'] === 'model') {
- return $object;
+
+ if (isset($object['section']) === true && $object['section'] === 'model') {
+ return (array) $object;
}
}
-
+
return [];
- }
+ }//end extractModelMetadata()
/**
* Add model metadata (name, documentation, properties) to XML root
*/
private function addModelMetadataToXml(\SimpleXMLElement $xml, array $modelMetadata): void
{
- // Prefer xml field data (preserves full array structure with xml:lang from import)
+ // Prefer xml field data (preserves full array structure with xml:lang from import).
$xmlField = $modelMetadata['xml'] ?? [];
- // Resolve name: prefer xml field (array with _value/_xml__lang), fall back to flat field
+ // Resolve name: prefer xml field (array with _value/_xml__lang), fall back to flat field.
$nameData = $xmlField['name'] ?? $modelMetadata['name'] ?? null;
if ($nameData !== null) {
$nameNode = $xml->addChild('name');
- if (is_array($nameData) && isset($nameData['_value'])) {
- $nameNode[0] = (string)$nameData['_value'];
+ if (is_array($nameData) === true && isset($nameData['_value']) === true) {
+ $nameNode[0] = (string) $nameData['_value'];
foreach (['xml:lang', '_xml:lang', '_xml__lang', 'xml_lang'] as $langKey) {
- if (isset($nameData[$langKey])) {
+ if (isset($nameData[$langKey]) === true) {
$nameNode->addAttribute('xml:lang', $nameData[$langKey], 'http://www.w3.org/XML/1998/namespace');
break;
}
}
- } elseif (is_string($nameData)) {
+ } else if (is_string($nameData) === true) {
$nameNode[0] = $nameData;
}
}
- // Resolve documentation: prefer xml field, fall back to flat field
+ // Resolve documentation: prefer xml field, fall back to flat field.
$docData = $xmlField['documentation'] ?? $modelMetadata['documentation'] ?? null;
if ($docData !== null) {
$docNode = $xml->addChild('documentation');
- if (is_array($docData) && isset($docData['_value'])) {
- $docNode[0] = (string)$docData['_value'];
+ if (is_array($docData) === true && isset($docData['_value']) === true) {
+ $docNode[0] = (string) $docData['_value'];
foreach (['xml:lang', '_xml:lang', '_xml__lang', 'xml_lang'] as $langKey) {
- if (isset($docData[$langKey])) {
+ if (isset($docData[$langKey]) === true) {
$docNode->addAttribute('xml:lang', $docData[$langKey], 'http://www.w3.org/XML/1998/namespace');
break;
}
}
- } elseif (is_string($docData)) {
+ } else if (is_string($docData) === true) {
$docNode[0] = $docData;
}
}
- // Resolve properties: prefer xml field, fall back to flat field
+ // Resolve properties: prefer xml field, fall back to flat field.
$propsData = $xmlField['properties'] ?? $modelMetadata['properties'] ?? null;
- if ($propsData !== null && is_array($propsData)) {
- $this->addPropertiesToXml($xml, $propsData);
+ if ($propsData !== null && is_array($propsData) === true) {
+ $this->addPropertiesToXml(node: $xml, properties: $propsData);
}
- }
+ }//end addModelMetadataToXml()
/**
* Optimized method to add data to XML node
*/
private function addDataToXmlNode(\SimpleXMLElement $node, array $data): void
{
- // Add attributes first
- if (isset($data['_attributes']) && is_array($data['_attributes'])) {
+ // Add attributes first.
+ if (isset($data['_attributes']) === true && is_array($data['_attributes']) === true) {
foreach ($data['_attributes'] as $attrName => $attrValue) {
- $node->addAttribute($attrName, (string)$attrValue);
+ $node->addAttribute($attrName, (string) $attrValue);
}
}
- // Add text content
- if (isset($data['_value'])) {
- $node[0] = (string)$data['_value'];
+ // Add text content.
+ if (isset($data['_value']) === true) {
+ $node[0] = (string) $data['_value'];
}
- // Add child elements
+ // Add child elements.
foreach ($data as $key => $value) {
- if ($key === '_attributes' || $key === '_value' || is_int($key)) {
+ if ($key === '_attributes' || $key === '_value' || is_int($key) === true) {
continue;
}
- if (is_array($value)) {
- if (isset($value[0])) {
- // Array of items
+ if (is_array($value) === true) {
+ if (isset($value[0]) === true) {
+ // Array of items.
foreach ($value as $item) {
$child = $node->addChild($key);
- if (is_array($item)) {
- $this->addDataToXmlNode($child, $item);
+ if (is_array($item) === true) {
+ $this->addDataToXmlNode(node: $child, data: $item);
} else {
- $child[0] = (string)$item;
+ $child[0] = (string) $item;
}
}
} else {
- // Single object
+ // Single object.
$child = $node->addChild($key);
- $this->addDataToXmlNode($child, $value);
+ $this->addDataToXmlNode(node: $child, data: $value);
}
} else {
- // Scalar value
- $child = $node->addChild($key);
- $child[0] = (string)$value;
- }
- }
- }
+ // Scalar value.
+ $child = $node->addChild($key);
+ $child[0] = (string) $value;
+ }//end if
+ }//end foreach
+ }//end addDataToXmlNode()
/**
* Convert OpenRegister objects back to ArchiMate format
- *
- * @param array $objects OpenRegister objects from all schemas
- * @param array $schemaIdMap Mapping of schema IDs to schema types
+ *
+ * @param array $objects OpenRegister objects from all schemas
+ * @param array $schemaIdMap Mapping of schema IDs to schema types
* @return array ArchiMate data structure
*/
public function convertFromOpenRegisterObjects(array $objects, array $schemaIdMap): array
{
- $this->logger->info('Converting from OpenRegister objects back to ArchiMate format', [
- 'total_objects' => count($objects)
- ]);
+ $this->logger->info(
+ 'Converting from OpenRegister objects back to ArchiMate format',
+ [
+ 'total_objects' => count($objects),
+ ]
+ );
+
+ // First, organize objects by schema type based on their schema ID.
+ $organizedObjects = $this->organizeObjectsBySchemaType(objects: $objects, schemaIdMap: $schemaIdMap);
- // First, organize objects by schema type based on their schema ID
- $organizedObjects = $this->organizeObjectsBySchemaType($objects, $schemaIdMap);
-
$archiMateData = [
- 'model_metadata' => [],
- 'elements' => [],
- 'relationships' => [],
- 'organizations' => [],
- 'views' => [],
- 'property_definitions' => []
+ 'model_metadata' => [],
+ 'elements' => [],
+ 'relationships' => [],
+ 'organizations' => [],
+ 'views' => [],
+ 'property_definitions' => [],
];
- // Process organized objects by schema type
+ // Process organized objects by schema type.
foreach ($organizedObjects as $schemaType => $schemaObjects) {
- $this->logger->debug("Processing objects for schema type", [
- 'schema_type' => $schemaType,
- 'object_count' => count($schemaObjects)
- ]);
+ $this->logger->debug(
+ "Processing objects for schema type",
+ [
+ 'schema_type' => $schemaType,
+ 'object_count' => count($schemaObjects),
+ ]
+ );
foreach ($schemaObjects as $object) {
- $section = $this->mapSchemaTypeToSection($schemaType);
+ $section = $this->mapSchemaTypeToSection(schemaType: $schemaType);
$identifier = $object['identifier'] ?? '';
- $xmlData = json_decode($object['xml_data'] ?? '{}', true);
+ $xmlData = json_decode($object['xml_data'] ?? '{}', true);
if ($section === 'model_metadata') {
$archiMateData['model_metadata'] = $xmlData;
- $this->logger->debug('Added model metadata', [
- 'identifier' => $identifier
- ]);
+ $this->logger->debug(
+ 'Added model metadata',
+ [
+ 'identifier' => $identifier,
+ ]
+ );
} else {
$archiMateData[$section][$identifier] = $xmlData;
- $this->logger->debug('Added section object', [
- 'section' => $section,
- 'identifier' => $identifier,
- 'schema_type' => $schemaType
- ]);
+ $this->logger->debug(
+ 'Added section object',
+ [
+ 'section' => $section,
+ 'identifier' => $identifier,
+ 'schema_type' => $schemaType,
+ ]
+ );
}
- }
- }
+ }//end foreach
+ }//end foreach
- // Reconstruct the proper nested XML structure for export
- $archiMateData = $this->reconstructNestedXmlStructure($archiMateData);
+ // Reconstruct the proper nested XML structure for export.
+ $archiMateData = $this->reconstructNestedXmlStructure(archiMateData: $archiMateData);
- $this->logger->info('Conversion completed', [
- 'sections' => array_keys($archiMateData)
- ]);
+ $this->logger->info(
+ 'Conversion completed',
+ [
+ 'sections' => array_keys($archiMateData),
+ ]
+ );
return $archiMateData;
- }
+ }//end convertFromOpenRegisterObjects()
/**
* Organize objects by schema type based on their schema ID
- *
- * @param array $objects Raw objects from database
- * @param array $schemaIdMap Mapping of schema IDs to schema types
+ *
+ * @param array $objects Raw objects from database
+ * @param array $schemaIdMap Mapping of schema IDs to schema types
* @return array Objects organized by schema type
*/
private function organizeObjectsBySchemaType(array $objects, array $schemaIdMap): array
{
$organizedObjects = [];
- // Organize objects by their schema
+ // Organize objects by their schema.
foreach ($objects as $object) {
- // Serialize the object if it's not already an array
- if (is_object($object) && method_exists($object, 'jsonSerialize')) {
+ // Serialize the object if it's not already an array.
+ if (is_object($object) === true && method_exists($object, 'jsonSerialize') === true) {
$object = $object->jsonSerialize();
}
$schemaId = $object['@self']['schema'] ?? null;
-
- if ($schemaId && isset($schemaIdMap[$schemaId])) {
+
+ if ($schemaId !== false && isset($schemaIdMap[$schemaId]) === true) {
$schemaType = $schemaIdMap[$schemaId];
-
- if (!isset($organizedObjects[$schemaType])) {
+
+ if (isset($organizedObjects[$schemaType]) === false) {
$organizedObjects[$schemaType] = [];
}
-
+
$organizedObjects[$schemaType][] = $object;
}
}
return $organizedObjects;
- }
+ }//end organizeObjectsBySchemaType()
/**
* Map schema type to ArchiMate section name
- *
- * @param string $schemaType Schema type from AMEF config
+ *
+ * @param string $schemaType Schema type from AMEF config
* @return string Section name for ArchiMate data structure
*/
private function mapSchemaTypeToSection(string $schemaType): string
{
$mapping = [
- 'model' => 'model_metadata',
- 'element' => 'elements',
- 'relationship' => 'relationships',
- 'view' => 'views',
- 'organization' => 'organizations',
- 'property_definition' => 'property_definitions'
+ 'model' => 'model_metadata',
+ 'element' => 'elements',
+ 'relationship' => 'relationships',
+ 'view' => 'views',
+ 'organization' => 'organizations',
+ 'property_definition' => 'property_definitions',
];
return $mapping[$schemaType] ?? $schemaType;
- }
+ }//end mapSchemaTypeToSection()
/**
* Reconstruct the proper nested XML structure for export
- *
- * @param array $archiMateData Flattened ArchiMate data
+ *
+ * @param array $archiMateData Flattened ArchiMate data
* @return array Properly nested XML structure
*/
private function reconstructNestedXmlStructure(array $archiMateData): array
{
- // Reconstruct views with diagrams wrapper
- if (!empty($archiMateData['views']) && is_array($archiMateData['views'])) {
+ // Reconstruct views with diagrams wrapper.
+ if (empty($archiMateData['views']) === false && is_array($archiMateData['views']) === true) {
$archiMateData['views'] = [
- 'diagrams' => $archiMateData['views']
+ 'diagrams' => $archiMateData['views'],
];
}
- // Reconstruct organizations with items wrapper
- if (!empty($archiMateData['organizations']) && is_array($archiMateData['organizations'])) {
+ // Reconstruct organizations with items wrapper.
+ if (empty($archiMateData['organizations']) === false && is_array($archiMateData['organizations']) === true) {
$items = [];
foreach ($archiMateData['organizations'] as $org) {
$items[] = $org;
}
+
$archiMateData['organizations'] = [
- 'item' => $items
+ 'item' => $items,
];
}
return $archiMateData;
- }
+ }//end reconstructNestedXmlStructure()
/**
* Run comprehensive Quality Assurance checks on exported XML
- *
- * @param string $xmlString The generated XML string
- * @param array $sourceData The original source data for reference
+ *
+ * @param string $xmlString The generated XML string
+ * @param array $sourceData The original source data for reference
* @throws \InvalidArgumentException If any QA check fails
*/
private function runQualityAssuranceChecks(string $xmlString, array $sourceData): void
{
$this->logger->info('Running Quality Assurance checks on exported XML');
-
- // DEBUG: Save XML to file for inspection
+
+ // DEBUG: Save XML to file for inspection.
$debugPath = '/tmp/debug_export.xml';
file_put_contents($debugPath, $xmlString);
- $this->logger->info('DEBUG: Raw XML saved to ' . $debugPath . ' (size: ' . strlen($xmlString) . ' bytes)');
-
+ $this->logger->info('DEBUG: Raw XML saved to '.$debugPath.' (size: '.strlen($xmlString).' bytes)');
+
try {
$xml = new \SimpleXMLElement($xmlString);
-
- // QA Check 1: Every has xsi:type and unique identifier
- $this->validateElementsHaveTypeAndIdentifier($xml);
-
- // QA Check 2: Every has xsi:type, source, target with valid references
- $this->validateRelationshipsHaveSourceTarget($xml);
-
- // QA Check 3: No empty tags; all have propertyDefinitionRef and
- $this->validatePropertiesAreNotEmpty($xml);
-
- // QA Check 4: propid-2 exists for all elements and value == identifier
- $this->validateObjectIdProperty($xml);
-
- // QA Check 5: name/documentation are trimmed and whitespace normalized
- $this->validateTextContentNormalized($xml);
-
+
+ // QA Check 1: Every has xsi:type and unique identifier.
+ $this->validateElementsHaveTypeAndIdentifier(xml: $xml);
+
+ // QA Check 2: Every has xsi:type, source, target with valid references.
+ $this->validateRelationshipsHaveSourceTarget(xml: $xml);
+
+ // QA Check 3: No empty tags; all have propertyDefinitionRef and .
+ $this->validatePropertiesAreNotEmpty(xml: $xml);
+
+ // QA Check 4: propid-2 exists for all elements and value === identifier.
+ $this->validateObjectIdProperty(xml: $xml);
+
+ // QA Check 5: name/documentation are trimmed and whitespace normalized.
+ $this->validateTextContentNormalized(xml: $xml);
+
$this->logger->info('All Quality Assurance checks passed successfully');
-
} catch (\Exception $e) {
- $this->logger->error('Quality Assurance check failed: ' . $e->getMessage());
- throw new \InvalidArgumentException('Export QA validation failed: ' . $e->getMessage());
- }
- }
+ $this->logger->error('Quality Assurance check failed: '.$e->getMessage());
+ throw new \InvalidArgumentException('Export QA validation failed: '.$e->getMessage());
+ }//end try
+ }//end runQualityAssuranceChecks()
/**
* Validate that every element has xsi:type and unique identifier
*/
private function validateElementsHaveTypeAndIdentifier(\SimpleXMLElement $xml): void
{
- $elements = $xml->xpath('//element');
+ $elements = $xml->xpath('//element');
$identifiers = [];
-
+
foreach ($elements as $element) {
$attributes = $element->attributes();
-
- // Check xsi:type exists
+
+ // Check xsi:type exists.
$xsiType = $element->attributes('http://www.w3.org/2001/XMLSchema-instance');
- if (!isset($xsiType['type'])) {
- throw new \InvalidArgumentException("Element missing xsi:type: " . (string)$attributes['identifier']);
+ if (isset($xsiType['type']) === false) {
+ throw new \InvalidArgumentException("Element missing xsi:type: ".(string) $attributes['identifier']);
}
-
- // Check identifier exists and is unique
- if (!isset($attributes['identifier'])) {
+
+ // Check identifier exists and is unique.
+ if (isset($attributes['identifier']) === false) {
throw new \InvalidArgumentException("Element missing identifier");
}
-
- $identifier = (string)$attributes['identifier'];
- if (in_array($identifier, $identifiers)) {
- throw new \InvalidArgumentException("Duplicate identifier found: " . $identifier);
+
+ $identifier = (string) $attributes['identifier'];
+ if (in_array($identifier, $identifiers) === true) {
+ throw new \InvalidArgumentException("Duplicate identifier found: ".$identifier);
}
+
$identifiers[] = $identifier;
- }
-
- $this->logger->debug("Validated " . count($elements) . " elements with unique identifiers and xsi:type");
- }
+ }//end foreach
+
+ $this->logger->debug("Validated ".count($elements)." elements with unique identifiers and xsi:type");
+ }//end validateElementsHaveTypeAndIdentifier()
/**
* Validate that every relationship has xsi:type, source, target with valid references
*/
private function validateRelationshipsHaveSourceTarget(\SimpleXMLElement $xml): void
{
- $relationships = $xml->xpath('//relationship');
+ $relationships = $xml->xpath('//relationship');
$allIdentifiers = [];
-
- // Collect all valid identifiers from elements and relationships
+
+ // Collect all valid identifiers from elements and relationships.
foreach ($xml->xpath('//*[@identifier]') as $node) {
- $allIdentifiers[] = (string)$node->attributes()['identifier'];
+ $allIdentifiers[] = (string) $node->attributes()['identifier'];
}
-
+
foreach ($relationships as $relationship) {
$attributes = $relationship->attributes();
-
- // Check xsi:type exists
+
+ // Check xsi:type exists.
$xsiType = $relationship->attributes('http://www.w3.org/2001/XMLSchema-instance');
- if (!isset($xsiType['type'])) {
- throw new \InvalidArgumentException("Relationship missing xsi:type: " . (string)$attributes['identifier']);
+ if (isset($xsiType['type']) === false) {
+ throw new \InvalidArgumentException("Relationship missing xsi:type: ".(string) $attributes['identifier']);
}
-
- // Check source exists and references valid identifier
- if (!isset($attributes['source'])) {
- throw new \InvalidArgumentException("Relationship missing source: " . (string)$attributes['identifier']);
+
+ // Check source exists and references valid identifier.
+ if (isset($attributes['source']) === false) {
+ throw new \InvalidArgumentException("Relationship missing source: ".(string) $attributes['identifier']);
}
-
- $source = (string)$attributes['source'];
- if (!in_array($source, $allIdentifiers)) {
- throw new \InvalidArgumentException("Relationship source references non-existent identifier: " . $source);
+
+ $source = (string) $attributes['source'];
+ if (in_array($source, $allIdentifiers) === false) {
+ throw new \InvalidArgumentException("Relationship source references non-existent identifier: ".$source);
}
-
- // Check target exists and references valid identifier
- if (!isset($attributes['target'])) {
- throw new \InvalidArgumentException("Relationship missing target: " . (string)$attributes['identifier']);
+
+ // Check target exists and references valid identifier.
+ if (isset($attributes['target']) === false) {
+ throw new \InvalidArgumentException("Relationship missing target: ".(string) $attributes['identifier']);
}
-
- $target = (string)$attributes['target'];
- if (!in_array($target, $allIdentifiers)) {
- throw new \InvalidArgumentException("Relationship target references non-existent identifier: " . $target);
+
+ $target = (string) $attributes['target'];
+ if (in_array($target, $allIdentifiers) === false) {
+ throw new \InvalidArgumentException("Relationship target references non-existent identifier: ".$target);
}
- }
-
- $this->logger->debug("Validated " . count($relationships) . " relationships with valid source/target references");
- }
+ }//end foreach
+
+ $this->logger->debug("Validated ".count($relationships)." relationships with valid source/target references");
+ }//end validateRelationshipsHaveSourceTarget()
/**
* Validate that no properties are empty; all have propertyDefinitionRef and value
@@ -1811,29 +1961,29 @@ private function validateRelationshipsHaveSourceTarget(\SimpleXMLElement $xml):
private function validatePropertiesAreNotEmpty(\SimpleXMLElement $xml): void
{
$properties = $xml->xpath('//property');
-
+
foreach ($properties as $property) {
$attributes = $property->attributes();
-
- // Check propertyDefinitionRef exists
- if (!isset($attributes['propertyDefinitionRef'])) {
+
+ // Check propertyDefinitionRef exists.
+ if (isset($attributes['propertyDefinitionRef']) === false) {
throw new \InvalidArgumentException("Property missing propertyDefinitionRef");
}
-
- // Check value element exists and has content
+
+ // Check value element exists and has content.
$valueElements = $property->xpath('value');
- if (empty($valueElements)) {
- throw new \InvalidArgumentException("Property missing value element: " . (string)$attributes['propertyDefinitionRef']);
+ if (empty($valueElements) === true) {
+ throw new \InvalidArgumentException("Property missing value element: ".(string) $attributes['propertyDefinitionRef']);
}
-
- $value = trim((string)$valueElements[0]);
- if (empty($value)) {
- throw new \InvalidArgumentException("Property has empty value: " . (string)$attributes['propertyDefinitionRef']);
+
+ $value = trim((string) $valueElements[0]);
+ if (empty($value) === true) {
+ throw new \InvalidArgumentException("Property has empty value: ".(string) $attributes['propertyDefinitionRef']);
}
}
-
- $this->logger->debug("Validated " . count($properties) . " properties have propertyDefinitionRef and non-empty values");
- }
+
+ $this->logger->debug("Validated ".count($properties)." properties have propertyDefinitionRef and non-empty values");
+ }//end validatePropertiesAreNotEmpty()
/**
* Validate that propid-2 exists for all elements and value equals identifier
@@ -1841,31 +1991,31 @@ private function validatePropertiesAreNotEmpty(\SimpleXMLElement $xml): void
private function validateObjectIdProperty(\SimpleXMLElement $xml): void
{
$elements = $xml->xpath('//element');
-
+
foreach ($elements as $element) {
- $identifier = (string)$element->attributes()['identifier'];
-
- // Find propid-2 property
+ $identifier = (string) $element->attributes()['identifier'];
+
+ // Find propid-2 property.
$objectIdProps = $element->xpath('properties/property[@propertyDefinitionRef="propid-2"]');
- if (empty($objectIdProps)) {
- throw new \InvalidArgumentException("Element missing propid-2 property: " . $identifier);
+ if (empty($objectIdProps) === true) {
+ throw new \InvalidArgumentException("Element missing propid-2 property: ".$identifier);
}
-
+
$valueElements = $objectIdProps[0]->xpath('value');
- if (empty($valueElements)) {
- throw new \InvalidArgumentException("Element propid-2 missing value: " . $identifier);
+ if (empty($valueElements) === true) {
+ throw new \InvalidArgumentException("Element propid-2 missing value: ".$identifier);
}
-
- $objectIdValue = trim((string)$valueElements[0]);
- $expectedValue = str_replace('id-', '', $identifier); // Remove 'id-' prefix for comparison
-
+
+ $objectIdValue = trim((string) $valueElements[0]);
+ $expectedValue = str_replace('id-', '', $identifier);
+ // Remove 'id-' prefix for comparison.
if ($objectIdValue !== $expectedValue) {
- throw new \InvalidArgumentException("Element propid-2 value mismatch. Expected: " . $expectedValue . ", Got: " . $objectIdValue . " (Element: " . $identifier . ")");
+ throw new \InvalidArgumentException("Element propid-2 value mismatch. Expected: ".$expectedValue.", Got: ".$objectIdValue." (Element: ".$identifier.")");
}
- }
-
- $this->logger->debug("Validated propid-2 property for " . count($elements) . " elements");
- }
+ }//end foreach
+
+ $this->logger->debug("Validated propid-2 property for ".count($elements)." elements");
+ }//end validateObjectIdProperty()
/**
* Validate that name/documentation text content is trimmed and whitespace normalized
@@ -1873,28 +2023,29 @@ private function validateObjectIdProperty(\SimpleXMLElement $xml): void
private function validateTextContentNormalized(\SimpleXMLElement $xml): void
{
$textElements = $xml->xpath('//name | //documentation | //value');
-
+
foreach ($textElements as $element) {
- $content = (string)$element;
- $trimmed = trim($content);
- $normalized = preg_replace('/\s+/', ' ', $trimmed); // Normalize multiple whitespace to single space
-
+ $content = (string) $element;
+ $trimmed = trim($content);
+ $normalized = preg_replace('/\s+/', ' ', $trimmed);
+ // Normalize multiple whitespace to single space.
if ($content !== $normalized) {
- $tagName = $element->getName();
+ $tagName = $element->getName();
$parentId = '';
- if ($element->xpath('../@identifier')) {
- $parentId = ' (Parent: ' . (string)$element->xpath('../@identifier')[0] . ')';
+ if ($element->xpath('../@identifier') === true) {
+ $parentId = ' (Parent: '.(string) $element->xpath('../@identifier')[0].')';
}
- throw new \InvalidArgumentException("Text content not normalized in <" . $tagName . ">" . $parentId . ". Expected: '" . $normalized . "', Got: '" . $content . "'");
+
+ throw new \InvalidArgumentException("Text content not normalized in <".$tagName.">".$parentId.". Expected: '".$normalized."', Got: '".$content."'");
}
}
-
- $this->logger->debug("Validated " . count($textElements) . " text elements are properly trimmed and normalized");
- }
- // =========================================================================
- // Organization-specific ArchiMate export
- // =========================================================================
+ $this->logger->debug("Validated ".count($textElements)." text elements are properly trimmed and normalized");
+ }//end validateTextContentNormalized()
+
+ // =======================================================.
+ // Organization-specific ArchiMate export.
+ // =======================================================.
/**
* Export organization-enriched ArchiMate XML.
@@ -1904,13 +2055,14 @@ private function validateTextContentNormalized(\SimpleXMLElement $xml): void
* referentiecomponenten, copies views with applications plotted inside, and
* adds SWC-specific organization folders.
*
- * @param \OCA\OpenRegister\Service\ObjectService $objectService
- * @param int $registerId AMEF register ID
- * @param array $schemaIdMap Schema ID → type mapping
- * @param string $orgName Human-readable organization name
- * @param string $orgUuid Organization UUID
- * @param array $gebruikData Usage objects for this organization
- * @param array $modulesData Module objects for this organization
+ * @param \OCA\OpenRegister\Service\ObjectService $objectService
+ * @param int $registerId AMEF register ID
+ * @param array $schemaIdMap Schema ID → type
+ * mapping
+ * @param string $orgName Human-readable organization name
+ * @param string $orgUuid Organization UUID
+ * @param array $gebruikData Usage objects for this organization
+ * @param array $modulesData Module objects for this organization
* @return string Generated XML
*/
public function exportOrganizationArchiMateXml(
@@ -1921,72 +2073,91 @@ public function exportOrganizationArchiMateXml(
string $orgUuid,
array $gebruikData,
array $modulesData,
- array $deelnamesData = [],
- array $options = []
+ array $deelnamesData=[],
+ array $options=[]
): string {
$startTime = microtime(true);
- $this->logger->info('Starting organization ArchiMate export', [
- 'organization' => $orgName,
- 'gebruik_count' => count($gebruikData),
- 'modules_count' => count($modulesData),
- 'deelnames_count' => count($deelnamesData),
- 'options' => $options
- ]);
-
- // Step 1: Get all base GEMMA objects
- $baseObjects = $this->getObjectsFromDatabase($objectService, $registerId, $schemaIdMap);
-
- // Step 2: Ensure Bron property definition
- $bronPropDefId = $this->ensureBronPropertyDefinition($baseObjects);
-
- // Step 3: Build lookup maps and generate elements per data type
- $gebruiktAppElements = [];
+ $this->logger->info(
+ 'Starting organization ArchiMate export',
+ [
+ 'organization' => $orgName,
+ 'gebruik_count' => count($gebruikData),
+ 'modules_count' => count($modulesData),
+ 'deelnames_count' => count($deelnamesData),
+ 'options' => $options,
+ ]
+ );
+
+ // Step 1: Get all base GEMMA objects.
+ $baseObjects = $this->getObjectsFromDatabase(objectService: $objectService, registerId: $registerId, schemaIdMap: $schemaIdMap);
+
+ // Step 2: Ensure Bron property definition.
+ $bronPropDefId = $this->ensureBronPropertyDefinition(baseObjects: $baseObjects);
+
+ // Step 3: Build lookup maps and generate elements per data type.
+ $gebruiktAppElements = [];
$gebruiktRelationships = [];
if ($options['modules'] ?? true) {
- [$moduleRefMap, $moduleNameMap] = $this->buildModuleLookupMaps($gebruikData, $modulesData);
- $gebruiktAppElements = $this->generateApplicationElements($moduleRefMap, $moduleNameMap, $bronPropDefId);
- $gebruiktRelationships = $this->generateSpecializationRelationships($moduleRefMap, $bronPropDefId);
+ [$moduleRefMap, $moduleNameMap] = $this->buildModuleLookupMaps(gebruikData: $gebruikData, modulesData: $modulesData);
+ $gebruiktAppElements = $this->generateApplicationElements(moduleRefMap: $moduleRefMap, moduleNameMap: $moduleNameMap, bronPropDefId: $bronPropDefId);
+ $gebruiktRelationships = $this->generateSpecializationRelationships(moduleRefMap: $moduleRefMap, bronPropDefId: $bronPropDefId);
}
- $deelnamesAppElements = [];
+ $deelnamesAppElements = [];
$deelnamesRelationships = [];
- if (($options['deelnames'] ?? false) && !empty($deelnamesData)) {
- [$deelnameRefMap, $deelnameNameMap] = $this->buildModuleLookupMaps($deelnamesData, $modulesData);
- $deelnamesAppElements = $this->generateApplicationElements($deelnameRefMap, $deelnameNameMap, $bronPropDefId, 'deelname');
- $deelnamesRelationships = $this->generateSpecializationRelationships($deelnameRefMap, $bronPropDefId, 'deelname');
+ if (($options['deelnames'] ?? false) === true && empty($deelnamesData) === false) {
+ [$deelnameRefMap, $deelnameNameMap] = $this->buildModuleLookupMaps(gebruikData: $deelnamesData, modulesData: $modulesData);
+ $deelnamesAppElements = $this->generateApplicationElements(moduleRefMap: $deelnameRefMap, moduleNameMap: $deelnameNameMap, bronPropDefId: $bronPropDefId, prefix: 'deelname');
+ $deelnamesRelationships = $this->generateSpecializationRelationships(moduleRefMap: $deelnameRefMap, bronPropDefId: $bronPropDefId, prefix: 'deelname');
}
- // Merge all elements and relationships for view enrichment
- $allAppElements = array_merge($gebruiktAppElements, $deelnamesAppElements);
+ // Merge all elements and relationships for view enrichment.
+ $allAppElements = array_merge($gebruiktAppElements, $deelnamesAppElements);
$allRelationships = array_merge($gebruiktRelationships, $deelnamesRelationships);
- // Step 4: Copy and enrich views with all elements
+ // Step 4: Copy and enrich views with all elements.
$viewCopies = $this->copyAndEnrichViews(
- $baseObjects, $orgName, $allAppElements, $allRelationships, $bronPropDefId
+ $baseObjects,
+ $orgName,
+ $allAppElements,
+ $allRelationships,
+ $bronPropDefId
);
- // Step 5: Build SWC organization folders with typed structure
+ // Step 5: Build SWC organization folders with typed structure.
$swcFolders = $this->buildSwcOrganizationFolders(
- $gebruiktAppElements, $deelnamesAppElements, $allRelationships, $viewCopies
+ $gebruiktAppElements,
+ $deelnamesAppElements,
+ $allRelationships,
+ $viewCopies
);
- // Step 6: Assemble into XML
+ // Step 6: Assemble into XML.
$xml = $this->assembleOrganizationXml(
- $baseObjects, $orgName, $allAppElements, $allRelationships, $viewCopies, $swcFolders, $bronPropDefId
+ $baseObjects,
+ $orgName,
+ $allAppElements,
+ $allRelationships,
+ $viewCopies,
+ $swcFolders,
+ $bronPropDefId
);
$totalTime = microtime(true) - $startTime;
- $this->logger->info('Organization ArchiMate export completed', [
- 'organization' => $orgName,
- 'gebruikt_elements' => count($gebruiktAppElements),
- 'deelnames_elements' => count($deelnamesAppElements),
- 'relationships' => count($allRelationships),
- 'view_copies' => count($viewCopies),
- 'total_time_seconds' => round($totalTime, 3)
- ]);
+ $this->logger->info(
+ 'Organization ArchiMate export completed',
+ [
+ 'organization' => $orgName,
+ 'gebruikt_elements' => count($gebruiktAppElements),
+ 'deelnames_elements' => count($deelnamesAppElements),
+ 'relationships' => count($allRelationships),
+ 'view_copies' => count($viewCopies),
+ 'total_time_seconds' => round($totalTime, 3),
+ ]
+ );
return $xml;
- }
+ }//end exportOrganizationArchiMateXml()
/**
* Build lookup maps from gebruik and modules data.
@@ -1997,62 +2168,79 @@ public function exportOrganizationArchiMateXml(
*/
private function buildModuleLookupMaps(array $gebruikData, array $modulesData): array
{
- $moduleRefMap = []; // moduleId => [refCompIdentifiers]
- $moduleNameMap = []; // moduleId => name
-
- // Build name map from modules data
+ $moduleRefMap = [];
+ // moduleId => [refCompIdentifiers].
+ $moduleNameMap = [];
+ // moduleId => name.
+ // Build name map from modules data.
foreach ($modulesData as $module) {
- if (is_object($module) && method_exists($module, 'jsonSerialize')) {
+ if (is_object($module) === true && method_exists($module, 'jsonSerialize') === true) {
$module = $module->jsonSerialize();
}
- $id = $module['id'] ?? $module['@self']['id'] ?? null;
+
+ $id = $module['id'] ?? $module['@self']['id'] ?? null;
$name = $module['naam'] ?? $module['name'] ?? $module['@self']['name'] ?? null;
- if ($id && $name) {
+ if ($id !== false && $name === true) {
$moduleNameMap[$id] = $name;
}
}
- // Build ref map from gebruik data
+ // Build ref map from gebruik data.
foreach ($gebruikData as $gebruik) {
- if (is_object($gebruik) && method_exists($gebruik, 'jsonSerialize')) {
+ if (is_object($gebruik) === true && method_exists($gebruik, 'jsonSerialize') === true) {
$gebruik = $gebruik->jsonSerialize();
}
$moduleId = $gebruik['module'] ?? null;
- if (!$moduleId) continue;
+ if ($moduleId === null) {
+ continue;
+ }
- // Get module name from gebruik if not already known
- if (!isset($moduleNameMap[$moduleId])) {
+ // Get module name from gebruik if not already known.
+ if (isset($moduleNameMap[$moduleId]) === false) {
$moduleNameMap[$moduleId] = $gebruik['moduleName'] ?? $gebruik['@self']['name'] ?? 'Module';
}
- // Get referentiecomponenten UUIDs
+ // Get referentiecomponenten UUIDs.
$refComps = $gebruik['gebruiktVoorReferentiecomponenten'] ?? [];
- if (!is_array($refComps)) continue;
+ if (is_array($refComps) === false) {
+ continue;
+ }
foreach ($refComps as $refComp) {
- $refCompUuid = is_string($refComp) ? $refComp : ($refComp['id'] ?? $refComp['uuid'] ?? null);
- if (!$refCompUuid) continue;
+ if (is_string($refComp) === true) {
+ $refCompUuid = $refComp;
+ } else {
+ $refCompUuid = ($refComp['id'] ?? $refComp['uuid'] ?? null);
+ }
- // Build the ArchiMate identifier (id-{uuid})
- $refCompIdentifier = 'id-' . $refCompUuid;
+ if ($refCompUuid === null) {
+ continue;
+ }
- if (!isset($moduleRefMap[$moduleId])) {
+ // Build the ArchiMate identifier (id-{uuid}).
+ $refCompIdentifier = 'id-'.$refCompUuid;
+
+ if (isset($moduleRefMap[$moduleId]) === false) {
$moduleRefMap[$moduleId] = [];
}
- if (!in_array($refCompIdentifier, $moduleRefMap[$moduleId])) {
+
+ if (in_array($refCompIdentifier, $moduleRefMap[$moduleId]) === false) {
$moduleRefMap[$moduleId][] = $refCompIdentifier;
}
- }
- }
-
- $this->logger->debug('Module lookup maps built', [
- 'modules_with_refs' => count($moduleRefMap),
- 'modules_with_names' => count($moduleNameMap)
- ]);
+ }//end foreach
+ }//end foreach
+
+ $this->logger->debug(
+ 'Module lookup maps built',
+ [
+ 'modules_with_refs' => count($moduleRefMap),
+ 'modules_with_names' => count($moduleNameMap),
+ ]
+ );
return [$moduleRefMap, $moduleNameMap];
- }
+ }//end buildModuleLookupMaps()
/**
* Check if a Bron property definition exists in base objects, add one if not.
@@ -2063,18 +2251,19 @@ private function ensureBronPropertyDefinition(array &$baseObjects): string
{
$bronId = 'id-swc-propdef-bron';
- // Check if "Bron" already exists
+ // Check if "Bron" already exists.
foreach ($baseObjects as $obj) {
- if (is_object($obj) && method_exists($obj, 'jsonSerialize')) {
+ if (is_object($obj) === true && method_exists($obj, 'jsonSerialize') === true) {
$obj = $obj->jsonSerialize();
}
+
$section = $obj['section'] ?? '';
if ($section === 'property_definition') {
- $xml = $obj['xml'] ?? [];
+ $xml = $obj['xml'] ?? [];
$name = $xml['name']['_value'] ?? $xml['name'] ?? null;
if ($name === 'Bron') {
$existingId = $xml['_identifier'] ?? $obj['identifier'] ?? null;
- if ($existingId) {
+ if (empty($existingId) === false) {
$this->logger->debug('Found existing Bron property definition', ['id' => $existingId]);
return $existingId;
}
@@ -2084,57 +2273,70 @@ private function ensureBronPropertyDefinition(array &$baseObjects): string
$this->logger->debug('Bron property definition not found, will create', ['id' => $bronId]);
return $bronId;
- }
+ }//end ensureBronPropertyDefinition()
/**
* Generate ApplicationComponent element arrays for each module.
*
* @return array Array of element data arrays ready for XML generation
*/
- private function generateApplicationElements(array $moduleRefMap, array $moduleNameMap, string $bronPropDefId, string $prefix = ''): array
+ private function generateApplicationElements(array $moduleRefMap, array $moduleNameMap, string $bronPropDefId, string $prefix=''): array
{
$elements = [];
- $idPrefix = $prefix ? 'id-swc-' . $prefix . '-app-' : 'id-swc-app-';
+ if ($prefix) {
+ $idPrefix = 'id-swc-'.$prefix.'-app-';
+ } else {
+ $idPrefix = 'id-swc-app-';
+ }
foreach ($moduleRefMap as $moduleId => $refCompIds) {
- $appIdentifier = $idPrefix . $moduleId;
- $name = $moduleNameMap[$moduleId] ?? 'Module';
+ $appIdentifier = $idPrefix.$moduleId;
+ $name = $moduleNameMap[$moduleId] ?? 'Module';
$elements[] = [
- 'identifier' => $appIdentifier,
- 'name' => $name,
- 'xsi_type' => 'ApplicationComponent',
+ 'identifier' => $appIdentifier,
+ 'name' => $name,
+ 'xsi_type' => 'ApplicationComponent',
'bronPropDefId' => $bronPropDefId,
- 'moduleId' => $moduleId,
+ 'moduleId' => $moduleId,
];
}
$this->logger->debug('Generated application elements', ['count' => count($elements), 'prefix' => $prefix]);
return $elements;
- }
+ }//end generateApplicationElements()
/**
* Generate SpecializationRelationship arrays for module → refcomp mappings.
*
* @return array Array of relationship data arrays
*/
- private function generateSpecializationRelationships(array $moduleRefMap, string $bronPropDefId, string $prefix = ''): array
+ private function generateSpecializationRelationships(array $moduleRefMap, string $bronPropDefId, string $prefix=''): array
{
$relationships = [];
- $appIdPrefix = $prefix ? 'id-swc-' . $prefix . '-app-' : 'id-swc-app-';
- $relIdPrefix = $prefix ? 'id-swc-' . $prefix . '-rel-' : 'id-swc-rel-';
+ if ($prefix) {
+ $appIdPrefix = 'id-swc-'.$prefix.'-app-';
+ } else {
+ $appIdPrefix = 'id-swc-app-';
+ }
+
+ if ($prefix) {
+ $relIdPrefix = 'id-swc-'.$prefix.'-rel-';
+ } else {
+ $relIdPrefix = 'id-swc-rel-';
+ }
foreach ($moduleRefMap as $moduleId => $refCompIds) {
- $appIdentifier = $appIdPrefix . $moduleId;
+ $appIdentifier = $appIdPrefix.$moduleId;
foreach ($refCompIds as $refCompIdentifier) {
- $relIdentifier = $relIdPrefix . $moduleId . '-' . str_replace('id-', '', $refCompIdentifier);
+ $relIdentifier = $relIdPrefix.$moduleId.'-'.str_replace('id-', '', $refCompIdentifier);
$relationships[] = [
- 'identifier' => $relIdentifier,
- 'xsi_type' => 'SpecializationRelationship',
- 'source' => $appIdentifier,
- 'target' => $refCompIdentifier,
+ 'identifier' => $relIdentifier,
+ 'xsi_type' => 'SpecializationRelationship',
+ 'source' => $appIdentifier,
+ 'target' => $refCompIdentifier,
'bronPropDefId' => $bronPropDefId,
];
}
@@ -2142,7 +2344,7 @@ private function generateSpecializationRelationships(array $moduleRefMap, string
$this->logger->debug('Generated specialization relationships', ['count' => count($relationships), 'prefix' => $prefix]);
return $relationships;
- }
+ }//end generateSpecializationRelationships()
/**
* Copy qualifying views and inject application nodes inside referentiecomponent nodes.
@@ -2158,73 +2360,82 @@ private function copyAndEnrichViews(
): array {
$viewCopies = [];
- // Build a reverse lookup: refCompIdentifier => [(appIdentifier, relIdentifier, moduleName)]
- // Derived from the actual generated elements and relationships (handles all prefixes)
+ // Build a reverse lookup: refCompIdentifier => [(appIdentifier, relIdentifier, moduleName)].
+ // Derived from the actual generated elements and relationships (handles all prefixes).
$appNameMap = [];
foreach ($appElements as $el) {
$appNameMap[$el['identifier']] = $el['name'];
}
+
$refCompApps = [];
foreach ($relationships as $rel) {
- $appIdentifier = $rel['source'];
+ $appIdentifier = $rel['source'];
$refCompIdentifier = $rel['target'];
- $relIdentifier = $rel['identifier'];
+ $relIdentifier = $rel['identifier'];
$name = $appNameMap[$appIdentifier] ?? 'Module';
- if (!isset($refCompApps[$refCompIdentifier])) {
+ if (isset($refCompApps[$refCompIdentifier]) === false) {
$refCompApps[$refCompIdentifier] = [];
}
+
$refCompApps[$refCompIdentifier][] = [
'appIdentifier' => $appIdentifier,
'relIdentifier' => $relIdentifier,
- 'name' => $name,
+ 'name' => $name,
];
}
- // Iterate view objects
+ // Iterate view objects.
foreach ($baseObjects as $obj) {
- if (is_object($obj) && method_exists($obj, 'jsonSerialize')) {
+ if (is_object($obj) === true && method_exists($obj, 'jsonSerialize') === true) {
$obj = $obj->jsonSerialize();
}
+
$section = $obj['section'] ?? '';
- if ($section !== 'view') continue;
+ if ($section !== 'view') {
+ continue;
+ }
$xmlData = $obj['xml'] ?? [];
- if (empty($xmlData)) continue;
+ if (empty($xmlData) === true) {
+ continue;
+ }
$originalIdentifier = $xmlData['_identifier'] ?? $obj['identifier'] ?? null;
- if (!$originalIdentifier) continue;
+ if ($originalIdentifier === null) {
+ continue;
+ }
- // Deep-copy the view XML
+ // Deep-copy the view XML.
$viewCopy = json_decode(json_encode($xmlData), true);
- // Assign new identifier
- $newIdentifier = 'id-swc-view-' . str_replace('id-', '', $originalIdentifier);
+ // Assign new identifier.
+ $newIdentifier = 'id-swc-view-'.str_replace('id-', '', $originalIdentifier);
$viewCopy['_identifier'] = $newIdentifier;
- if (isset($viewCopy['_attributes']['identifier'])) {
+ if (isset($viewCopy['_attributes']['identifier']) === true) {
$viewCopy['_attributes']['identifier'] = $newIdentifier;
}
- // Rename view: use Titel view SWC property or fallback to original name
- $viewName = $this->getViewSwcTitle($viewCopy) ?? $this->getViewName($viewCopy);
- $viewCopy['name'] = ['_value' => $viewName . ' ' . $orgName];
+ // Rename view: use Titel view SWC property or fallback to original name.
+ $viewName = $this->getViewSwcTitle(viewData: $viewCopy) ?? $this->getViewName(viewData: $viewCopy);
+ $viewCopy['name'] = ['_value' => $viewName.' '.$orgName];
- // Add Bron property to view
- $viewCopy = $this->addBronProperty($viewCopy, $bronPropDefId);
+ // Add Bron property to view.
+ $viewCopy = $this->addBronProperty(data: $viewCopy, bronPropDefId: $bronPropDefId);
- // Inject application nodes and connections
- $viewCopy = $this->injectApplicationNodesInView($viewCopy, $refCompApps);
+ // Inject application nodes and connections.
+ $viewCopy = $this->injectApplicationNodesInView(viewData: $viewCopy, refCompApps: $refCompApps);
$viewCopies[] = [
'identifier' => $newIdentifier,
- 'xml' => $viewCopy,
- 'section' => 'view',
+ 'xml' => $viewCopy,
+ 'section' => 'view',
];
- }
+ }//end foreach
$this->logger->debug('Copied and enriched views', ['count' => count($viewCopies)]);
return $viewCopies;
- }
+ }//end copyAndEnrichViews()
/**
* Extract "Titel view SWC" property value from view XML data.
@@ -2232,41 +2443,53 @@ private function copyAndEnrichViews(
private function getViewSwcTitle(array $viewData): ?string
{
$properties = $viewData['properties']['property'] ?? $viewData['properties'] ?? [];
- if (!is_array($properties)) return null;
- // Normalize to list
- if (isset($properties['_propertyDefinitionRef'])) {
+ if (is_array($properties) === false) {
+ return null;
+ }
+
+ // Normalize to list.
+ if (isset($properties['_propertyDefinitionRef']) === true) {
$properties = [$properties];
}
foreach ($properties as $prop) {
- if (!is_array($prop)) continue;
- // Check property definition name or ref
+ if (is_array($prop) === false) {
+ continue;
+ }
+
+ // Check property definition name or ref.
$value = $prop['value']['_value'] ?? $prop['value'] ?? null;
- // We look for a property whose name contains "Titel view SWC"
- // This is stored as the property's propertyDefinitionRef linking to a named definition
- // For now, check if the property key/label matches
+ // We look for a property whose name contains "Titel view SWC".
+ // This is stored as the property's propertyDefinitionRef linking to a named definition.
+ // For now, check if the property key/label matches.
$propName = $prop['_name'] ?? $prop['name'] ?? '';
- if (is_string($propName) && stripos($propName, 'Titel view SWC') !== false && $value) {
- return is_string($value) ? $value : null;
+ if (is_string($propName) === true && stripos($propName, 'Titel view SWC') !== false && $value === true) {
+ if (is_string($value) === true) {
+ return $value;
+ } else {
+ return null;
+ }
}
}
return null;
- }
+ }//end getViewSwcTitle()
/**
* Extract view name from view XML data.
*/
private function getViewName(array $viewData): string
{
- if (isset($viewData['name']['_value'])) {
+ if (isset($viewData['name']['_value']) === true) {
return $viewData['name']['_value'];
}
- if (isset($viewData['name']) && is_string($viewData['name'])) {
+
+ if (isset($viewData['name']) === true && is_string($viewData['name']) === true) {
return $viewData['name'];
}
+
return 'View';
- }
+ }//end getViewName()
/**
* Add Bron=Softwarecatalogus property to an XML data array.
@@ -2275,14 +2498,14 @@ private function addBronProperty(array $data, string $bronPropDefId): array
{
$bronProp = [
'_propertyDefinitionRef' => $bronPropDefId,
- 'value' => ['_value' => 'Softwarecatalogus']
+ 'value' => ['_value' => 'Softwarecatalogus'],
];
- if (!isset($data['properties'])) {
+ if (isset($data['properties']) === false) {
$data['properties'] = ['property' => [$bronProp]];
- } elseif (isset($data['properties']['property'])) {
- if (isset($data['properties']['property']['_propertyDefinitionRef'])) {
- // Single property, convert to list
+ } else if (isset($data['properties']['property']) === true) {
+ if (isset($data['properties']['property']['_propertyDefinitionRef']) === true) {
+ // Single property, convert to list.
$data['properties']['property'] = [$data['properties']['property'], $bronProp];
} else {
$data['properties']['property'][] = $bronProp;
@@ -2292,7 +2515,7 @@ private function addBronProperty(array $data, string $bronPropDefId): array
}
return $data;
- }
+ }//end addBronProperty()
/**
* Walk the view node tree and inject application child nodes.
@@ -2302,31 +2525,32 @@ private function addBronProperty(array $data, string $bronPropDefId): array
*/
private function injectApplicationNodesInView(array $viewData, array $refCompApps): array
{
- // Inject into top-level nodes
- if (isset($viewData['node']) && is_array($viewData['node'])) {
+ // Inject into top-level nodes.
+ if (isset($viewData['node']) === true && is_array($viewData['node']) === true) {
$nodes = $viewData['node'];
- if (!$this->isList($nodes)) {
+ if ($this->isLis === falset(arr: $nodes)) {
$nodes = [$nodes];
}
- $newConnections = [];
- $viewData['node'] = $this->processNodesForInjection($nodes, $refCompApps, $newConnections);
+ $newConnections = [];
+ $viewData['node'] = $this->processNodesForInjection(nodes: $nodes, refCompApps: $refCompApps, newConnections: $newConnections);
- // Add connections to the view
- if (!empty($newConnections)) {
- if (!isset($viewData['connection'])) {
+ // Add connections to the view.
+ if (empty($newConnections) === false) {
+ if (isset($viewData['connection']) === false) {
$viewData['connection'] = [];
- } elseif (!$this->isList($viewData['connection'])) {
+ } else if ($this->isLis === falset(arr: $viewData['connection'])) {
$viewData['connection'] = [$viewData['connection']];
}
+
foreach ($newConnections as $conn) {
$viewData['connection'][] = $conn;
}
}
- }
+ }//end if
return $viewData;
- }
+ }//end injectApplicationNodesInView()
/**
* Recursively process nodes, injecting application child nodes where appropriate.
@@ -2334,26 +2558,28 @@ private function injectApplicationNodesInView(array $viewData, array $refCompApp
private function processNodesForInjection(array $nodes, array $refCompApps, array &$newConnections): array
{
foreach ($nodes as &$node) {
- if (!is_array($node)) continue;
+ if (is_array($node) === false) {
+ continue;
+ }
$elementRef = $node['_elementRef'] ?? $node['_attributes']['elementRef'] ?? null;
- if ($elementRef && isset($refCompApps[$elementRef])) {
- $apps = $refCompApps[$elementRef];
- $parentW = (int)($node['_w'] ?? $node['_attributes']['w'] ?? 120);
- $parentH = (int)($node['_h'] ?? $node['_attributes']['h'] ?? 80);
+ if ($elementRef !== false && isset($refCompApps[$elementRef]) === true) {
+ $apps = $refCompApps[$elementRef];
+ $parentW = (int) ($node['_w'] ?? $node['_attributes']['w'] ?? 120);
+ $parentH = (int) ($node['_h'] ?? $node['_attributes']['h'] ?? 80);
$parentIdentifier = $node['_identifier'] ?? $node['_attributes']['identifier'] ?? null;
- // Calculate child node positions
+ // Calculate child node positions.
$childW = max($parentW - 20, 40);
$childH = 18;
- $gap = 2;
+ $gap = 2;
$childX = 10;
- // Ensure nested nodes array
- if (!isset($node['node'])) {
+ // Ensure nested nodes array.
+ if (isset($node['node']) === false) {
$node['node'] = [];
- } elseif (!$this->isList($node['node'])) {
+ } else if ($this->isLis === falset(arr: $node['node'])) {
$node['node'] = [$node['node']];
}
@@ -2361,55 +2587,59 @@ private function processNodesForInjection(array $nodes, array $refCompApps, arra
foreach ($apps as $index => $app) {
$stackIndex = $existingChildCount + $index;
- $childY = $parentH - 5 - (($stackIndex + 1) * ($childH + $gap));
- if ($childY < 20) $childY = 20 + ($stackIndex * ($childH + $gap));
+ $childY = $parentH - 5 - (($stackIndex + 1) * ($childH + $gap));
+ if ($childY < 20) {
+ $childY = 20 + ($stackIndex * ($childH + $gap));
+ }
- $childNodeId = 'id-swc-node-' . $app['appIdentifier'] . '-' . str_replace('id-', '', $elementRef);
+ $childNodeId = 'id-swc-node-'.$app['appIdentifier'].'-'.str_replace('id-', '', $elementRef);
$childNode = [
'_identifier' => $childNodeId,
'_elementRef' => $app['appIdentifier'],
- '_xsi__type' => 'Element',
- '_x' => (string)$childX,
- '_y' => (string)max(20, $childY),
- '_w' => (string)$childW,
- '_h' => (string)$childH,
- 'style' => [
+ '_xsi__type' => 'Element',
+ '_x' => (string) $childX,
+ '_y' => (string) max(20, $childY),
+ '_w' => (string) $childW,
+ '_h' => (string) $childH,
+ 'style' => [
'fillColor' => ['_r' => '200', '_g' => '255', '_b' => '200', '_a' => '100'],
'lineColor' => ['_r' => '0', '_g' => '150', '_b' => '0'],
- 'font' => ['_name' => 'Segoe UI', '_size' => '9']
- ]
+ 'font' => ['_name' => 'Segoe UI', '_size' => '9'],
+ ],
];
$node['node'][] = $childNode;
- // Create connection for the relationship
- if ($parentIdentifier) {
- $connId = 'id-swc-conn-' . str_replace('id-swc-rel-', '', $app['relIdentifier']);
+ // Create connection for the relationship.
+ if (empty($parentIdentifier) === false) {
+ $connId = 'id-swc-conn-'.str_replace('id-swc-rel-', '', $app['relIdentifier']);
$newConnections[] = [
- '_identifier' => $connId,
+ '_identifier' => $connId,
'_relationshipRef' => $app['relIdentifier'],
- '_source' => $childNodeId,
- '_target' => $parentIdentifier,
- '_xsi__type' => 'Relationship',
+ '_source' => $childNodeId,
+ '_target' => $parentIdentifier,
+ '_xsi__type' => 'Relationship',
];
}
- }
- }
+ }//end foreach
+ }//end if
- // Recurse into nested nodes
- if (isset($node['node']) && is_array($node['node'])) {
+ // Recurse into nested nodes.
+ if (isset($node['node']) === true && is_array($node['node']) === true) {
$nestedNodes = $node['node'];
- if (!$this->isList($nestedNodes)) {
+ if ($this->isLis === falset(arr: $nestedNodes)) {
$nestedNodes = [$nestedNodes];
}
- $node['node'] = $this->processNodesForInjection($nestedNodes, $refCompApps, $newConnections);
+
+ $node['node'] = $this->processNodesForInjection(nodes: $nestedNodes, refCompApps: $refCompApps, newConnections: $newConnections);
}
- }
+ }//end foreach
+
unset($node);
return $nodes;
- }
+ }//end processNodesForInjection()
/**
* Build SWC organization folder items.
@@ -2424,46 +2654,50 @@ private function buildSwcOrganizationFolders(
): array {
$folders = [];
- // Typed application folders — only created when data exists
- if (!empty($gebruiktAppElements)) {
+ // Typed application folders — only created when data exists.
+ if (empty($gebruiktAppElements) === false) {
$items = [];
foreach ($gebruiktAppElements as $el) {
$items[] = ['_identifierRef' => $el['identifier']];
}
+
$folders[] = [
'label' => ['_value' => 'Gebruikt (Softwarecatalogus)'],
'items' => $items,
];
}
- if (!empty($deelnamesAppElements)) {
+ if (empty($deelnamesAppElements) === false) {
$items = [];
foreach ($deelnamesAppElements as $el) {
$items[] = ['_identifierRef' => $el['identifier']];
}
+
$folders[] = [
'label' => ['_value' => 'Deelnames (Softwarecatalogus)'],
'items' => $items,
];
}
- // Shared folders — always present when data exists
- if (!empty($relationships)) {
+ // Shared folders — always present when data exists.
+ if (empty($relationships) === false) {
$relItems = [];
foreach ($relationships as $rel) {
$relItems[] = ['_identifierRef' => $rel['identifier']];
}
+
$folders[] = [
'label' => ['_value' => 'Relaties (Softwarecatalogus)'],
'items' => $relItems,
];
}
- if (!empty($viewCopies)) {
+ if (empty($viewCopies) === false) {
$viewItems = [];
foreach ($viewCopies as $vc) {
$viewItems[] = ['_identifierRef' => $vc['identifier']];
}
+
$folders[] = [
'label' => ['_value' => 'Views (Softwarecatalogus)'],
'items' => $viewItems,
@@ -2471,7 +2705,7 @@ private function buildSwcOrganizationFolders(
}
return $folders;
- }
+ }//end buildSwcOrganizationFolders()
/**
* Assemble the final organization-specific ArchiMate XML.
@@ -2485,16 +2719,16 @@ private function assembleOrganizationXml(
array $swcFolders,
string $bronPropDefId
): string {
- // Extract model metadata
- $modelMetadata = $this->extractModelMetadata($baseObjects);
+ // Extract model metadata.
+ $modelMetadata = $this->extractModelMetadata(objects: $baseObjects);
$propertyDefinitionMap = $modelMetadata['propertyDefinitionMap'] ?? [];
- // Create base XML
- $xml = $this->createCleanArchiMateXml($modelMetadata);
+ // Create base XML.
+ $xml = $this->createCleanArchiMateXml(modelMetadata: $modelMetadata);
- // Override model name
- $modelName = 'Softwarecatalogus ' . $orgName;
- // Remove existing name children and add new one
+ // Override model name.
+ $modelName = 'Softwarecatalogus '.$orgName;
+ // Remove existing name children and add new one.
foreach ($xml->children() as $child) {
if ($child->getName() === 'name') {
$dom = dom_import_simplexml($child);
@@ -2502,61 +2736,67 @@ private function assembleOrganizationXml(
break;
}
}
+
$nameEl = $xml->addChild('name', htmlspecialchars($modelName));
$nameEl->addAttribute('xml:lang', 'nl', 'http://www.w3.org/XML/1998/namespace');
- // Organize base objects by section
+ // Organize base objects by section.
$objectsBySection = [];
foreach ($baseObjects as $object) {
- if (is_object($object) && method_exists($object, 'jsonSerialize')) {
+ if (is_object($object) === true && method_exists($object, 'jsonSerialize') === true) {
$object = $object->jsonSerialize();
}
+
$sectionName = $object['section'] ?? null;
- if ($sectionName) {
+ if (empty($sectionName) === false) {
$objectsBySection[$sectionName][] = $object;
}
}
- // --- Elements section ---
+ // --- Elements section ---.
$elementsFolder = $xml->addChild('elements');
$sectionMapping = ['element' => 'elements'];
foreach ($objectsBySection as $dbSection => $objects) {
if (($sectionMapping[$dbSection] ?? null) === 'elements') {
foreach ($objects as $obj) {
- if (is_object($obj) && method_exists($obj, 'jsonSerialize')) {
+ if (is_object($obj) === true && method_exists($obj, 'jsonSerialize') === true) {
$obj = $obj->jsonSerialize();
}
- $this->addObjectDirectlyToXmlWithProperties($elementsFolder, $obj, 'elements', $propertyDefinitionMap);
+
+ $this->addObjectDirectlyToXmlWithProperties(folder: $elementsFolder, object: $obj, sectionName: 'elements', propertyDefinitionMap: $propertyDefinitionMap);
}
}
}
- // Add SWC application elements
+
+ // Add SWC application elements.
foreach ($appElements as $appEl) {
$elNode = $elementsFolder->addChild('element');
$elNode->addAttribute('identifier', $appEl['identifier']);
$elNode->addAttribute('xsi:type', $appEl['xsi_type'], 'http://www.w3.org/2001/XMLSchema-instance');
$nameChild = $elNode->addChild('name', htmlspecialchars($appEl['name']));
$nameChild->addAttribute('xml:lang', 'nl', 'http://www.w3.org/XML/1998/namespace');
- // Add Bron property
+ // Add Bron property.
$propsEl = $elNode->addChild('properties');
- $propEl = $propsEl->addChild('property');
+ $propEl = $propsEl->addChild('property');
$propEl->addAttribute('propertyDefinitionRef', $appEl['bronPropDefId']);
$propEl->addChild('value', 'Softwarecatalogus');
}
- // --- Relationships section ---
+ // --- Relationships section ---.
$relsFolder = $xml->addChild('relationships');
foreach ($objectsBySection as $dbSection => $objects) {
if ($dbSection === 'relationship') {
foreach ($objects as $obj) {
- if (is_object($obj) && method_exists($obj, 'jsonSerialize')) {
+ if (is_object($obj) === true && method_exists($obj, 'jsonSerialize') === true) {
$obj = $obj->jsonSerialize();
}
- $this->addObjectDirectlyToXmlWithProperties($relsFolder, $obj, 'relationships', $propertyDefinitionMap);
+
+ $this->addObjectDirectlyToXmlWithProperties(folder: $relsFolder, object: $obj, sectionName: 'relationships', propertyDefinitionMap: $propertyDefinitionMap);
}
}
}
- // Add SWC relationships
+
+ // Add SWC relationships.
foreach ($relationships as $rel) {
$relNode = $relsFolder->addChild('relationship');
$relNode->addAttribute('identifier', $rel['identifier']);
@@ -2564,24 +2804,26 @@ private function assembleOrganizationXml(
$relNode->addAttribute('source', $rel['source']);
$relNode->addAttribute('target', $rel['target']);
$propsEl = $relNode->addChild('properties');
- $propEl = $propsEl->addChild('property');
+ $propEl = $propsEl->addChild('property');
$propEl->addAttribute('propertyDefinitionRef', $rel['bronPropDefId']);
$propEl->addChild('value', 'Softwarecatalogus');
}
- // --- Property Definitions section ---
+ // --- Property Definitions section ---.
$propDefsFolder = $xml->addChild('propertyDefinitions');
foreach ($objectsBySection as $dbSection => $objects) {
if ($dbSection === 'property_definition') {
foreach ($objects as $obj) {
- if (is_object($obj) && method_exists($obj, 'jsonSerialize')) {
+ if (is_object($obj) === true && method_exists($obj, 'jsonSerialize') === true) {
$obj = $obj->jsonSerialize();
}
- $this->addObjectDirectlyToXmlWithProperties($propDefsFolder, $obj, 'property_definitions', $propertyDefinitionMap);
+
+ $this->addObjectDirectlyToXmlWithProperties(folder: $propDefsFolder, object: $obj, sectionName: 'property_definitions', propertyDefinitionMap: $propertyDefinitionMap);
}
}
}
- // Add Bron property definition if we created it
+
+ // Add Bron property definition if we created it.
if ($bronPropDefId === 'id-swc-propdef-bron') {
$propDefNode = $propDefsFolder->addChild('propertyDefinition');
$propDefNode->addAttribute('identifier', $bronPropDefId);
@@ -2590,37 +2832,42 @@ private function assembleOrganizationXml(
$nameChild->addAttribute('xml:lang', 'nl', 'http://www.w3.org/XML/1998/namespace');
}
- // --- Organizations section ---
+ // --- Organizations section ---.
$orgsFolder = $xml->addChild('organizations');
- // Write existing organization tree
+ // Write existing organization tree.
foreach ($objectsBySection as $dbSection => $objects) {
if ($dbSection === 'organization') {
foreach ($objects as $obj) {
- if (is_object($obj) && method_exists($obj, 'jsonSerialize')) {
+ if (is_object($obj) === true && method_exists($obj, 'jsonSerialize') === true) {
$obj = $obj->jsonSerialize();
}
+
$xmlField = $obj['xml'] ?? [];
- if (isset($xmlField['item'])) {
+ if (isset($xmlField['item']) === true) {
$items = $xmlField['item'];
- if (!isset($items[0])) $items = [$items];
+ if (isset($items[0]) === false) {
+ $items = [$items];
+ }
+
foreach ($items as $itemData) {
- if (is_array($itemData)) {
+ if (is_array($itemData) === true) {
$itemNode = $orgsFolder->addChild('item');
- $this->addOrganizationItemToXml($itemNode, $itemData);
+ $this->addOrganizationItemToXml(itemNode: $itemNode, itemData: $itemData);
}
}
}
}
- }
- }
- // Add SWC folder: top-level folder named after organization, with sub-folders
- if (!empty($swcFolders)) {
- $orgFolder = $orgsFolder->addChild('item');
+ }//end if
+ }//end foreach
+
+ // Add SWC folder: top-level folder named after organization, with sub-folders.
+ if (empty($swcFolders) === false) {
+ $orgFolder = $orgsFolder->addChild('item');
$orgLabelEl = $orgFolder->addChild('label', htmlspecialchars($orgName));
$orgLabelEl->addAttribute('xml:lang', 'nl', 'http://www.w3.org/XML/1998/namespace');
foreach ($swcFolders as $folderData) {
$subFolder = $orgFolder->addChild('item');
- $labelEl = $subFolder->addChild('label', htmlspecialchars($folderData['label']['_value']));
+ $labelEl = $subFolder->addChild('label', htmlspecialchars($folderData['label']['_value']));
$labelEl->addAttribute('xml:lang', 'nl', 'http://www.w3.org/XML/1998/namespace');
foreach ($folderData['items'] as $identifierRefItem) {
$childItem = $subFolder->addChild('item');
@@ -2629,28 +2876,28 @@ private function assembleOrganizationXml(
}
}
- // --- Views section ---
- $viewsSection = $xml->addChild('views');
+ // --- Views section ---.
+ $viewsSection = $xml->addChild('views');
$diagramsFolder = $viewsSection->addChild('diagrams');
- // Write original views
+ // Write original views.
foreach ($objectsBySection as $dbSection => $objects) {
if ($dbSection === 'view') {
foreach ($objects as $obj) {
- if (is_object($obj) && method_exists($obj, 'jsonSerialize')) {
+ if (is_object($obj) === true && method_exists($obj, 'jsonSerialize') === true) {
$obj = $obj->jsonSerialize();
}
- $this->addObjectDirectlyToXmlWithProperties($diagramsFolder, $obj, 'views', $propertyDefinitionMap);
+
+ $this->addObjectDirectlyToXmlWithProperties(folder: $diagramsFolder, object: $obj, sectionName: 'views', propertyDefinitionMap: $propertyDefinitionMap);
}
}
}
- // Write enriched view copies
+
+ // Write enriched view copies.
foreach ($viewCopies as $vc) {
$viewNode = $diagramsFolder->addChild('view');
- $this->addViewDataToXmlNode($viewNode, $vc['xml']);
+ $this->addViewDataToXmlNode(viewNode: $viewNode, viewData: $vc['xml']);
}
- return $this->formatXmlOutput($xml->asXML());
- }
-}
-
-
+ return $this->formatXmlOutput(xmlString: $xml->asXML());
+ }//end assembleOrganizationXml()
+}//end class
diff --git a/lib/Service/ArchiMateImportService.php b/lib/Service/ArchiMateImportService.php
index 65979cbd..d3cd71b5 100644
--- a/lib/Service/ArchiMateImportService.php
+++ b/lib/Service/ArchiMateImportService.php
@@ -8,9 +8,8 @@
*
* @category Service
* @package OCA\SoftwareCatalog\Service
- * @author SoftwareCatalog Team
- * @license AGPL-3.0
- * @version 1.0.0
+ * @author SoftwareCatalog Team
+ * @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.en.html
* @link https://github.com/nextcloud/softwarecatalog
*/
@@ -40,9 +39,8 @@
*
* @category Service
* @package OCA\SoftwareCatalog\Service
- * @author SoftwareCatalog Team
- * @license AGPL-3.0
- * @version 1.0.0
+ * @author SoftwareCatalog Team
+ * @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.en.html
* @link https://github.com/nextcloud/softwarecatalog
*/
class ArchiMateImportService
@@ -51,27 +49,33 @@ class ArchiMateImportService
* Configuration keys for ArchiMate processing
*/
private const CONFIG_KEYS = [
- 'archimate_register_id' => 'archimate_register_id',
- 'archimate_schema_id' => 'archimate_schema_id',
- 'archimate_model_schema_id' => 'archimate_model_schema_id'
+ 'archimate_register_id' => 'archimate_register_id',
+ 'archimate_schema_id' => 'archimate_schema_id',
+ 'archimate_model_schema_id' => 'archimate_model_schema_id',
];
/**
* Performance optimization settings
*/
private const PERFORMANCE_OPTIMIZATIONS = [
- 'disable_validation' => true,
- 'disable_events' => true,
- 'disable_rbac' => false, // Keep RBAC for security
- 'use_multi' => true,
- 'xml_parse_flags' => LIBXML_NOCDATA | LIBXML_NONET,
- 'memory_cleanup' => true,
- 'parallel_processing' => true,
- 'batch_size' => 1000, // Default batch size (will be adjusted intelligently)
- 'parallel_batches' => 8, // Process 8 batches concurrently
- 'max_batch_size_bytes' => 8388608, // 8 MB - safe under MySQL's 16 MB limit
- 'min_batch_size' => 50, // Minimum batch size for very large objects
- 'size_estimation_sample' => 10 // Sample size for estimating object sizes
+ 'disable_validation' => true,
+ 'disable_events' => true,
+ 'disable_rbac' => false,
+ // Keep RBAC for security.
+ 'use_multi' => true,
+ 'xml_parse_flags' => LIBXML_NOCDATA | LIBXML_NONET,
+ 'memory_cleanup' => true,
+ 'parallel_processing' => true,
+ 'batch_size' => 1000,
+ // Default batch size (will be adjusted intelligently).
+ 'parallel_batches' => 8,
+ // Process 8 batches concurrently.
+ 'max_batch_size_bytes' => 8388608,
+ // 8 MB - safe under MySQL's 16 MB limit.
+ 'min_batch_size' => 50,
+ // Minimum batch size for very large objects.
+ 'size_estimation_sample' => 10,
+ // Sample size for estimating object sizes.
];
/**
@@ -103,7 +107,7 @@ class ArchiMateImportService
/**
* Flag to track if we've already logged finding a GEMMA type property
*
- * @var bool
+ * @var boolean
*/
private bool $gemmaTypePropertyFound = false;
@@ -115,26 +119,31 @@ class ArchiMateImportService
private ?array $propertyDefinitionMapCache = null;
/**
- * Storage for the last save operation results
- * Contains the structured return from ObjectService::saveObjects
+ * Storage for the last save operation results.
+ * Contains the structured return from ObjectService::saveObjects.
+ *
+ * @var array|null
*/
private ?array $lastSaveResult = null;
/**
- * Cached configuration values for performance optimization
+ * Cached configuration values for performance optimization.
+ *
+ * @var array|null
*/
private ?array $cachedConfig = null;
/**
* Constructor for ArchiMateImportService
*
- * @param IAppConfig $config Nextcloud app configuration service
- * @param IRootFolder $rootFolder Root folder service
- * @param IUserSession $userSession User session service
- * @param IAppManager $appManager App manager service
- * @param ContainerInterface $container PSR-11 container interface
- * @param LoggerInterface $logger Logger service
- * @param SettingsService $settingsService Settings service for AMEF configuration
+ * @param IAppConfig $config Nextcloud app configuration service
+ * @param IRootFolder $rootFolder Root folder service
+ * @param IUserSession $userSession User session service
+ * @param IAppManager $appManager App manager service
+ * @param ContainerInterface $container PSR-11 container interface
+ * @param LoggerInterface $logger Logger service
+ * @param SettingsService $settingsService Settings service for AMEF configuration.
+ * @param OrganisationService $organisationService Organisation service.
*/
public function __construct(
private readonly IAppConfig $config,
@@ -146,7 +155,7 @@ public function __construct(
private readonly SettingsService $settingsService,
private readonly OrganisationService $organisationService
) {
- }
+ }//end __construct()
/**
* Convert a SimpleXMLElement into a normalized associative array.
@@ -159,96 +168,110 @@ public function __construct(
* - Leaf node text is available as `_value`; when children exist alongside
* text, it is available as `_text`.
* - Repeated child nodes are represented as arrays.
+ *
+ * @param \SimpleXMLElement $xml The XML element to convert.
+ *
+ * @return array The normalized associative array.
*/
public function xmlToArray(\SimpleXMLElement $xml): array
{
- // PERFORMANCE OPTIMIZATION: Initialize result only
+ // PERFORMANCE OPTIMIZATION: Initialize result only.
$result = [];
- // OPTIMIZATION: Extract non-namespaced attributes (skip redundant processing)
+ // OPTIMIZATION: Extract non-namespaced attributes (skip redundant processing).
$attributes = $xml->attributes();
if (count($attributes) > 0) {
$attrBag = [];
foreach ($attributes as $attrName => $attrValue) {
- $name = (string) $attrName;
+ $name = (string) $attrName;
$value = (string) $attrValue;
- // OPTIMIZATION: Only create underscored key if needed (skip str_replace for simple names)
- $underscoredKey = (strpos($name, ':') !== false) ? '_' . str_replace(':', '__', $name) : '_' . $name;
+ // OPTIMIZATION: Only create underscored key if needed (skip str_replace for simple names).
+ if ((strpos($name, ':') !== false)) {
+ $underscoredKey = '_'.str_replace(':', '__', $name);
+ } else {
+ $underscoredKey = '_'.$name;
+ }
+
$result[$underscoredKey] = $value;
- $attrBag[$name] = $value;
+ $attrBag[$name] = $value;
}
+
$result['_attributes'] = $attrBag;
}
- // OPTIMIZATION: Extract namespaced attributes (simplified processing)
- foreach ($xml->getNameSpaces(true) as $prefix => $_) {
+ // OPTIMIZATION: Extract namespaced attributes (simplified processing).
+ foreach ($xml->getNameSpaces(true) as $prefix => $nsUri) {
$nsAttributes = $xml->attributes($prefix, true);
if (count($nsAttributes) > 0) {
foreach ($nsAttributes as $attrName => $attrValue) {
- $name = (string) $attrName;
- $value = (string) $attrValue;
- $underscoredKey = '_' . $prefix . '__' . $name;
+ $name = (string) $attrName;
+ $value = (string) $attrValue;
+ $underscoredKey = '_'.$prefix.'__'.$name;
$result[$underscoredKey] = $value;
- if (!isset($result['_attributes'])) {
+ if (isset($result['_attributes']) === false) {
$result['_attributes'] = [];
}
- $result['_attributes'][$prefix . ':' . $name] = $value;
+
+ $result['_attributes'][$prefix.':'.$name] = $value;
}
}
}
- // Extract children
+ // Extract children.
$children = $xml->children();
if (count($children) === 0) {
- // Leaf node: always return array shape for compatibility
+ // Leaf node: always return array shape for compatibility.
$text = trim((string) $xml);
if ($text !== '') {
$result['_value'] = $text;
}
+
return $result;
}
- // OPTIMIZATION: Process child elements with faster array operations
+ // OPTIMIZATION: Process child elements with faster array operations.
foreach ($children as $child) {
- $childName = $child->getName();
- $childValue = $this->xmlToArray($child);
+ $childName = $child->getName();
+ $childValue = $this->xmlToArray(xml: $child);
- // OPTIMIZATION: Use isset instead of array_key_exists (faster)
- if (!isset($result[$childName])) {
+ // OPTIMIZATION: Use isset instead of array_key_exists (faster).
+ if (isset($result[$childName]) === false) {
$result[$childName] = $childValue;
} else {
- // OPTIMIZATION: Fast array conversion without expensive isAssoc check
- if (!is_array($result[$childName]) || !isset($result[$childName][0])) {
- // Convert to indexed array if it's a single value or associative array
+ // OPTIMIZATION: Fast array conversion without expensive isAssoc check.
+ if (is_array($result[$childName]) === false || isset($result[$childName][0]) === false) {
+ // Convert to indexed array if it's a single value or associative array.
$result[$childName] = [$result[$childName]];
}
+
$result[$childName][] = $childValue;
}
}
- // Preserve text content when children exist
+ // Preserve text content when children exist.
$text = trim((string) $xml);
if ($text !== '') {
$result['_text'] = $text;
}
return $result;
- }
-
+ }//end xmlToArray()
/**
- * Check if an array is associative (has string keys)
+ * Check if an array is associative (has string keys).
+ *
+ * @param mixed $value The value to check.
*
- * @param array $array The array to check
* @return bool True if associative, false if indexed
*/
private function isAssoc(mixed $value): bool
{
- if (!is_array($value)) {
+ if (is_array($value) === false) {
return false;
}
+
return array_keys($value) !== range(0, count($value) - 1);
- }
+ }//end isAssoc()
/**
* OPTIMIZED: Import ArchiMate XML file using OpenRegister-style performance optimization
@@ -260,125 +283,126 @@ private function isAssoc(mixed $value): bool
*
* Expected performance: <1 minute for 8000 objects (vs current 13 minutes)
*
- * @param array $options Import options including file_path, fileName, etc.
+ * @param array $options Import options including file_path, fileName, etc.
+ *
* @return array Import results with detailed status
*/
- public function importArchiMateFileFromPathOptimized(array $options = []): array
+ public function importArchiMateFileFromPathOptimized(array $options=[]): array
{
- $startTime = microtime(true);
+ $startTime = microtime(true);
$startMemory = memory_get_usage(true);
- // DEBUG: Verify that the optimized import method is being called
+ // DEBUG: Verify that the optimized import method is being called.
$this->logger->info('GEMMA IMPORT DEBUG: Starting optimized import', $options);
-
- // Starting OPTIMIZED ArchiMate XML import
-
+ // Starting OPTIMIZED ArchiMate XML import.
try {
- // OPTIMIZATION: Cache all configuration once at start
+ // OPTIMIZATION: Cache all configuration once at start.
$cacheStartTime = microtime(true);
$this->initializeCache();
$cacheTime = microtime(true) - $cacheStartTime;
- // Cache initialization completed
-
- // STEP 1: Parse XML to array (same as before)
+ // Cache initialization completed.
+ // STEP 1: Parse XML to array (same as before).
$filePath = $options['filePath'] ?? $options['file_path'] ?? '';
- if (empty($filePath) || !file_exists($filePath)) {
+ if (empty($filePath) === true || file_exists($filePath) === false) {
throw new \InvalidArgumentException("File not found: {$filePath}");
}
$parseStartTime = microtime(true);
- $xmlData = $this->parseArchiMateXml($filePath);
- $parseTime = microtime(true) - $parseStartTime;
+ $xmlData = $this->parseArchiMateXml(filePath: $filePath);
+ $parseTime = microtime(true) - $parseStartTime;
- // PERFORMANCE OPTIMIZATION: Clean up memory after XML parsing
+ // PERFORMANCE OPTIMIZATION: Clean up memory after XML parsing.
$memoryCleanupTime = 0;
- if (self::PERFORMANCE_OPTIMIZATIONS['memory_cleanup']) {
+ if (self::PERFORMANCE_OPTIMIZATIONS['memory_cleanup'] !== false) {
$memoryCleanupStartTime = microtime(true);
$this->cleanupMemory();
$memoryCleanupTime = microtime(true) - $memoryCleanupStartTime;
}
- // STEP 2: Extract model identifier
+ // STEP 2: Extract model identifier.
$modelIdentifierStartTime = microtime(true);
- $modelIdentifier = $this->extractModelIdentifier($xmlData);
- $modelIdentifierTime = microtime(true) - $modelIdentifierStartTime;
+ $modelIdentifier = $this->extractModelIdentifier(xmlData: $xmlData);
+ $modelIdentifierTime = microtime(true) - $modelIdentifierStartTime;
- // STEP 3: Parse ALL objects in one go (like CSV import)
+ // STEP 3: Parse ALL objects in one go (like CSV import).
$transformStartTime = microtime(true);
- $allObjects = $this->transformArchiMateXmlToObjectsBatch($xmlData, $modelIdentifier);
- $transformTime = microtime(true) - $transformStartTime;
+ $allObjects = $this->transformArchiMateXmlToObjectsBatch(xmlData: $xmlData, modelIdentifier: $modelIdentifier);
+ $transformTime = microtime(true) - $transformStartTime;
- // Parsed and transformed all objects
-
- // STEP 4: Single saveObjects() call (like CSV import)
+ // Parsed and transformed all objects.
+ // STEP 4: Single saveObjects() call (like CSV import).
$saveStartTime = microtime(true);
- $this->logger->info('GEMMA IMPORT DEBUG: About to save objects to database', [
- 'object_count' => count($allObjects)
- ]);
- $savedObjects = $this->saveObjectsToDatabase($allObjects);
- $saveTime = microtime(true) - $saveStartTime;
+ $this->logger->info(
+ 'GEMMA IMPORT DEBUG: About to save objects to database',
+ [
+ 'object_count' => count($allObjects),
+ ]
+ );
+ $savedObjects = $this->saveObjectsToDatabase(objects: $allObjects);
+ $saveTime = microtime(true) - $saveStartTime;
- // Capture detailed save timing from internal tracking
+ // Capture detailed save timing from internal tracking.
$saveBreakdown = $this->lastSaveTimingBreakdown;
- $totalTime = microtime(true) - $startTime;
+ $totalTime = microtime(true) - $startTime;
$itemsPerSecond = count($allObjects) / max($totalTime, 0.001);
// Use statistics directly from saveObjects result (stored in lastSaveResult).
// No need for custom calculation since ObjectService already provides accurate stats.
- $statistics = $this->buildStatisticsFromSaveResult();
- $detailedErrors = $this->extractDetailedErrors($statistics);
-
- // OPTIMIZED import completed successfully
+ $statistics = $this->buildStatisticsFromSaveResult();
+ $detailedErrors = $this->extractDetailedErrors(statistics: $statistics);
+ // OPTIMIZED import completed successfully.
return [
- 'success' => true,
- 'file_info' => [
+ 'success' => true,
+ 'file_info' => [
'name' => $options['fileName'] ?? basename($filePath),
- 'size' => filesize($filePath)
+ 'size' => filesize($filePath),
],
'performance_metrics' => [
- 'total_time_seconds' => round($totalTime, 3),
- 'items_per_second' => round($itemsPerSecond, 1),
- 'objects_processed' => count($allObjects),
- 'timing_breakdown' => [
- 'cache_initialization_seconds' => round($cacheTime, 3),
- 'xml_parsing_seconds' => round($parseTime, 3),
- 'memory_cleanup_seconds' => round($memoryCleanupTime, 3),
+ 'total_time_seconds' => round($totalTime, 3),
+ 'items_per_second' => round($itemsPerSecond, 1),
+ 'objects_processed' => count($allObjects),
+ 'timing_breakdown' => [
+ 'cache_initialization_seconds' => round($cacheTime, 3),
+ 'xml_parsing_seconds' => round($parseTime, 3),
+ 'memory_cleanup_seconds' => round($memoryCleanupTime, 3),
'model_identifier_extraction_seconds' => round($modelIdentifierTime, 3),
- 'data_transformation_seconds' => round($transformTime, 3),
- 'database_save_seconds' => round($saveTime, 3)
+ 'data_transformation_seconds' => round($transformTime, 3),
+ 'database_save_seconds' => round($saveTime, 3),
],
'save_operation_breakdown' => $saveBreakdown,
- 'memory_usage' => [
- 'start_memory_mb' => round($startMemory / 1024 / 1024, 2),
+ 'memory_usage' => [
+ 'start_memory_mb' => round($startMemory / 1024 / 1024, 2),
'current_memory_mb' => round(memory_get_usage(true) / 1024 / 1024, 2),
- 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2)
+ 'peak_memory_mb' => round(memory_get_peak_usage(true) / 1024 / 1024, 2),
],
- 'processing_rates' => [
+ 'processing_rates' => [
'xml_parse_objects_per_second' => round(count($allObjects) / max($parseTime, 0.001), 1),
'transform_objects_per_second' => round(count($allObjects) / max($transformTime, 0.001), 1),
- 'save_objects_per_second' => round(count($allObjects) / max($saveTime, 0.001), 1)
- ]
+ 'save_objects_per_second' => round(count($allObjects) / max($saveTime, 0.001), 1),
+ ],
],
- 'statistics' => $statistics,
- 'detailed_errors' => $detailedErrors
+ 'statistics' => $statistics,
+ 'detailed_errors' => $detailedErrors,
];
-
} catch (\Exception $e) {
- $this->logger->error('OPTIMIZED ArchiMate import failed', [
- 'error' => $e->getMessage(),
- 'file_path' => $options['file_path'] ?? 'unknown'
- ]);
+ $this->logger->error(
+ 'OPTIMIZED ArchiMate import failed',
+ [
+ 'error' => $e->getMessage(),
+ 'file_path' => $options['file_path'] ?? 'unknown',
+ ]
+ );
return [
'success' => false,
- 'error' => $e->getMessage()
+ 'error' => $e->getMessage(),
];
- }
- }
+ }//end try
+ }//end importArchiMateFileFromPathOptimized()
/**
* Import ArchiMate XML file from path with model detection and round-trip fidelity
@@ -390,180 +414,191 @@ public function importArchiMateFileFromPathOptimized(array $options = []): array
* 4. Convert to OpenRegister objects with proper @self structure
* 5. Save objects using ObjectService::saveObjects
*
- * @param array $options Import options including file_path, fileName, etc.
+ * @param array $options Import options including file_path, fileName, etc.
+ *
* @return array Import results with detailed status
*/
- public function importArchiMateFileFromPath(array $options = []): array
+ public function importArchiMateFileFromPath(array $options=[]): array
{
- // Track start time and memory for performance metrics
- $startTime = microtime(true);
+ // Track start time and memory for performance metrics.
+ $startTime = microtime(true);
$startMemory = memory_get_usage(true);
- // OPTIMIZATION: Cache configuration values once at the start
+ // OPTIMIZATION: Cache configuration values once at the start.
$this->initializeCache();
- // Starting ArchiMate XML import with model detection
-
+ // Starting ArchiMate XML import with model detection.
try {
- // STEP 1: Parse XML to array using the specialized import service
- // This captures ALL possible XML values including attributes, text content, and nested elements
+ // STEP 1: Parse XML to array using the specialized import service.
+ // This captures ALL possible XML values including attributes, text content, and nested elements.
$filePath = $options['filePath'] ?? $options['file_path'] ?? '';
- if (empty($filePath)) {
+ if (empty($filePath) === true) {
throw new \InvalidArgumentException('File path is required for import');
}
- if (!file_exists($filePath)) {
+ if (file_exists($filePath) === false) {
throw new \InvalidArgumentException("File not found: {$filePath}");
}
$this->logger->info('Step 1: Parsing XML to array for complete data capture', ['filePath' => $filePath]);
$parseStartTime = microtime(true);
- $xmlData = $this->parseArchiMateXml($filePath);
- $parseTime = microtime(true) - $parseStartTime;
+ $xmlData = $this->parseArchiMateXml(filePath: $filePath);
+ $parseTime = microtime(true) - $parseStartTime;
- // STEP 2: Extract model identifier and detect if model already exists
- // This is critical for determining whether to create new or update existing model
+ // STEP 2: Extract model identifier and detect if model already exists.
+ // This is critical for determining whether to create new or update existing model.
$this->logger->info('Step 2: Extracting model identifier and checking for existing model');
$validationStartTime = microtime(true);
- $modelIdentifier = $this->extractModelIdentifier($xmlData);
- $modelExists = $this->checkIfModelExists($modelIdentifier);
- $validationTime = microtime(true) - $validationStartTime;
+ $modelIdentifier = $this->extractModelIdentifier(xmlData: $xmlData);
+ $modelExists = $this->checkIfModelExists(modelIdentifier: $modelIdentifier);
+ $validationTime = microtime(true) - $validationStartTime;
- // STEP 3: Normalize data structure for storage as JSON blob
- // Store complete raw XML data for exact round-trip fidelity during export
+ // STEP 3: Normalize data structure for storage as JSON blob.
+ // Store complete raw XML data for exact round-trip fidelity during export.
$this->logger->info('Step 3: Normalizing data structure for JSON blob storage');
- $normalizedData = $this->normalizeArchiMateData($xmlData, $modelIdentifier);
+ $normalizedData = $this->normalizeArchiMateData(data: $xmlData, modelIdentifier: $modelIdentifier);
- // STEP 4: Convert to OpenRegister objects with proper @self structure
- // Each object must have @self with register, schema, and id for ObjectService::saveObjects
+ // STEP 4: Convert to OpenRegister objects with proper @self structure.
+ // Each object must have @self with register, schema, and id for ObjectService::saveObjects.
$this->logger->info('Step 4: Converting to OpenRegister objects with @self structure');
$convertStartTime = microtime(true);
- $objects = $this->convertToOpenRegisterObjects($normalizedData, $modelIdentifier);
- $convertTime = microtime(true) - $convertStartTime;
+ $objects = $this->convertToOpenRegisterObjects(normalizedData: $normalizedData, modelIdentifier: $modelIdentifier);
+ $convertTime = microtime(true) - $convertStartTime;
- // STEP 5: Save objects using ObjectService::saveObjects
- // This handles the actual database persistence with proper validation
+ // STEP 5: Save objects using ObjectService::saveObjects.
+ // This handles the actual database persistence with proper validation.
$this->logger->info('Step 5: Saving objects to database using ObjectService::saveObjects');
- $savedObjects = $this->saveObjectsToDatabase($objects);
+ $savedObjects = $this->saveObjectsToDatabase(objects: $objects);
- // Calculate total time and memory usage
- $totalTime = microtime(true) - $startTime;
- $endMemory = memory_get_usage(true);
+ // Calculate total time and memory usage.
+ $totalTime = microtime(true) - $startTime;
+ $endMemory = memory_get_usage(true);
$peakMemory = memory_get_peak_usage(true);
- // Count objects by type for detailed statistics
- $statistics = $this->calculateObjectStatistics($normalizedData, $savedObjects);
+ // Count objects by type for detailed statistics.
+ $statistics = $this->calculateObjectStatistics(normalizedData: $normalizedData, savedObjects: $savedObjects);
- // Calculate performance metrics
+ // Calculate performance metrics.
$totalObjects = $statistics['summary']['total_objects_created'] + $statistics['summary']['total_objects_updated'];
- $itemsPerSecond = $totalObjects > 0 ? $totalObjects / $totalTime : 0;
+ if ($totalObjects > 0) {
+ $itemsPerSecond = $totalObjects / $totalTime;
+ } else {
+ $itemsPerSecond = 0;
+ }
- // Extract detailed error information from statistics
- $detailedErrors = $this->extractDetailedErrors($statistics);
+ // Extract detailed error information from statistics.
+ $detailedErrors = $this->extractDetailedErrors(statistics: $statistics);
- // Prepare comprehensive result with detailed information
+ // Prepare comprehensive result with detailed information.
$result = [
- 'success' => true,
- 'file_info' => [
- 'name' => $options['fileName'] ?? basename($filePath),
- 'size' => filesize($filePath),
- 'mime_type' => $options['mimeType'] ?? 'text/xml'
+ 'success' => true,
+ 'file_info' => [
+ 'name' => $options['fileName'] ?? basename($filePath),
+ 'size' => filesize($filePath),
+ 'mime_type' => $options['mimeType'] ?? 'text/xml',
],
- 'processing_times' => [
- 'total_time_seconds' => round($totalTime, 3),
+ 'processing_times' => [
+ 'total_time_seconds' => round($totalTime, 3),
'validation_time_seconds' => round($validationTime, 3),
- 'parse_time_seconds' => round($parseTime, 3),
- 'convert_time_seconds' => round($convertTime, 3),
- 'performance_breakdown' => [
+ 'parse_time_seconds' => round($parseTime, 3),
+ 'convert_time_seconds' => round($convertTime, 3),
+ 'performance_breakdown' => [
'validation_percent' => round(($validationTime / $totalTime) * 100, 1),
- 'parse_percent' => round(($parseTime / $totalTime) * 100, 1),
- 'convert_percent' => round(($convertTime / $totalTime) * 100, 1)
- ]
+ 'parse_percent' => round(($parseTime / $totalTime) * 100, 1),
+ 'convert_percent' => round(($convertTime / $totalTime) * 100, 1),
+ ],
],
- 'memory_usage' => [
- 'start_mb' => round($startMemory / 1024 / 1024, 1),
- 'end_mb' => round($endMemory / 1024 / 1024, 1),
- 'peak_mb' => round($peakMemory / 1024 / 1024, 2),
- 'total_used_mb' => round(($endMemory - $startMemory) / 1024 / 1024, 1)
+ 'memory_usage' => [
+ 'start_mb' => round($startMemory / 1024 / 1024, 1),
+ 'end_mb' => round($endMemory / 1024 / 1024, 1),
+ 'peak_mb' => round($peakMemory / 1024 / 1024, 2),
+ 'total_used_mb' => round(($endMemory - $startMemory) / 1024 / 1024, 1),
],
- 'statistics' => $statistics,
- 'summary' => [
+ 'statistics' => $statistics,
+ 'summary' => [
'total_objects_created' => $statistics['summary']['total_objects_created'],
'total_objects_updated' => $statistics['summary']['total_objects_updated'],
'total_objects_deleted' => $statistics['summary']['total_objects_deleted'],
'total_objects_skipped' => $statistics['summary']['total_objects_skipped'],
- 'total_errors' => $statistics['summary']['total_errors']
+ 'total_errors' => $statistics['summary']['total_errors'],
],
'performance_metrics' => [
- 'items_per_second' => round($itemsPerSecond, 2),
+ 'items_per_second' => round($itemsPerSecond, 2),
'processing_method' => 'synchronous_batch_processing',
- 'batch_size_used' => 100,
- 'dataset_size' => $totalObjects
+ 'batch_size_used' => 100,
+ 'dataset_size' => $totalObjects,
],
- 'detailed_errors' => $detailedErrors
+ 'detailed_errors' => $detailedErrors,
];
- $this->logger->info('ArchiMate XML import completed successfully', [
- 'model_identifier' => $modelIdentifier,
- 'model_exists' => $modelExists,
- 'imported_objects' => $totalObjects,
- 'round_trip_fidelity' => 'enabled'
- ]);
+ $this->logger->info(
+ 'ArchiMate XML import completed successfully',
+ [
+ 'model_identifier' => $modelIdentifier,
+ 'model_exists' => $modelExists,
+ 'imported_objects' => $totalObjects,
+ 'round_trip_fidelity' => 'enabled',
+ ]
+ );
return $result;
-
} catch (\Exception $e) {
- $this->logger->error('ArchiMate XML import failed', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString(),
- 'file_path' => $options['filePath'] ?? $options['file_path'] ?? 'unknown'
- ]);
+ $this->logger->error(
+ 'ArchiMate XML import failed',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ 'file_path' => $options['filePath'] ?? $options['file_path'] ?? 'unknown',
+ ]
+ );
return [
- 'success' => false,
- 'message' => 'Import failed: ' . $e->getMessage(),
- 'error' => $e->getMessage(),
- 'step_failed' => 'unknown' // Will be refined with better error tracking
+ 'success' => false,
+ 'message' => 'Import failed: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
+ 'step_failed' => 'unknown',
+ // Will be refined with better error tracking.
];
- }
- }
+ }//end try
+ }//end importArchiMateFileFromPath()
/**
* Parse ArchiMate XML file to array using the import service
*
- * @param string $filePath Path to XML file
+ * @param string $filePath Path to XML file
+ *
* @return array Parsed XML data
*/
private function parseArchiMateXml(string $filePath): array
{
- if (!file_exists($filePath)) {
+ if (file_exists($filePath) === false) {
throw new \InvalidArgumentException("File not found: {$filePath}");
}
- // PERFORMANCE OPTIMIZATION: Use more efficient XML parsing
+ // PERFORMANCE OPTIMIZATION: Use more efficient XML parsing.
$xmlContent = file_get_contents($filePath);
if ($xmlContent === false) {
throw new \RuntimeException("Failed to read file: {$filePath}");
}
- // PERFORMANCE OPTIMIZATION: Disable external entity loading for security and speed
+ // PERFORMANCE OPTIMIZATION: Disable external entity loading for security and speed.
$previousValue = libxml_disable_entity_loader(true);
try {
- // PERFORMANCE OPTIMIZATION: Use LIBXML_NOCDATA for faster parsing
- $xml = new SimpleXMLElement($xmlContent, LIBXML_NOCDATA | LIBXML_NONET);
- $result = $this->xmlToArray($xml);
+ // PERFORMANCE OPTIMIZATION: Use LIBXML_NOCDATA for faster parsing.
+ $xml = new SimpleXMLElement($xmlContent, LIBXML_NOCDATA | LIBXML_NONET);
+ $result = $this->xmlToArray(xml: $xml);
- // PERFORMANCE OPTIMIZATION: Clear XML object from memory immediately
+ // PERFORMANCE OPTIMIZATION: Clear XML object from memory immediately.
unset($xml);
return $result;
} finally {
- // Restore previous entity loader setting
+ // Restore previous entity loader setting.
libxml_disable_entity_loader($previousValue);
}
- }
+ }//end parseArchiMateXml()
/**
* Extract model identifier from parsed XML data
@@ -573,120 +608,148 @@ private function parseArchiMateXml(string $filePath): array
* 2. Model element attributes
* 3. Fallback to generated identifier if none found
*
- * @param array $xmlData Parsed XML data array
+ * @param array $xmlData Parsed XML data array
+ *
* @return string Model identifier for tracking and storage
*/
private function extractModelIdentifier(array $xmlData): string
{
- $this->logger->debug('Extracting model identifier from XML data', [
- 'xml_keys' => array_keys($xmlData)
- ]);
+ $this->logger->debug(
+ 'Extracting model identifier from XML data',
+ [
+ 'xml_keys' => array_keys($xmlData),
+ ]
+ );
- // STEP 1: Try to find identifier in root attributes (most common location)
- if (isset($xmlData['_attributes']['identifier'])) {
+ // STEP 1: Try to find identifier in root attributes (most common location).
+ if (isset($xmlData['_attributes']['identifier']) === true) {
$modelId = $xmlData['_attributes']['identifier'];
- $this->logger->info('Found model identifier in root attributes', [
- 'identifier' => $modelId
- ]);
+ $this->logger->info(
+ 'Found model identifier in root attributes',
+ [
+ 'identifier' => $modelId,
+ ]
+ );
return $modelId;
}
- // STEP 2: Look for model element with identifier
- if (isset($xmlData['model']) && is_array($xmlData['model'])) {
- if (isset($xmlData['model']['_attributes']['identifier'])) {
+ // STEP 2: Look for model element with identifier.
+ if (isset($xmlData['model']) === true && is_array($xmlData['model']) === true) {
+ if (isset($xmlData['model']['_attributes']['identifier']) === true) {
$modelId = $xmlData['model']['_attributes']['identifier'];
- $this->logger->info('Found model identifier in model element attributes', [
- 'identifier' => $modelId
- ]);
+ $this->logger->info(
+ 'Found model identifier in model element attributes',
+ [
+ 'identifier' => $modelId,
+ ]
+ );
return $modelId;
}
}
- // STEP 3: Look for archimate:model namespace (ArchiMate Tool format)
- if (isset($xmlData['archimate:model']) && is_array($xmlData['archimate:model'])) {
- if (isset($xmlData['archimate:model']['_attributes']['identifier'])) {
+ // STEP 3: Look for archimate:model namespace (ArchiMate Tool format).
+ if (isset($xmlData['archimate:model']) === true && is_array($xmlData['archimate:model']) === true) {
+ if (isset($xmlData['archimate:model']['_attributes']['identifier']) === true) {
$modelId = $xmlData['archimate:model']['_attributes']['identifier'];
- $this->logger->info('Found model identifier in archimate:model namespace', [
- 'identifier' => $modelId
- ]);
+ $this->logger->info(
+ 'Found model identifier in archimate:model namespace',
+ [
+ 'identifier' => $modelId,
+ ]
+ );
return $modelId;
}
}
- // STEP 4: Generate fallback identifier if none found
- $fallbackId = 'model-' . uniqid() . '-' . time();
- $this->logger->warning('No model identifier found, generating fallback', [
- 'fallback_id' => $fallbackId
- ]);
+ // STEP 4: Generate fallback identifier if none found.
+ $fallbackId = 'model-'.uniqid().'-'.time();
+ $this->logger->warning(
+ 'No model identifier found, generating fallback',
+ [
+ 'fallback_id' => $fallbackId,
+ ]
+ );
return $fallbackId;
- }
+ }//end extractModelIdentifier()
/**
* Check if a model already exists in the database
*
- * @param string $modelIdentifier The model identifier to check
+ * @param string $modelIdentifier The model identifier to check
+ *
* @return bool True if model exists, false otherwise
*/
private function checkIfModelExists(string $modelIdentifier): bool
{
- $this->logger->debug('Checking if model already exists', [
- 'model_identifier' => $modelIdentifier
- ]);
+ $this->logger->debug(
+ 'Checking if model already exists',
+ [
+ 'model_identifier' => $modelIdentifier,
+ ]
+ );
try {
- // Get ObjectService to query existing objects
+ // Get ObjectService to query existing objects.
$objectService = $this->getObjectService();
- if (!$objectService) {
+ if ($objectService === null) {
$this->logger->warning('ObjectService not available, assuming new model');
return false;
}
- // Get AMEF configuration for register and schema IDs
+ // Get AMEF configuration for register and schema IDs.
$registerId = $this->getAmefRegisterId();
- $schemaId = $this->getAmefSchemaIdForType('model');
-
- if (!$registerId || !$schemaId) {
- $this->logger->warning('AMEF register or model schema not configured, assuming new model', [
- 'registerId' => $registerId,
- 'schemaId' => $schemaId
- ]);
+ $schemaId = $this->getAmefSchemaIdForType(archiMateType: 'model');
+
+ if ($registerId === null || $schemaId === false) {
+ $this->logger->warning(
+ 'AMEF register or model schema not configured, assuming new model',
+ [
+ 'registerId' => $registerId,
+ 'schemaId' => $schemaId,
+ ]
+ );
return false;
}
- // Query for existing model objects with this identifier
- // Use searchObjects with @self structure for proper querying
+ // Query for existing model objects with this identifier.
+ // Use searchObjects with @self structure for proper querying.
$query = [
- '@self' => [
+ '@self' => [
'register' => $registerId,
- 'schema' => $schemaId
+ 'schema' => $schemaId,
],
- 'archimate_id' => $modelIdentifier
+ 'archimate_id' => $modelIdentifier,
];
$existingModels = $objectService->searchObjects($query);
- $exists = !empty($existingModels);
+ $exists = empty($existingModels) === false;
- $this->logger->info('Model existence check completed', [
- 'model_identifier' => $modelIdentifier,
- 'exists' => $exists,
- 'found_count' => count($existingModels),
- 'registerId' => $registerId,
- 'schemaId' => $schemaId
- ]);
+ $this->logger->info(
+ 'Model existence check completed',
+ [
+ 'model_identifier' => $modelIdentifier,
+ 'exists' => $exists,
+ 'found_count' => count($existingModels),
+ 'registerId' => $registerId,
+ 'schemaId' => $schemaId,
+ ]
+ );
return $exists;
-
} catch (\Exception $e) {
- $this->logger->error('Error checking model existence', [
- 'model_identifier' => $modelIdentifier,
- 'error' => $e->getMessage()
- ]);
- // If we can't check, assume new model to avoid data loss
+ $this->logger->error(
+ 'Error checking model existence',
+ [
+ 'model_identifier' => $modelIdentifier,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ // If we can't check, assume new model to avoid data loss.
return false;
- }
- }
+ }//end try
+ }//end checkIfModelExists()
/**
* Normalize ArchiMate data structure for storage as JSON blob
@@ -697,229 +760,258 @@ private function checkIfModelExists(string $modelIdentifier): bool
* 3. Stores complete raw XML data for each item to ensure round-trip fidelity
* 4. Adds model identifier to each item for proper linking
*
- * @param array $data Raw parsed XML data from import service
- * @param string $modelIdentifier The model identifier for linking items
+ * @param array $data Raw parsed XML data from import service
+ * @param string $modelIdentifier The model identifier for linking items
+ *
* @return array Normalized data structure ready for database storage
*/
private function normalizeArchiMateData(array $data, string $modelIdentifier): array
{
- $this->logger->info('Normalizing ArchiMate data structure for JSON blob storage', [
- 'model_identifier' => $modelIdentifier
- ]);
+ $this->logger->info(
+ 'Normalizing ArchiMate data structure for JSON blob storage',
+ [
+ 'model_identifier' => $modelIdentifier,
+ ]
+ );
- // STEP 0: Extract propertyDefinition map and store in model metadata
- $propertyDefinitionMap = $this->extractPropertyDefinitionMap($data);
+ // STEP 0: Extract propertyDefinition map and store in model metadata.
+ $propertyDefinitionMap = $this->extractPropertyDefinitionMap(data: $data);
- // Log property mapping for debugging
- if (!empty($propertyDefinitionMap)) {
- $this->logger->info('Property definitions extracted and mapped', [
- 'total_properties' => count($propertyDefinitionMap),
- 'property_mapping' => $this->getPropertyNameMapping($propertyDefinitionMap)
- ]);
+ // Log property mapping for debugging.
+ if (empty($propertyDefinitionMap) === false) {
+ $this->logger->info(
+ 'Property definitions extracted and mapped',
+ [
+ 'total_properties' => count($propertyDefinitionMap),
+ 'property_mapping' => $this->getPropertyNameMapping(propertyDefinitionMap: $propertyDefinitionMap),
+ ]
+ );
}
- // Initialize normalized structure with model metadata
+ // Initialize normalized structure with model metadata.
$normalized = [
- 'model_metadata' => [],
- 'model_identifier' => $modelIdentifier, // Add model identifier for linking
- 'elements' => [],
- 'relationships' => [],
- 'organizations' => [],
- 'views' => [],
- 'property_definitions' => []
+ 'model_metadata' => [],
+ 'model_identifier' => $modelIdentifier,
+ // Add model identifier for linking.
+ 'elements' => [],
+ 'relationships' => [],
+ 'organizations' => [],
+ 'views' => [],
+ 'property_definitions' => [],
];
- // STEP 1: Extract and store model metadata
- if (isset($data['_attributes'])) {
+ // STEP 1: Extract and store model metadata.
+ if (isset($data['_attributes']) === true) {
$normalized['model_metadata'] = $data['_attributes'];
}
- // Also extract name and documentation from root level
- if (isset($data['name'])) {
+
+ // Also extract name and documentation from root level.
+ if (isset($data['name']) === true) {
$normalized['model_metadata']['name'] = $data['name'];
}
- if (isset($data['documentation'])) {
+
+ if (isset($data['documentation']) === true) {
$normalized['model_metadata']['documentation'] = $data['documentation'];
}
- if (isset($data['properties'])) {
+
+ if (isset($data['properties']) === true) {
$normalized['model_metadata']['properties'] = $data['properties'];
}
- // Store propertyDefinitionMap in model_metadata
+
+ // Store propertyDefinitionMap in model_metadata.
$normalized['model_metadata']['propertyDefinitionMap'] = $propertyDefinitionMap;
- $this->logger->debug('Extracted model metadata', [
- 'metadata_keys' => array_keys($normalized['model_metadata']),
- 'has_name' => isset($normalized['model_metadata']['name']),
- 'has_documentation' => isset($normalized['model_metadata']['documentation'])
- ]);
+ $this->logger->debug(
+ 'Extracted model metadata',
+ [
+ 'metadata_keys' => array_keys($normalized['model_metadata']),
+ 'has_name' => isset($normalized['model_metadata']['name']) === true,
+ 'has_documentation' => isset($normalized['model_metadata']['documentation']) === true,
+ ]
+ );
- // STEP 2: Process each section and store complete raw XML data
- $sections = ['elements', 'relationships', 'organizations', 'views', 'property_definitions'];
+ // STEP 2: Process each section and store complete raw XML data.
+ $sections = ['elements', 'relationships', 'organizations', 'views', 'property_definitions'];
$alternativeNames = [
- 'views' => ['views', 'diagrams'],
- 'organizations' => ['organizations', 'organisation'],
- 'property_definitions' => ['propertyDefinitions', 'property_definitions', 'propertydefinitions']
+ 'views' => ['views', 'diagrams'],
+ 'organizations' => ['organizations', 'organisation'],
+ 'property_definitions' => ['propertyDefinitions', 'property_definitions', 'propertydefinitions'],
];
foreach ($sections as $section) {
- $sectionData = null;
+ $sectionData = null;
$actualSectionName = null;
- if (isset($data[$section])) {
- $sectionData = $data[$section];
+ if (isset($data[$section]) === true) {
+ $sectionData = $data[$section];
$actualSectionName = $section;
} else {
- if (isset($alternativeNames[$section])) {
+ if (isset($alternativeNames[$section]) === true) {
foreach ($alternativeNames[$section] as $altName) {
- if (isset($data[$altName])) {
- $sectionData = $data[$altName];
+ if (isset($data[$altName]) === true) {
+ $sectionData = $data[$altName];
$actualSectionName = $altName;
break;
}
}
}
}
+
if ($sectionData !== null) {
// Organizations are hierarchical folder trees, not flat objects with identifiers.
// Store the entire tree as one raw entry so round-trip export can reconstruct it.
if ($section === 'organizations') {
- $syntheticId = 'org-' . preg_replace('/^id-/', '', $modelIdentifier);
+ $syntheticId = 'org-'.preg_replace('/^id-/', '', $modelIdentifier);
$normalized[$section][$syntheticId] = [
- 'identifier' => $syntheticId,
- 'section' => 'organization',
+ 'identifier' => $syntheticId,
+ 'section' => 'organization',
'model_identifier' => $modelIdentifier,
- 'name' => 'Organizations',
- 'xml' => $sectionData // complete hierarchy preserved
+ 'name' => 'Organizations',
+ 'xml' => $sectionData,
+ // complete hierarchy preserved.
];
} else {
- $normalized[$section] = $this->extractSectionDataWithProperties($sectionData, $section, $modelIdentifier, $propertyDefinitionMap);
+ $normalized[$section] = $this->extractSectionDataWithProperties(sectionData: $sectionData, sectionName: $section, modelIdentifier: $modelIdentifier, propertyDefinitionMap: $propertyDefinitionMap);
}
}
- }
- $this->logger->info('Data normalization completed', [
- 'model_identifier' => $modelIdentifier,
- 'sections_processed' => $sections,
- 'round_trip_fidelity' => 'enabled'
- ]);
+ }//end foreach
+
+ $this->logger->info(
+ 'Data normalization completed',
+ [
+ 'model_identifier' => $modelIdentifier,
+ 'sections_processed' => $sections,
+ 'round_trip_fidelity' => 'enabled',
+ ]
+ );
return $normalized;
- }
+ }//end normalizeArchiMateData()
/**
* Extract data from a specific section, flatten properties, and store xml
*
- * @param mixed $sectionData Section data from XML parsing
- * @param string $sectionName Name of the section being processed
- * @param string $modelIdentifier The model identifier for linking items
- * @param array $propertyDefinitionMap Map of propertyDefinitionRef => property name
+ * @param mixed $sectionData Section data from XML parsing
+ * @param string $sectionName Name of the section being processed
+ * @param string $modelIdentifier The model identifier for linking items
+ * @param array $propertyDefinitionMap Map of propertyDefinitionRef => property name
+ *
* @return array Extracted section data with complete XML preservation and flattened properties
*/
private function extractSectionDataWithProperties(mixed $sectionData, string $sectionName, string $modelIdentifier, array $propertyDefinitionMap): array
{
$extracted = [];
- if (is_array($sectionData)) {
- $items = $this->findItemsInSection($sectionData, $sectionName);
+ if (is_array($sectionData) === true) {
+ $items = $this->findItemsInSection(sectionData: $sectionData, sectionName: $sectionName);
foreach ($items as $item) {
- $identifier = $this->extractIdentifier($item, $sectionName);
- if ($identifier) {
- // OPTIMIZATION: Store XML data directly without expensive deep copy
- // Start with base object structure
+ $identifier = $this->extractIdentifier(item: $item, sectionName: $sectionName);
+ if (empty($identifier) === false) {
+ // OPTIMIZATION: Store XML data directly without expensive deep copy.
+ // Start with base object structure.
$object = [
- 'identifier' => $identifier,
- 'section' => $sectionName,
+ 'identifier' => $identifier,
+ 'section' => $sectionName,
'model_identifier' => $modelIdentifier,
- 'extracted_at' => time(),
- 'xml' => $this->extractEssentialXmlData($item) // OPTIMIZATION: Store only essential XML data
+ 'extracted_at' => time(),
+ 'xml' => $this->extractEssentialXmlData(item: $item),
+ // OPTIMIZATION: Store only essential XML data.
];
- // Extract type from xsi:type attribute (e.g., "Capability", "ApplicationComponent", "Referentiecomponent")
- // The xsi:type is stored as _xsi__type or in _attributes['xsi:type']
- if (isset($item['_xsi__type'])) {
+ // Extract type from xsi:type attribute (e.g., "Capability", "ApplicationComponent", "Referentiecomponent").
+ // The xsi:type is stored as _xsi__type or in _attributes['xsi:type'].
+ if (isset($item['_xsi__type']) === true) {
$object['type'] = $item['_xsi__type'];
- } elseif (isset($item['_attributes']['xsi:type'])) {
+ } else if (isset($item['_attributes']['xsi:type']) === true) {
$object['type'] = $item['_attributes']['xsi:type'];
}
- // Extract name from XML if it exists
- if (isset($item['name'])) {
- if (is_array($item['name']) && isset($item['name']['_value'])) {
+ // Extract name from XML if it exists.
+ if (isset($item['name']) === true) {
+ if (is_array($item['name']) === true && isset($item['name']['_value']) === true) {
$object['name'] = $item['name']['_value'];
- } elseif (is_string($item['name'])) {
+ } else if (is_string($item['name']) === true) {
$object['name'] = $item['name'];
}
}
- // Extract documentation from XML if it exists - set both summary and documentation
- if (isset($item['documentation'])) {
+ // Extract documentation from XML if it exists - set both summary and documentation.
+ if (isset($item['documentation']) === true) {
$docValue = null;
- if (is_array($item['documentation']) && isset($item['documentation']['_value'])) {
+ if (is_array($item['documentation']) === true && isset($item['documentation']['_value']) === true) {
$docValue = $item['documentation']['_value'];
- } elseif (is_string($item['documentation'])) {
+ } else if (is_string($item['documentation']) === true) {
$docValue = $item['documentation'];
}
+
if ($docValue !== null) {
- $object['summary'] = $docValue;
- $object['documentation'] = $docValue; // Also set documentation field for schema compatibility
+ $object['summary'] = $docValue;
+ $object['documentation'] = $docValue;
+ // Also set documentation field for schema compatibility.
}
}
- // Flatten properties to root fields using the propertyDefinitionMap
- if (isset($item['properties']) && isset($item['properties']['property'])) {
+ // Flatten properties to root fields using the propertyDefinitionMap.
+ if (isset($item['properties']) === true && isset($item['properties']['property']) === true) {
$props = $item['properties']['property'];
$processedProperties = [];
- if (isset($props[0])) {
- // Multiple properties
+ if (isset($props[0]) === true) {
+ // Multiple properties.
foreach ($props as $prop) {
$defRef = $prop['_attributes']['propertyDefinitionRef'] ?? null;
- $value = $prop['value']['_value'] ?? $prop['value'] ?? null;
- if ($defRef && isset($propertyDefinitionMap[$defRef])) {
- $name = $propertyDefinitionMap[$defRef];
- $camelCaseName = $this->convertToCamelCase($name);
+ $value = $prop['value']['_value'] ?? $prop['value'] ?? null;
+ if ($defRef !== false && isset($propertyDefinitionMap[$defRef]) === true) {
+ $name = $propertyDefinitionMap[$defRef];
+ $camelCaseName = $this->convertToCamelCase(propertyName: $name);
$object[$camelCaseName] = $value;
- // Store property mapping for reference
- if (!isset($object['_propertyMapping'])) {
+ // Store property mapping for reference.
+ if (isset($object['_propertyMapping']) === false) {
$object['_propertyMapping'] = [];
}
+
$object['_propertyMapping'][$camelCaseName] = $name;
- // If this property is 'Object ID', set slug for later use
+ // If this property is 'Object ID', set slug for later use.
if (strtolower($name) === 'object id') {
- $object['_slug'] = $value; // Store temporarily, will be moved to @self.slug later
+ $object['_slug'] = $value;
+ // Store temporarily, will be moved to @self.slug later.
}
}
- }
- } elseif (isset($props['_attributes']['propertyDefinitionRef'])) {
- // Single property
+ }//end foreach
+ } else if (isset($props['_attributes']['propertyDefinitionRef']) === true) {
+ // Single property.
$defRef = $props['_attributes']['propertyDefinitionRef'];
- $value = $props['value']['_value'] ?? $props['value'] ?? null;
- if ($defRef && isset($propertyDefinitionMap[$defRef])) {
- $name = $propertyDefinitionMap[$defRef];
- $camelCaseName = $this->convertToCamelCase($name);
+ $value = $props['value']['_value'] ?? $props['value'] ?? null;
+ if ($defRef !== false && isset($propertyDefinitionMap[$defRef]) === true) {
+ $name = $propertyDefinitionMap[$defRef];
+ $camelCaseName = $this->convertToCamelCase(propertyName: $name);
$object[$camelCaseName] = $value;
- // Store property mapping for reference
- if (!isset($object['_propertyMapping'])) {
+ // Store property mapping for reference.
+ if (isset($object['_propertyMapping']) === false) {
$object['_propertyMapping'] = [];
}
+
$object['_propertyMapping'][$camelCaseName] = $name;
$processedProperties[] = [
- 'original' => $name,
+ 'original' => $name,
'camelCase' => $camelCaseName,
- 'value' => $value
+ 'value' => $value,
];
if (strtolower($name) === 'object id') {
- $object['_slug'] = $value; // Store temporarily, will be moved to @self.slug later
+ $object['_slug'] = $value;
+ // Store temporarily, will be moved to @self.slug later.
}
- }
+ }//end if
+ }//end if
+ }//end if
- // OPTIMIZATION: Removed debug logging from tight loop for performance
- }
- }
$extracted[$identifier] = $object;
- }
- }
- }
+ }//end if
+ }//end foreach
+ }//end if
+
return $extracted;
- }
+ }//end extractSectionDataWithProperties()
/**
* Convert normalized data to OpenRegister objects with @self structure
@@ -930,255 +1022,288 @@ private function extractSectionDataWithProperties(mixed $sectionData, string $se
* 3. Ensures each object has the required @self structure for ObjectService::saveObjects
* 4. Links all objects to the parent model via model_identifier
*
- * @param array $normalizedData Normalized ArchiMate data with model_identifier
- * @param string $modelIdentifier The model identifier for linking objects
+ * @param array $normalizedData Normalized ArchiMate data with model_identifier
+ * @param string $modelIdentifier The model identifier for linking objects
+ *
* @return array Array of OpenRegister objects with proper @self structure
*/
private function convertToOpenRegisterObjects(array $normalizedData, string $modelIdentifier): array
{
- $this->logger->info('Converting to OpenRegister objects with @self structure', [
- 'model_identifier' => $modelIdentifier
- ]);
+ $this->logger->info(
+ 'Converting to OpenRegister objects with @self structure',
+ [
+ 'model_identifier' => $modelIdentifier,
+ ]
+ );
$objects = [];
- // STEP 1: Convert model metadata to model object
- if (!empty($normalizedData['model_metadata'])) {
+ // STEP 1: Convert model metadata to model object.
+ if (empty($normalizedData['model_metadata']) === false) {
$this->logger->debug('Creating model object from metadata');
- $objects[] = $this->createModelObject($normalizedData['model_metadata'], $modelIdentifier);
+ $objects[] = $this->createModelObject(metadata: $normalizedData['model_metadata'], modelIdentifier: $modelIdentifier);
}
- // STEP 2: Convert each section to individual objects
+ // STEP 2: Convert each section to individual objects.
$sections = ['elements', 'relationships', 'organizations', 'views', 'property_definitions'];
- // OPTIMIZATION: Removed excessive debug logging from tight loops
+ // OPTIMIZATION: Removed excessive debug logging from tight loops.
$sectionCounts = [];
foreach ($sections as $section) {
- if (!empty($normalizedData[$section]) && is_array($normalizedData[$section])) {
+ if (empty($normalizedData[$section]) === false && is_array($normalizedData[$section]) === true) {
$sectionCounts[$section] = count($normalizedData[$section]);
foreach ($normalizedData[$section] as $identifier => $data) {
- $objects[] = $this->createSectionObject($section, $identifier, $data, $modelIdentifier);
+ $objects[] = $this->createSectionObject(section: $section, identifier: $identifier, data: $data, modelIdentifier: $modelIdentifier);
}
} else {
$sectionCounts[$section] = 0;
}
}
- // Single consolidated log entry
+ // Single consolidated log entry.
$this->logger->debug('Sections processed', $sectionCounts);
- $this->logger->info('Conversion to OpenRegister objects completed', [
- 'model_identifier' => $modelIdentifier,
- 'total_objects' => count($objects),
- 'sections_processed' => $sections
- ]);
+ $this->logger->info(
+ 'Conversion to OpenRegister objects completed',
+ [
+ 'model_identifier' => $modelIdentifier,
+ 'total_objects' => count($objects),
+ 'sections_processed' => $sections,
+ ]
+ );
return $objects;
- }
+ }//end convertToOpenRegisterObjects()
/**
* Create model object with @self structure
*
- * @param array $metadata Model metadata
- * @param string $modelIdentifier Model identifier
+ * @param array $metadata Model metadata
+ * @param string $modelIdentifier Model identifier
+ *
* @return array Model object with @self structure
*/
private function createModelObject(array $metadata, string $modelIdentifier): array
{
- // OPTIMIZATION: Use cached configuration values
+ // OPTIMIZATION: Use cached configuration values.
$registerId = $this->cachedConfig['registerId'] ?? throw new \RuntimeException("Register ID not found in cached configuration. Please ensure AMEF configuration is properly initialized.");
- $schemaId = $this->cachedConfig['schemaIds']['model'] ?? throw new \RuntimeException("Schema ID for 'model' not found in cached configuration. Please ensure AMEF configuration is properly initialized.");
+ $schemaId = $this->cachedConfig['schemaIds']['model'] ?? throw new \RuntimeException("Schema ID for 'model' not found in cached configuration. Please ensure AMEF configuration is properly initialized.");
- // Extract a plain string name (schema column expects string, not array)
+ // Extract a plain string name (schema column expects string, not array).
$nameString = null;
- if (isset($metadata['name'])) {
- if (is_array($metadata['name']) && isset($metadata['name']['_value'])) {
- $nameString = (string)$metadata['name']['_value'];
- } elseif (is_string($metadata['name'])) {
+ if (isset($metadata['name']) === true) {
+ if (is_array($metadata['name']) === true && isset($metadata['name']['_value']) === true) {
+ $nameString = (string) $metadata['name']['_value'];
+ } else if (is_string($metadata['name']) === true) {
$nameString = $metadata['name'];
}
}
- // Build xml field preserving full array structure for round-trip fidelity
+ // Build xml field preserving full array structure for round-trip fidelity.
$xmlData = [];
- if (isset($metadata['name'])) {
+ if (isset($metadata['name']) === true) {
$xmlData['name'] = $metadata['name'];
}
- if (isset($metadata['documentation'])) {
+
+ if (isset($metadata['documentation']) === true) {
$xmlData['documentation'] = $metadata['documentation'];
}
- if (isset($metadata['properties'])) {
+
+ if (isset($metadata['properties']) === true) {
$xmlData['properties'] = $metadata['properties'];
}
- if (isset($metadata['propertyDefinitionMap'])) {
+
+ if (isset($metadata['propertyDefinitionMap']) === true) {
$xmlData['propertyDefinitionMap'] = $metadata['propertyDefinitionMap'];
}
- // Create object with @self structure and metadata at root level (no JSON serialization)
+ // Create object with @self structure and metadata at root level (no JSON serialization).
$object = [
- '@self' => [
- 'register' => $registerId,
- 'schema' => $schemaId,
- 'id' => $metadata['identifier'] ?? uniqid('model_'),
- 'owner' => $this->getCurrentUserId(),
+ '@self' => [
+ 'register' => $registerId,
+ 'schema' => $schemaId,
+ 'id' => $metadata['identifier'] ?? uniqid('model_'),
+ 'owner' => $this->getCurrentUserId(),
'organisation' => $this->getCurrentOrganisation(),
- 'published' => date('Y-m-d\TH:i:s\Z')
+ 'published' => date('Y-m-d\TH:i:s\Z'),
],
- 'identifier' => $metadata['identifier'] ?? '',
- 'section' => 'model',
+ 'identifier' => $metadata['identifier'] ?? '',
+ 'section' => 'model',
'model_identifier' => $modelIdentifier,
- 'xml' => $xmlData
+ 'xml' => $xmlData,
];
- // Merge metadata directly at root level, but override name with string version
+ // Merge metadata directly at root level, but override name with string version.
$merged = array_merge($object, $metadata);
if ($nameString !== null) {
$merged['name'] = $nameString;
}
return $merged;
- }
+ }//end createModelObject()
/**
* Create section object with @self structure and flattened XML data
*
- * @param string $section Section name
- * @param string $identifier Item identifier
- * @param array $data Item data (already contains XML data at root level)
- * @param string $modelIdentifier Model identifier for linking
+ * @param string $section Section name
+ * @param string $identifier Item identifier
+ * @param array $data Item data (already contains XML data at root level)
+ * @param string $modelIdentifier Model identifier for linking
+ *
* @return array Section object with @self structure
*/
private function createSectionObject(string $section, string $identifier, array $data, string $modelIdentifier): array
{
- // OPTIMIZATION: Use cached configuration values
+ // OPTIMIZATION: Use cached configuration values.
$registerId = $this->cachedConfig['registerId'] ?? throw new \RuntimeException("Register ID not found in cached configuration. Please ensure AMEF configuration is properly initialized.");
- $schemaId = $this->cachedConfig['schemaIds'][$section] ?? $this->getSchemaIdForSection($section);
+ $schemaId = $this->cachedConfig['schemaIds'][$section] ?? $this->getSchemaIdForSection(section: $section);
- // FIXED: Use objectId as main ID and AMEF identifier as slug
+ // FIXED: Use objectId as main ID and AMEF identifier as slug.
$objectId = null;
- $slug = null;
+ $slug = null;
- // Priority 1: Check for objectId property (flattened from "Object ID")
- if (isset($data['objectId'])) {
+ // Priority 1: Check for objectId property (flattened from "Object ID").
+ if (isset($data['objectId']) === true) {
$objectId = $data['objectId'];
- $slug = $identifier; // Use AMEF identifier as slug
+ $slug = $identifier;
+ // Use AMEF identifier as slug.
}
- // Priority 2: Check for temporary _slug field (legacy support)
- elseif (isset($data['_slug'])) {
+ // Priority 2: Check for temporary _slug field (legacy support).
+ else if (isset($data['_slug']) === true) {
$objectId = $data['_slug'];
- $slug = $identifier; // Use AMEF identifier as slug
- unset($data['_slug']); // Remove the temporary field
+ $slug = $identifier;
+ // Use AMEF identifier as slug.
+ unset($data['_slug']);
+ // Remove the temporary field.
}
- // Priority 3: Check for direct "Object ID" property
- elseif (isset($data['Object ID'])) {
+ // Priority 3: Check for direct "Object ID" property.
+ else if (isset($data['Object ID']) === true) {
$objectId = $data['Object ID'];
- $slug = $identifier; // Use AMEF identifier as slug
+ $slug = $identifier;
+ // Use AMEF identifier as slug.
}
- // Fallback: Use AMEF identifier as both ID and extract clean UUID for slug
+ // Fallback: Use AMEF identifier as both ID and extract clean UUID for slug.
else {
$objectId = $identifier;
- // Extract clean UUID from AMEF identifier (remove "id-" prefix if present)
- if ($identifier && str_starts_with($identifier, 'id-')) {
- $slug = substr($identifier, 3); // Remove "id-" prefix
+ // Extract clean UUID from AMEF identifier (remove "id-" prefix if present).
+ if ($identifier !== false && str_starts_with($identifier, 'id-') === true) {
+ $slug = substr($identifier, 3);
+ // Remove "id-" prefix.
} else {
$slug = $identifier;
}
}
- // Create object with @self structure using correct ID and slug
+ // Create object with @self structure using correct ID and slug.
$object = [
'@self' => [
- 'register' => $registerId,
- 'schema' => $schemaId,
- 'id' => $objectId, // Now using objectId as main ID
- 'slug' => $slug, // Now using AMEF identifier as slug
- 'owner' => $this->getCurrentUserId(),
+ 'register' => $registerId,
+ 'schema' => $schemaId,
+ 'id' => $objectId,
+ // Now using objectId as main ID.
+ 'slug' => $slug,
+ // Now using AMEF identifier as slug.
+ 'owner' => $this->getCurrentUserId(),
'organisation' => $this->getCurrentOrganisation(),
- 'published' => date('Y-m-d\TH:i:s\Z')
- ]
+ 'published' => date('Y-m-d\TH:i:s\Z'),
+ ],
];
- // Merge XML data directly at root level (data already contains identifier, section, model_identifier)
+ // Merge XML data directly at root level (data already contains identifier, section, model_identifier).
return array_merge($object, $data);
- }
+ }//end createSectionObject()
/**
* Save objects to database using ObjectService::saveObjects
*
- * @param array $objects Objects to save
+ * @param array $objects Objects to save
+ *
* @return array Saved objects
*/
private function saveObjectsToDatabase(array $objects): array
{
$saveStartTime = microtime(true);
- // DEBUG: Log basic object info before sending to ObjectService
- // Find first element with gemmaType for debugging
- $elementsWithGemmaType = array_filter($objects, fn($o) => ($o['section'] ?? '') === 'element' && !empty($o['gemmaType']));
- $sampleElementWithGemmaType = !empty($elementsWithGemmaType) ? array_values($elementsWithGemmaType)[0] : null;
-
- $this->logger->debug('Objects before save', [
- 'total_objects_to_save' => count($objects),
- 'elements_with_gemmaType' => count($elementsWithGemmaType),
- ]);
+ // DEBUG: Log basic object info before sending to ObjectService.
+ // Find first element with gemmaType for debugging.
+ $elementsWithGemmaType = array_filter($objects, fn($o) => ($o['section'] ?? '') === 'element' && empty($o['gemmaType']) === false);
+ if (empty($elementsWithGemmaType) === false) {
+ $sampleElementWithGemmaType = array_values($elementsWithGemmaType)[0];
+ } else {
+ $sampleElementWithGemmaType = null;
+ }
+ $this->logger->debug(
+ 'Objects before save',
+ [
+ 'total_objects_to_save' => count($objects),
+ 'elements_with_gemmaType' => count($elementsWithGemmaType),
+ ]
+ );
$serviceInitStartTime = microtime(true);
- $objectService = $this->getObjectService();
- if (!$objectService) {
+ $objectService = $this->getObjectService();
+ if ($objectService === null) {
throw new \RuntimeException('ObjectService not available');
}
+
$serviceInitTime = microtime(true) - $serviceInitStartTime;
- // ENHANCEMENT: Process GEMMA Referentiecomponent-Standaard relationships before saving
+ // ENHANCEMENT: Process GEMMA Referentiecomponent-Standaard relationships before saving.
$gemmaProcessingStartTime = microtime(true);
- $objects = $this->processGemmaReferenceComponentStandards($objects);
+ $objects = $this->processGemmaReferenceComponentStandards(objects: $objects);
$gemmaProcessingTime = microtime(true) - $gemmaProcessingStartTime;
- // Saving objects to database
-
- // OPTIMIZATION: Use cached register ID
+ // Saving objects to database.
+ // OPTIMIZATION: Use cached register ID.
$registerId = $this->cachedConfig['registerId'] ?? throw new \RuntimeException("Register ID not found in cached configuration. Please ensure AMEF configuration is properly initialized.");
-
-
// MAGIC MAPPING SUPPORT: Group objects by schema first, then save each schema group.
// This ensures each batch has a single schema so UnifiedObjectMapper can route to the correct magic table.
$batchProcessingStartTime = microtime(true);
- // Group objects by schema
+ // Group objects by schema.
$schemaGroups = [];
foreach ($objects as $obj) {
$schemaId = $obj['@self']['schema'] ?? 'unknown';
$schemaGroups[$schemaId][] = $obj;
}
- $this->logger->info('ArchiMate import: Grouped objects by schema for magic mapping', [
- 'schemaCount' => count($schemaGroups),
- 'schemas' => array_map('count', $schemaGroups)
- ]);
+ $this->logger->info(
+ 'ArchiMate import: Grouped objects by schema for magic mapping',
+ [
+ 'schemaCount' => count($schemaGroups),
+ 'schemas' => array_map('count', $schemaGroups),
+ ]
+ );
// Process each schema group.
- $allResults = [];
+ $allResults = [];
$aggregatedStats = [
- 'saved' => [],
- 'updated' => [],
+ 'saved' => [],
+ 'updated' => [],
'unchanged' => [],
- 'invalid' => [],
+ 'invalid' => [],
];
- // Track counts per schema for accurate statistics (serialized objects lose the 'section' field)
+ // Track counts per schema for accurate statistics (serialized objects lose the 'section' field).
$countsBySchema = [];
foreach ($schemaGroups as $schemaId => $schemaObjects) {
$schemaObjectCount = count($schemaObjects);
try {
- // Save this schema group with the specific schema ID
- // PERFORMANCE: Disabled validation and events for bulk import (like CSV import pattern)
+ // Save this schema group with the specific schema ID.
+ // PERFORMANCE: Disabled validation and events for bulk import (like CSV import pattern).
+ if ($schemaId !== 'unknown') {
+ $schemaValue = (int) $schemaId;
+ } else {
+ $schemaValue = null;
+ }
+
$saveResult = $objectService->saveObjects(
objects: $schemaObjects,
register: $registerId,
- schema: $schemaId !== 'unknown' ? (int) $schemaId : null,
+ schema: $schemaValue,
_rbac: false,
_multitenancy: false,
validation: false,
@@ -1186,68 +1311,77 @@ private function saveObjectsToDatabase(array $objects): array
);
// Merge results.
- $aggregatedStats['saved'] = array_merge($aggregatedStats['saved'], $saveResult['saved'] ?? []);
- $aggregatedStats['updated'] = array_merge($aggregatedStats['updated'], $saveResult['updated'] ?? []);
+ $aggregatedStats['saved'] = array_merge($aggregatedStats['saved'], $saveResult['saved'] ?? []);
+ $aggregatedStats['updated'] = array_merge($aggregatedStats['updated'], $saveResult['updated'] ?? []);
$aggregatedStats['unchanged'] = array_merge($aggregatedStats['unchanged'], $saveResult['unchanged'] ?? []);
- $aggregatedStats['invalid'] = array_merge($aggregatedStats['invalid'], $saveResult['invalid'] ?? []);
+ $aggregatedStats['invalid'] = array_merge($aggregatedStats['invalid'], $saveResult['invalid'] ?? []);
- // Track per-schema counts for statistics (since serialized objects lose 'section')
+ // Track per-schema counts for statistics (since serialized objects lose 'section').
$countsBySchema[$schemaId] = [
- 'saved' => count($saveResult['saved'] ?? []),
- 'updated' => count($saveResult['updated'] ?? []),
+ 'saved' => count($saveResult['saved'] ?? []),
+ 'updated' => count($saveResult['updated'] ?? []),
'unchanged' => count($saveResult['unchanged'] ?? []),
- 'invalid' => count($saveResult['invalid'] ?? []),
+ 'invalid' => count($saveResult['invalid'] ?? []),
];
$allResults = array_merge($allResults, $saveResult['saved'] ?? [], $saveResult['updated'] ?? [], $saveResult['unchanged'] ?? []);
- $this->logger->debug('Schema group saved for magic mapping', [
- 'schemaId' => $schemaId,
- 'objectCount' => $schemaObjectCount,
- 'saved' => count($saveResult['saved'] ?? []),
- 'updated' => count($saveResult['updated'] ?? []),
- ]);
+ $this->logger->debug(
+ 'Schema group saved for magic mapping',
+ [
+ 'schemaId' => $schemaId,
+ 'objectCount' => $schemaObjectCount,
+ 'saved' => count($saveResult['saved'] ?? []),
+ 'updated' => count($saveResult['updated'] ?? []),
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('Error saving schema group', [
- 'schemaId' => $schemaId,
- 'error' => $e->getMessage(),
- 'objectCount' => $schemaObjectCount
- ]);
- }
- }
-
- // Store aggregated result for statistics, including per-schema counts
+ $this->logger->error(
+ 'Error saving schema group',
+ [
+ 'schemaId' => $schemaId,
+ 'error' => $e->getMessage(),
+ 'objectCount' => $schemaObjectCount,
+ ]
+ );
+ }//end try
+ }//end foreach
+
+ // Store aggregated result for statistics, including per-schema counts.
$aggregatedStats['countsBySchema'] = $countsBySchema;
$this->lastSaveResult = $aggregatedStats;
$result = $allResults;
$batchProcessingTime = microtime(true) - $batchProcessingStartTime;
- // POST-PROCESSING: Fix StandaardVersie standaard field UUIDs
- // The standaard field was set with ArchiMate identifiers, but we need database UUIDs
+ // POST-PROCESSING: Fix StandaardVersie standaard field UUIDs.
+ // The standaard field was set with ArchiMate identifiers, but we need database UUIDs.
// for the inversedBy lookup to work correctly.
- $this->fixStandaardVersieUuids($registerId);
+ $this->fixStandaardVersieUuids(registerId: $registerId);
$totalSaveTime = microtime(true) - $saveStartTime;
-
-
- // Database save completed
-
- // Store timing breakdown for performance metrics
- // FIX: Use aggregatedStats counts instead of $result which may be empty from bulk operations
+ // Database save completed.
+ // Store timing breakdown for performance metrics.
+ // FIX: Use aggregatedStats counts instead of $result which may be empty from bulk operations.
$totalSavedCount = count($aggregatedStats['saved'] ?? []) + count($aggregatedStats['updated'] ?? []) + count($aggregatedStats['unchanged'] ?? []);
+ if ($totalSavedCount > 0) {
+ $objectsSavedValue = $totalSavedCount;
+ } else {
+ $objectsSavedValue = count($objects);
+ }
+
$this->lastSaveTimingBreakdown = [
- 'total_save_seconds' => round($totalSaveTime, 3),
- 'service_init_seconds' => round($serviceInitTime, 3),
- 'gemma_processing_seconds' => round($gemmaProcessingTime, 3),
- 'batch_processing_seconds' => round($batchProcessingTime, 3),
- 'objects_saved' => $totalSavedCount > 0 ? $totalSavedCount : count($objects),
- 'save_rate_objects_per_second' => round(count($objects) / max($totalSaveTime, 0.001), 1)
+ 'total_save_seconds' => round($totalSaveTime, 3),
+ 'service_init_seconds' => round($serviceInitTime, 3),
+ 'gemma_processing_seconds' => round($gemmaProcessingTime, 3),
+ 'batch_processing_seconds' => round($batchProcessingTime, 3),
+ 'objects_saved' => $objectsSavedValue,
+ 'save_rate_objects_per_second' => round(count($objects) / max($totalSaveTime, 0.001), 1),
];
return $result;
- }
+ }//end saveObjectsToDatabase()
/**
* Fix StandaardVersie standaard field UUIDs after import
@@ -1257,7 +1391,8 @@ private function saveObjectsToDatabase(array $objects): array
* 1. Queries all Standaarden to get identifier → uuid mapping
* 2. Updates StandaardVersie objects to use the correct database UUIDs
*
- * @param int $registerId The register ID
+ * @param int $registerId The register ID
+ *
* @return void
*/
private function fixStandaardVersieUuids(int $registerId): void
@@ -1269,70 +1404,79 @@ private function fixStandaardVersieUuids(int $registerId): void
return;
}
- // Get database connection
+ // Get database connection.
$connection = \OC::$server->getDatabaseConnection();
- $tableName = 'oc_openregister_table_' . $registerId . '_' . $elementSchemaId;
+ $tableName = 'oc_openregister_table_'.$registerId.'_'.$elementSchemaId;
- // Step 1: Build a mapping from ArchiMate identifier to database UUID for Standaarden
- // The identifier field contains "id-{archimate_id}" and we need the _uuid field
+ // Step 1: Build a mapping from ArchiMate identifier to database UUID for Standaarden.
+ // The identifier field contains "id-{archimate_id}" and we need the _uuid field.
$standaardQuery = $connection->executeQuery(
"SELECT _uuid, identifier FROM {$tableName} WHERE gemma_type = 'Standaard' AND identifier IS NOT NULL"
);
$identifierToUuid = [];
- while ($row = $standaardQuery->fetch()) {
+ while (($row = $standaardQuery->fetch()) !== false) {
$identifier = $row['identifier'] ?? '';
- $uuid = $row['_uuid'] ?? '';
+ $uuid = $row['_uuid'] ?? '';
- if ($identifier && $uuid) {
- // Store mapping without "id-" prefix for matching
+ if ($identifier !== false && $uuid === true) {
+ // Store mapping without "id-" prefix for matching.
$cleanId = str_replace('id-', '', $identifier);
$identifierToUuid[$cleanId] = $uuid;
}
}
- $this->logger->info('fixStandaardVersieUuids: Built identifier->uuid mapping', [
- 'standaard_count' => count($identifierToUuid)
- ]);
+ $this->logger->info(
+ 'fixStandaardVersieUuids: Built identifier->uuid mapping',
+ [
+ 'standaard_count' => count($identifierToUuid),
+ ]
+ );
- if (empty($identifierToUuid)) {
+ if (empty($identifierToUuid) === true) {
$this->logger->warning('fixStandaardVersieUuids: No Standaarden found to map');
return;
}
- // Step 2: Update StandaardVersies that have a standaard field with ArchiMate identifiers
- // We need to replace ArchiMate IDs with database UUIDs
+ // Step 2: Update StandaardVersies that have a standaard field with ArchiMate identifiers.
+ // We need to replace ArchiMate IDs with database UUIDs.
$updateCount = 0;
foreach ($identifierToUuid as $archiMateId => $dbUuid) {
- // Update all StandaardVersies where standaard matches this ArchiMate ID
- $result = $connection->executeStatement(
+ // Update all StandaardVersies where standaard matches this ArchiMate ID.
+ $result = $connection->executeStatement(
"UPDATE {$tableName} SET standaard = ? WHERE standaard = ? AND gemma_type = 'Standaardversie'",
[$dbUuid, $archiMateId]
);
$updateCount += $result;
}
- $this->logger->info('fixStandaardVersieUuids: Updated StandaardVersie standaard fields', [
- 'updated_count' => $updateCount,
- 'mapping_count' => count($identifierToUuid)
- ]);
-
+ $this->logger->info(
+ 'fixStandaardVersieUuids: Updated StandaardVersie standaard fields',
+ [
+ 'updated_count' => $updateCount,
+ 'mapping_count' => count($identifierToUuid),
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('fixStandaardVersieUuids: Error fixing UUIDs', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
- }
- }
+ $this->logger->error(
+ 'fixStandaardVersieUuids: Error fixing UUIDs',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+ }//end try
+ }//end fixStandaardVersieUuids()
/**
* Save objects directly to ObjectService without custom batching
* Lets ObjectService handle all batching, throttling, and optimization internally
*
- * @param array $objects Array of objects to save
- * @param ObjectService $objectService ObjectService instance
- * @param int $registerId Register ID
+ * @param array $objects Array of objects to save
+ * @param ObjectService $objectService ObjectService instance
+ * @param int $registerId Register ID
+ *
* @return array Array of saved objects
*/
private function saveObjectsDirectToService(array $objects, ObjectService $objectService, int $registerId): array
@@ -1346,136 +1490,154 @@ private function saveObjectsDirectToService(array $objects, ObjectService $objec
$schemaGroups[$schemaId][] = $obj;
}
- $this->logger->info('ArchiMate import: Grouped objects by schema', [
- 'schemaCount' => count($schemaGroups),
- 'schemas' => array_map('count', $schemaGroups)
- ]);
+ $this->logger->info(
+ 'ArchiMate import: Grouped objects by schema',
+ [
+ 'schemaCount' => count($schemaGroups),
+ 'schemas' => array_map('count', $schemaGroups),
+ ]
+ );
- // Save each schema group separately
- $allSaved = [];
- $allUpdated = [];
+ // Save each schema group separately.
+ $allSaved = [];
+ $allUpdated = [];
$allUnchanged = [];
- $allSkipped = [];
- $allInvalid = [];
+ $allSkipped = [];
+ $allInvalid = [];
foreach ($schemaGroups as $schemaId => $schemaObjects) {
+ if ($schemaId !== 'unknown') {
+ $schemaValue = (int) $schemaId;
+ } else {
+ $schemaValue = null;
+ }
+
$saveResult = $objectService->saveObjects(
objects: $schemaObjects,
register: $registerId,
- schema: $schemaId !== 'unknown' ? (int) $schemaId : null,
+ schema: $schemaValue,
_rbac: true,
_multitenancy: true,
validation: true,
events: true
);
- // Merge results
- $allSaved = array_merge($allSaved, $saveResult['saved'] ?? []);
- $allUpdated = array_merge($allUpdated, $saveResult['updated'] ?? []);
+ // Merge results.
+ $allSaved = array_merge($allSaved, $saveResult['saved'] ?? []);
+ $allUpdated = array_merge($allUpdated, $saveResult['updated'] ?? []);
$allUnchanged = array_merge($allUnchanged, $saveResult['unchanged'] ?? []);
- $allSkipped = array_merge($allSkipped, $saveResult['skipped'] ?? []);
- $allInvalid = array_merge($allInvalid, $saveResult['invalid'] ?? []);
-
- $this->logger->debug('Schema group saved', [
- 'schemaId' => $schemaId,
- 'objectCount' => count($schemaObjects),
- 'saved' => count($saveResult['saved'] ?? []),
- 'updated' => count($saveResult['updated'] ?? []),
- ]);
- }
-
- // Combine all results
+ $allSkipped = array_merge($allSkipped, $saveResult['skipped'] ?? []);
+ $allInvalid = array_merge($allInvalid, $saveResult['invalid'] ?? []);
+
+ $this->logger->debug(
+ 'Schema group saved',
+ [
+ 'schemaId' => $schemaId,
+ 'objectCount' => count($schemaObjects),
+ 'saved' => count($saveResult['saved'] ?? []),
+ 'updated' => count($saveResult['updated'] ?? []),
+ ]
+ );
+ }//end foreach
+
+ // Combine all results.
$saveResult = [
- 'saved' => $allSaved,
- 'updated' => $allUpdated,
+ 'saved' => $allSaved,
+ 'updated' => $allUpdated,
'unchanged' => $allUnchanged,
- 'skipped' => $allSkipped,
- 'invalid' => $allInvalid,
+ 'skipped' => $allSkipped,
+ 'invalid' => $allInvalid,
];
- // Store result for statistics
+ // Store result for statistics.
$this->lastSaveResult = $saveResult;
- // DEBUG: Log ObjectService save result
- $this->logger->info('ObjectService save result DEBUG', [
- 'total_objects_sent' => count($objects),
- 'saved_count' => count($allSaved),
- 'updated_count' => count($allUpdated),
- 'unchanged_count' => count($allUnchanged),
- 'skipped_count' => count($allSkipped),
- 'invalid_count' => count($allInvalid),
- ]);
-
- // Return combined saved and updated objects
- return array_merge($allSaved, $allUpdated, $allUnchanged);
+ // DEBUG: Log ObjectService save result.
+ $this->logger->info(
+ 'ObjectService save result DEBUG',
+ [
+ 'total_objects_sent' => count($objects),
+ 'saved_count' => count($allSaved),
+ 'updated_count' => count($allUpdated),
+ 'unchanged_count' => count($allUnchanged),
+ 'skipped_count' => count($allSkipped),
+ 'invalid_count' => count($allInvalid),
+ ]
+ );
+ // Return combined saved and updated objects.
+ return array_merge($allSaved, $allUpdated, $allUnchanged);
} catch (\Exception $e) {
- $this->logger->error('Error in direct ObjectService save', [
- 'error' => $e->getMessage(),
- 'object_count' => count($objects)
- ]);
+ $this->logger->error(
+ 'Error in direct ObjectService save',
+ [
+ 'error' => $e->getMessage(),
+ 'object_count' => count($objects),
+ ]
+ );
throw $e;
- }
- }
+ }//end try
+ }//end saveObjectsDirectToService()
/**
* Save objects in parallel batches for maximum performance (DEPRECATED)
*
- * @param array $objects Array of objects to save
- * @param ObjectService $objectService ObjectService instance
- * @param int $registerId Register ID
+ * @param array $objects Array of objects to save
+ * @param ObjectService $objectService ObjectService instance
+ * @param int $registerId Register ID
+ *
* @return array Array of saved objects
*/
private function saveObjectsInParallelBatches(array $objects, ObjectService $objectService, int $registerId): array
{
- $batchSize = self::PERFORMANCE_OPTIMIZATIONS['batch_size'];
+ $batchSize = self::PERFORMANCE_OPTIMIZATIONS['batch_size'];
$parallelBatches = self::PERFORMANCE_OPTIMIZATIONS['parallel_batches'];
- // INTELLIGENT BATCH SIZING: Create size-aware batches instead of fixed-size chunks
- $chunks = $this->createIntelligentBatches($objects);
+ // INTELLIGENT BATCH SIZING: Create size-aware batches instead of fixed-size chunks.
+ $chunks = $this->createIntelligentBatches(objects: $objects);
$totalChunks = count($chunks);
- // Batch processing initialized
-
- $allResults = [];
+ // Batch processing initialized.
+ $allResults = [];
$processedChunks = 0;
- // Accumulate statistics from all chunks (using new format)
+ // Accumulate statistics from all chunks (using new format).
$aggregatedStats = [
- 'saved' => [],
- 'updated' => [],
+ 'saved' => [],
+ 'updated' => [],
'unchanged' => [],
- 'invalid' => []
+ 'invalid' => [],
];
- // Process chunks sequentially but with larger batch sizes for better performance
+ // Process chunks sequentially but with larger batch sizes for better performance.
foreach ($chunks as $chunkIndex => $chunk) {
$chunkInputCount = count($chunk);
try {
+ if (self::PERFORMANCE_OPTIMIZATIONS['disable_rbac']) {
+ $_rbacValue = false;
+ } else {
+ $_rbacValue = true;
+ }
+
$saveResult = $objectService->saveObjects(
objects: $chunk,
register: $registerId,
schema: null,
- _rbac: self::PERFORMANCE_OPTIMIZATIONS['disable_rbac'] ? false : true,
+ _rbac: $_rbacValue,
_multitenancy: true,
validation: !self::PERFORMANCE_OPTIMIZATIONS['disable_validation'],
events: !self::PERFORMANCE_OPTIMIZATIONS['disable_events']
);
+ // Calculate totals received back from this chunk.
+ $chunkTotalReceived = count($saveResult['saved'] ?? []) + count($saveResult['updated'] ?? []) + count($saveResult['unchanged'] ?? []) + count($saveResult['invalid'] ?? []);
-
- // Calculate totals received back from this chunk
- $chunkTotalReceived = count($saveResult['saved'] ?? []) +
- count($saveResult['updated'] ?? []) +
- count($saveResult['unchanged'] ?? []) +
- count($saveResult['invalid'] ?? []);
-
- // Accumulate statistics from this chunk
- $aggregatedStats['saved'] = array_merge($aggregatedStats['saved'], $saveResult['saved'] ?? []);
- $aggregatedStats['updated'] = array_merge($aggregatedStats['updated'], $saveResult['updated'] ?? []);
+ // Accumulate statistics from this chunk.
+ $aggregatedStats['saved'] = array_merge($aggregatedStats['saved'], $saveResult['saved'] ?? []);
+ $aggregatedStats['updated'] = array_merge($aggregatedStats['updated'], $saveResult['updated'] ?? []);
$aggregatedStats['unchanged'] = array_merge($aggregatedStats['unchanged'], $saveResult['unchanged'] ?? []);
- $aggregatedStats['invalid'] = array_merge($aggregatedStats['invalid'], $saveResult['invalid'] ?? []);
+ $aggregatedStats['invalid'] = array_merge($aggregatedStats['invalid'], $saveResult['invalid'] ?? []);
$savedObjects = array_merge(
$saveResult['saved'] ?? [],
@@ -1485,85 +1647,88 @@ private function saveObjectsInParallelBatches(array $objects, ObjectService $obj
$allResults = array_merge($allResults, $savedObjects);
$processedChunks++;
-
} catch (\Exception $e) {
- $this->logger->error('Error processing chunk', [
- 'chunk_index' => $chunkIndex,
- 'error' => $e->getMessage()
- ]);
- // Continue with other chunks
- }
-
- // Memory cleanup between chunks
- if (self::PERFORMANCE_OPTIMIZATIONS['memory_cleanup']) {
+ $this->logger->error(
+ 'Error processing chunk',
+ [
+ 'chunk_index' => $chunkIndex,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ // Continue with other chunks.
+ }//end try
+
+ // Memory cleanup between chunks.
+ if (self::PERFORMANCE_OPTIMIZATIONS['memory_cleanup'] !== false) {
$this->cleanupMemory();
}
- }
+ }//end foreach
- // Store the aggregated result for statistics calculation
+ // Store the aggregated result for statistics calculation.
$this->lastSaveResult = $aggregatedStats;
$totalObjectsProcessed = count($aggregatedStats['saved']) + count($aggregatedStats['updated']) + count($aggregatedStats['unchanged']) + count($aggregatedStats['invalid']);
- // Batch processing completed
-
- // Log critical discrepancy if found
- if (count($objects) != $totalObjectsProcessed) {
- $this->logger->critical('OBJECT COUNT MISMATCH DETECTED', [
- 'objects_sent_to_openregister' => count($objects),
- 'objects_processed_by_openregister' => $totalObjectsProcessed,
- 'missing_objects' => count($objects) - $totalObjectsProcessed,
- 'this_explains_the_781_missing_objects' => true
- ]);
+ // Batch processing completed.
+ // Log critical discrepancy if found.
+ if (count($objects) !== $totalObjectsProcessed) {
+ $this->logger->critical(
+ 'OBJECT COUNT MISMATCH DETECTED',
+ [
+ 'objects_sent_to_openregister' => count($objects),
+ 'objects_processed_by_openregister' => $totalObjectsProcessed,
+ 'missing_objects' => count($objects) - $totalObjectsProcessed,
+ 'this_explains_the_781_missing_objects' => true,
+ ]
+ );
}
return $allResults;
- }
+ }//end saveObjectsInParallelBatches()
/**
* Save objects in a single batch (fallback method)
*
- * @param array $objects Array of objects to save
- * @param ObjectService $objectService ObjectService instance
- * @param int $registerId Register ID
+ * @param array $objects Array of objects to save
+ * @param ObjectService $objectService ObjectService instance
+ * @param int $registerId Register ID
+ *
* @return array Array of saved objects
*/
private function saveObjectsInSingleBatch(array $objects, ObjectService $objectService, int $registerId): array
{
- // Using single batch processing
-
-
+ // Using single batch processing.
+ if (self::PERFORMANCE_OPTIMIZATIONS['disable_rbac']) {
+ $_rbacValue = false;
+ } else {
+ $_rbacValue = true;
+ }
$saveResult = $objectService->saveObjects(
objects: $objects,
register: $registerId,
schema: null,
- _rbac: self::PERFORMANCE_OPTIMIZATIONS['disable_rbac'] ? false : true,
+ _rbac: $_rbacValue,
_multitenancy: true,
validation: !self::PERFORMANCE_OPTIMIZATIONS['disable_validation'],
events: !self::PERFORMANCE_OPTIMIZATIONS['disable_events']
);
-
-
- // Store the save result for later access to statistics
+ // Store the save result for later access to statistics.
$this->lastSaveResult = $saveResult;
- // Extract saved objects from the new structured return format
+ // Extract saved objects from the new structured return format.
$savedObjects = array_merge(
$saveResult['saved'] ?? [],
$saveResult['updated'] ?? []
);
- // Objects saved successfully
-
- // Validation errors logged if any
-
- // Unchanged objects noted if any
-
- // Return the combined saved and updated objects (maintaining backward compatibility)
+ // Objects saved successfully.
+ // Validation errors logged if any.
+ // Unchanged objects noted if any.
+ // Return the combined saved and updated objects (maintaining backward compatibility).
return $savedObjects;
- }
+ }//end saveObjectsInSingleBatch()
/**
* Get ObjectService from container
@@ -1572,19 +1737,22 @@ private function saveObjectsInSingleBatch(array $objects, ObjectService $objectS
*/
private function getObjectService(): ?ObjectService
{
- if (!$this->appManager->isInstalled('openregister')) {
+ if ($this->appManager->isInstalled(appId: 'openregister') === false) {
return null;
}
try {
return $this->container->get(ObjectService::class);
} catch (\Exception $e) {
- $this->logger->warning('Failed to get ObjectService', [
- 'error' => $e->getMessage()
- ]);
+ $this->logger->warning(
+ 'Failed to get ObjectService',
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
return null;
}
- }
+ }//end getObjectService()
/**
* Initialize cached configuration values for performance optimization
@@ -1594,44 +1762,49 @@ private function getObjectService(): ?ObjectService
private function initializeCache(): void
{
if ($this->cachedConfig !== null) {
- return; // Already cached
+ return;
+ // Already cached.
}
$this->cachedConfig = [
- 'userId' => $this->userSession->getUser()?->getUID(),
+ 'userId' => $this->userSession->getUser()?->getUID(),
'organisation' => 'default',
- 'registerId' => $this->getAmefRegisterId(),
- 'schemaIds' => [
- 'model' => $this->getAmefSchemaIdForType('model'),
- 'element' => $this->getAmefSchemaIdForType('element'),
- 'relationship' => $this->getAmefSchemaIdForType('relationship'),
- 'view' => $this->getAmefSchemaIdForType('view'),
- 'organization' => $this->getAmefSchemaIdForType('organization'),
- 'property_definition' => $this->getAmefSchemaIdForType('property_definition')
- // NOTE: 'property' removed - properties are never root-level AMEF objects, only nested within other elements
- ]
+ 'registerId' => $this->getAmefRegisterId(),
+ 'schemaIds' => [
+ 'model' => $this->getAmefSchemaIdForType(archiMateType: 'model'),
+ 'element' => $this->getAmefSchemaIdForType(archiMateType: 'element'),
+ 'relationship' => $this->getAmefSchemaIdForType(archiMateType: 'relationship'),
+ 'view' => $this->getAmefSchemaIdForType(archiMateType: 'view'),
+ 'organization' => $this->getAmefSchemaIdForType(archiMateType: 'organization'),
+ 'property_definition' => $this->getAmefSchemaIdForType(archiMateType: 'property_definition'),
+ // NOTE: 'property' removed - properties are never root-level AMEF objects, only nested within other elements.
+ ],
];
- }
+ }//end initializeCache()
/**
* Log current memory usage for performance monitoring
*
- * @param string $stage Description of the current processing stage
+ * @param string $stage Description of the current processing stage
+ *
* @return void
*/
private function logMemoryUsage(string $stage): void
{
- // Check if debug logging is available (Nextcloud logger doesn't have isDebug method)
+ // Check if debug logging is available (Nextcloud logger doesn't have isDebug method).
$memoryUsage = memory_get_usage(true);
- $memoryPeak = memory_get_peak_usage(true);
+ $memoryPeak = memory_get_peak_usage(true);
$memoryLimit = ini_get('memory_limit');
- $this->logger->debug("Memory usage at: {$stage}", [
- 'current_mb' => round($memoryUsage / 1024 / 1024, 2),
- 'peak_mb' => round($memoryPeak / 1024 / 1024, 2),
- 'limit' => $memoryLimit
- ]);
- }
+ $this->logger->debug(
+ "Memory usage at: {$stage}",
+ [
+ 'current_mb' => round($memoryUsage / 1024 / 1024, 2),
+ 'peak_mb' => round($memoryPeak / 1024 / 1024, 2),
+ 'limit' => $memoryLimit,
+ ]
+ );
+ }//end logMemoryUsage()
/**
* Clean up memory by forcing garbage collection
@@ -1640,14 +1813,17 @@ private function logMemoryUsage(string $stage): void
*/
private function cleanupMemory(): void
{
- if (function_exists('gc_collect_cycles')) {
+ if (function_exists('gc_collect_cycles') === true) {
$cycles = gc_collect_cycles();
- // Use PSR-3 standard logging instead of isDebug() check
- $this->logger->debug('Garbage collection completed', [
- 'cycles_collected' => $cycles
- ]);
+ // Use PSR-3 standard logging instead of isDebug() check.
+ $this->logger->debug(
+ 'Garbage collection completed',
+ [
+ 'cycles_collected' => $cycles,
+ ]
+ );
}
- }
+ }//end cleanupMemory()
/**
* Get current user ID from cache
@@ -1657,7 +1833,7 @@ private function cleanupMemory(): void
private function getCurrentUserId(): ?string
{
return $this->cachedConfig['userId'] ?? null;
- }
+ }//end getCurrentUserId()
/**
* Get current organisation UUID from OrganisationService
@@ -1671,16 +1847,15 @@ private function getCurrentOrganisation(): string
$defaultOrganisation = $this->organisationService->ensureDefaultOrganisation();
$uuid = $defaultOrganisation->getUuid();
-
- $this->logger->info('Got default organisation UUID: ' . $uuid);
+ $this->logger->info('Got default organisation UUID: '.$uuid);
return $uuid;
} catch (\Exception $e) {
- $this->logger->error('Failed to get default organisation: ' . $e->getMessage());
- $this->logger->error('Exception trace: ' . $e->getTraceAsString());
- // Fallback to cached value or 'default' string
+ $this->logger->error('Failed to get default organisation: '.$e->getMessage());
+ $this->logger->error('Exception trace: '.$e->getTraceAsString());
+ // Fallback to cached value or 'default' string.
return $this->cachedConfig['organisation'] ?? 'default';
}
- }
+ }//end getCurrentOrganisation()
/**
* Get AMEF configuration from app config
@@ -1692,36 +1867,39 @@ public function getAmefConfig(): array
$this->logger->info('Getting AMEF configuration');
try {
- // Get configuration from app config using the correct method
- $config = $this->config->getValueString('softwarecatalog', 'amef_config', '{}');
+ // Get configuration from app config using the correct method.
+ $config = $this->config->getValueString('softwarecatalog', 'amef_config', '{}');
$decoded = json_decode($config, true);
- if (!is_array($decoded)) {
- // Fallback to individual config values for backward compatibility
+ if (is_array($decoded) === false) {
+ // Fallback to individual config values for backward compatibility.
$decoded = [
- 'register_id' => $this->config->getValueString('softwarecatalog', 'amef_register', ''),
- 'model_schema_id' => $this->config->getValueString('softwarecatalog', 'amef_model_schema', ''),
- 'elements_schema' => $this->config->getValueString('softwarecatalog', 'amef_elements_schema', ''),
- 'relationships_schema' => $this->config->getValueString('softwarecatalog', 'amef_relationships_schema', ''),
- 'views_schema' => $this->config->getValueString('softwarecatalog', 'amef_views_schema', ''),
- 'organizations_schema' => $this->config->getValueString('softwarecatalog', 'amef_organizations_schema', ''),
- 'folders_schema' => $this->config->getValueString('softwarecatalog', 'amef_folders_schema', ''),
- 'property_definitions_schema' => $this->config->getValueString('softwarecatalog', 'amef_property_definitions_schema', '')
+ 'register_id' => $this->config->getValueString('softwarecatalog', 'amef_register', ''),
+ 'model_schema_id' => $this->config->getValueString('softwarecatalog', 'amef_model_schema', ''),
+ 'elements_schema' => $this->config->getValueString('softwarecatalog', 'amef_elements_schema', ''),
+ 'relationships_schema' => $this->config->getValueString('softwarecatalog', 'amef_relationships_schema', ''),
+ 'views_schema' => $this->config->getValueString('softwarecatalog', 'amef_views_schema', ''),
+ 'organizations_schema' => $this->config->getValueString('softwarecatalog', 'amef_organizations_schema', ''),
+ 'folders_schema' => $this->config->getValueString('softwarecatalog', 'amef_folders_schema', ''),
+ 'property_definitions_schema' => $this->config->getValueString('softwarecatalog', 'amef_property_definitions_schema', ''),
];
}
return $decoded;
} catch (\Exception $e) {
- $this->logger->error('Failed to get AMEF configuration', [
- 'error' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to get AMEF configuration',
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
return [
'success' => false,
- 'error' => $e->getMessage()
+ 'error' => $e->getMessage(),
];
- }
- }
+ }//end try
+ }//end getAmefConfig()
/**
* Get AMEF register ID from configuration
@@ -1730,28 +1908,33 @@ public function getAmefConfig(): array
*/
private function getAmefRegisterId(): ?int
{
- // Retrieve AMEF configuration
+ // Retrieve AMEF configuration.
$amefConfig = $this->getAmefConfig();
- // Try JSON config keys first: support both 'register_id' and 'register'
- $rawRegisterId = $amefConfig['register_id']
- ?? $amefConfig['register']
- ?? null;
+ // Try JSON config keys first: support both 'register_id' and 'register'.
+ $rawRegisterId = $amefConfig['register_id'] ?? $amefConfig['register'] ?? null;
- // Fallback to legacy individual app config keys if not present in JSON
+ // Fallback to legacy individual app config keys if not present in JSON.
if ($rawRegisterId === null || $rawRegisterId === '') {
- $rawRegisterId = $this->config->getValueString('softwarecatalog', 'amef_register', '')
- ?: $this->config->getValueString('softwarecatalog', 'amef_register_id', '');
+ if ($this->config->getValueString('softwarecatalog', 'amef_register', '')) {
+ $rawRegisterId = $this->config->getValueString('softwarecatalog', 'amef_register', '');
+ } else {
+ $rawRegisterId = $this->config->getValueString('softwarecatalog', 'amef_register_id', '');
+ }
}
- // Validate and normalize to positive int
- if ($rawRegisterId !== null && $rawRegisterId !== '' && is_numeric((string) $rawRegisterId)) {
+ // Validate and normalize to positive int.
+ if ($rawRegisterId !== null && $rawRegisterId !== '' && is_numeric((string) $rawRegisterId) === true) {
$registerId = (int) $rawRegisterId;
- return $registerId > 0 ? $registerId : null;
+ if ($registerId > 0) {
+ return $registerId;
+ } else {
+ return null;
+ }
}
return null;
- }
+ }//end getAmefRegisterId()
/**
* Get AMEF schema ID for a specific ArchiMate type
@@ -1759,45 +1942,46 @@ private function getAmefRegisterId(): ?int
* This method retrieves the schema ID for a given ArchiMate type from the AMEF configuration.
* It looks for the schema ID using the pattern '{type}_schema' in the configuration.
*
- * @param string $archiMateType The ArchiMate type (e.g., 'element', 'organization', 'relationship')
+ * @param string $archiMateType The ArchiMate type (e.g., 'element', 'organization', 'relationship')
+ *
* @return int|null The schema ID for the given type or null if not configured
*/
private function getAmefSchemaIdForType(string $archiMateType): ?int
{
- // Get AMEF configuration
+ // Get AMEF configuration.
$amefConfig = $this->getAmefConfig();
- // Normalize plural → singular and handle the actual config structure
- $typeMapping = [
- 'elements' => 'element',
- 'organizations' => 'organization',
- // Accept both 'relationships' (AMEF wording) and UI term 'relation'
- 'relationships' => 'relation',
- 'views' => 'view',
- 'models' => 'model',
- // NOTE: 'properties' mapping removed - properties are never root-level AMEF objects
- 'property_definitions' => 'property_definition'
+ // Normalize plural → singular and handle the actual config structure.
+ $typeMapping = [
+ 'elements' => 'element',
+ 'organizations' => 'organization',
+ // Accept both 'relationships' (AMEF wording) and UI term 'relation'.
+ 'relationships' => 'relation',
+ 'views' => 'view',
+ 'models' => 'model',
+ // NOTE: 'properties' mapping removed - properties are never root-level AMEF objects.
+ 'property_definitions' => 'property_definition',
];
$normalizedType = $typeMapping[$archiMateType] ?? $archiMateType;
- // Candidate keys: match the actual config structure
+ // Candidate keys: match the actual config structure.
$schemaKeyCandidatesByType = [
- 'element' => ['element_schema'],
- 'organization' => ['organization_schema'],
- 'relationship' => ['relation_schema'],
- 'view' => ['view_schema'],
- 'model' => ['model_schema'],
- 'property_definition' => ['property_definition_schema']
- // NOTE: 'property' removed - properties are never root-level AMEF objects, only nested within other elements
+ 'element' => ['element_schema'],
+ 'organization' => ['organization_schema'],
+ 'relationship' => ['relation_schema'],
+ 'view' => ['view_schema'],
+ 'model' => ['model_schema'],
+ 'property_definition' => ['property_definition_schema'],
+ // NOTE: 'property' removed - properties are never root-level AMEF objects, only nested within other elements.
];
- $candidates = $schemaKeyCandidatesByType[$normalizedType] ?? [$normalizedType . '_schema'];
+ $candidates = $schemaKeyCandidatesByType[$normalizedType] ?? [$normalizedType.'_schema'];
- // Try JSON config with the actual keys
+ // Try JSON config with the actual keys.
foreach ($candidates as $key) {
- if (array_key_exists($key, $amefConfig)) {
+ if (array_key_exists($key, $amefConfig) === true) {
$raw = $amefConfig[$key];
- if ($raw !== '' && $raw !== null && is_numeric((string) $raw)) {
+ if ($raw !== '' && $raw !== null && is_numeric((string) $raw) === true) {
$id = (int) $raw;
if ($id > 0) {
return $id;
@@ -1806,11 +1990,15 @@ private function getAmefSchemaIdForType(string $archiMateType): ?int
}
}
- // Fallback to legacy individual app config keys if not present in JSON
+ // Fallback to legacy individual app config keys if not present in JSON.
foreach ($candidates as $key) {
- $raw = $this->config->getValueString('softwarecatalog', 'amef_' . $key, '')
- ?: $this->config->getValueString('softwarecatalog', $key, '');
- if ($raw !== '' && is_numeric((string) $raw)) {
+ if ($this->config->getValueString('softwarecatalog', 'amef_'.$key, '')) {
+ $raw = $this->config->getValueString('softwarecatalog', 'amef_'.$key, '');
+ } else {
+ $raw = $this->config->getValueString('softwarecatalog', $key, '');
+ }
+
+ if ($raw !== '' && is_numeric((string) $raw) === true) {
$id = (int) $raw;
if ($id > 0) {
return $id;
@@ -1819,80 +2007,83 @@ private function getAmefSchemaIdForType(string $archiMateType): ?int
}
return null;
- }
+ }//end getAmefSchemaIdForType()
/**
* Get schema ID for a section using SettingsService (no hardcoded fallbacks)
*
- * @param string $section Section name
+ * @param string $section Section name
+ *
* @return int Schema ID
* @throws \RuntimeException If schema ID is not configured
*/
private function getSchemaIdForSection(string $section): int
{
- // Map section names to object types for SettingsService
+ // Map section names to object types for SettingsService.
$objectTypeMapping = [
- 'elements' => 'element',
- 'relationships' => 'relationship',
- 'views' => 'view',
- 'organizations' => 'organization',
- 'property_definitions' => 'property_definition'
+ 'elements' => 'element',
+ 'relationships' => 'relationship',
+ 'views' => 'view',
+ 'organizations' => 'organization',
+ 'property_definitions' => 'property_definition',
];
$objectType = $objectTypeMapping[$section] ?? $section;
- $schemaId = $this->settingsService->getSchemaIdForObjectType($objectType);
+ $schemaId = $this->settingsService->getSchemaIdForObjectType($objectType);
- // Ensure schema ID is configured - no hardcoded fallbacks
+ // Ensure schema ID is configured - no hardcoded fallbacks.
if ($schemaId === null) {
throw new \RuntimeException("Schema ID for section '{$section}' is not configured. Please configure all AMEF schema IDs via the admin interface. Expected object type: '{$objectType}'");
}
return $schemaId;
- }
+ }//end getSchemaIdForSection()
/**
* Extract propertyDefinitions from the parsed XML and build a map
*
- * @param array $data Parsed XML data
+ * @param array $data Parsed XML data
+ *
* @return array Map of propertyDefinitionRef => property name
*/
private function extractPropertyDefinitionMap(array $data): array
{
- // OPTIMIZATION: Return cached property definition map if available
+ // OPTIMIZATION: Return cached property definition map if available.
if ($this->propertyDefinitionMapCache !== null) {
return $this->propertyDefinitionMapCache;
}
$map = [];
- // Find propertyDefinitions section (handle possible alternative names)
+ // Find propertyDefinitions section (handle possible alternative names).
$propertyDefs = null;
- if (isset($data['propertyDefinitions'])) {
+ if (isset($data['propertyDefinitions']) === true) {
$propertyDefs = $data['propertyDefinitions'];
- } elseif (isset($data['property_definitions'])) {
+ } else if (isset($data['property_definitions']) === true) {
$propertyDefs = $data['property_definitions'];
- } elseif (isset($data['propertyDefinitions'])) {
+ } else if (isset($data['propertyDefinitions']) === true) {
$propertyDefs = $data['propertyDefinitions'];
}
- if ($propertyDefs && isset($propertyDefs['propertyDefinition'])) {
+
+ if ($propertyDefs !== false && isset($propertyDefs['propertyDefinition']) === true) {
$defs = $propertyDefs['propertyDefinition'];
- if (isset($defs[0])) {
- // Array of propertyDefinition
+ if (isset($defs[0]) === true) {
+ // Array of propertyDefinition.
foreach ($defs as $def) {
- if (isset($def['_attributes']['identifier']) && isset($def['name'])) {
- $map[$def['_attributes']['identifier']] = is_array($def['name']) && isset($def['name']['_value']) ? $def['name']['_value'] : $def['name'];
+ if (isset($def['_attributes']['identifier']) === true && isset($def['name']) === true) {
+ $map[$def['_attributes']['identifier']] = is_array($def['name']) === true && isset($def['name']['_value']) === true ? $def['name']['_value'] : $def['name'];
}
}
- } elseif (isset($defs['_attributes']['identifier']) && isset($defs['name'])) {
- // Single propertyDefinition
- $map[$defs['_attributes']['identifier']] = is_array($defs['name']) && isset($defs['name']['_value']) ? $defs['name']['_value'] : $defs['name'];
+ } else if (isset($defs['_attributes']['identifier']) === true && isset($defs['name']) === true) {
+ // Single propertyDefinition.
+ $map[$defs['_attributes']['identifier']] = is_array($defs['name']) === true && isset($defs['name']['_value']) === true ? $defs['name']['_value'] : $defs['name'];
}
}
- // OPTIMIZATION: Cache the result for subsequent calls during the same import
+ // OPTIMIZATION: Cache the result for subsequent calls during the same import.
$this->propertyDefinitionMapCache = $map;
return $map;
- }
+ }//end extractPropertyDefinitionMap()
/**
* Get property mapping information for debugging and reference
@@ -1900,7 +2091,8 @@ private function extractPropertyDefinitionMap(array $data): array
* This method returns a mapping of original property names to their camelCase equivalents
* which can be useful for understanding how properties are being processed.
*
- * @param array $propertyDefinitionMap The original property definition map
+ * @param array $propertyDefinitionMap The original property definition map
+ *
* @return array Mapping of original names to camelCase names
*/
public function getPropertyNameMapping(array $propertyDefinitionMap): array
@@ -1908,15 +2100,16 @@ public function getPropertyNameMapping(array $propertyDefinitionMap): array
$mapping = [];
foreach ($propertyDefinitionMap as $propertyRef => $originalName) {
- // Skip non-string values (e.g., empty arrays from incomplete property definitions)
- if (!is_string($originalName)) {
+ // Skip non-string values (e.g., empty arrays from incomplete property definitions).
+ if (is_string($originalName) === false) {
continue;
}
- $mapping[$originalName] = $this->convertToCamelCase($originalName);
+
+ $mapping[$originalName] = $this->convertToCamelCase(propertyName: $originalName);
}
return $mapping;
- }
+ }//end getPropertyNameMapping()
/**
* Convert property names with spaces to camelCase for better database compatibility
@@ -1926,27 +2119,28 @@ public function getPropertyNameMapping(array $propertyDefinitionMap): array
* - "Business Unit" -> "businessUnit"
* - "System Name" -> "systemName"
*
- * @param string $propertyName Property name that may contain spaces
+ * @param string $propertyName Property name that may contain spaces
+ *
* @return string CamelCase version of the property name
*/
private function convertToCamelCase(string $propertyName): string
{
- // OPTIMIZATION: Check cache first to avoid redundant conversions
- if (isset($this->camelCaseCache[$propertyName])) {
+ // OPTIMIZATION: Check cache first to avoid redundant conversions.
+ if (isset($this->camelCaseCache[$propertyName]) === true) {
return $this->camelCaseCache[$propertyName];
}
- // Remove any leading/trailing whitespace
+ // Remove any leading/trailing whitespace.
$propertyName = trim($propertyName);
- // Split by spaces and convert to camelCase
+ // Split by spaces and convert to camelCase.
$words = explode(' ', $propertyName);
if (count($words) === 1) {
- // Single word, just lowercase it
+ // Single word, just lowercase it.
$result = strtolower($words[0]);
} else {
- // First word is lowercase, subsequent words are capitalized
+ // First word is lowercase, subsequent words are capitalized.
$camelCase = strtolower($words[0]);
for ($i = 1; $i < count($words); $i++) {
@@ -1956,11 +2150,11 @@ private function convertToCamelCase(string $propertyName): string
$result = $camelCase;
}
- // OPTIMIZATION: Cache the result for future use
+ // OPTIMIZATION: Cache the result for future use.
$this->camelCaseCache[$propertyName] = $result;
return $result;
- }
+ }//end convertToCamelCase()
/**
* Build statistics structure from ObjectService save result.
@@ -1972,43 +2166,43 @@ private function buildStatisticsFromSaveResult(): array
{
// Initialize empty statistics.
$statistics = [
- 'elements' => [
- 'created' => 0,
- 'updated' => 0,
+ 'elements' => [
+ 'created' => 0,
+ 'updated' => 0,
'unchanged' => 0,
- 'errors' => []
+ 'errors' => [],
],
- 'relationships' => [
- 'created' => 0,
- 'updated' => 0,
+ 'relationships' => [
+ 'created' => 0,
+ 'updated' => 0,
'unchanged' => 0,
- 'errors' => []
+ 'errors' => [],
],
- 'organizations' => [
- 'created' => 0,
- 'updated' => 0,
+ 'organizations' => [
+ 'created' => 0,
+ 'updated' => 0,
'unchanged' => 0,
- 'errors' => []
+ 'errors' => [],
],
- 'views' => [
- 'created' => 0,
- 'updated' => 0,
+ 'views' => [
+ 'created' => 0,
+ 'updated' => 0,
'unchanged' => 0,
- 'errors' => []
+ 'errors' => [],
],
'property_definitions' => [
- 'created' => 0,
- 'updated' => 0,
+ 'created' => 0,
+ 'updated' => 0,
'unchanged' => 0,
- 'errors' => []
+ 'errors' => [],
],
- 'summary' => [
- 'total_objects_created' => 0,
- 'total_objects_updated' => 0,
- 'total_objects_deleted' => 0,
+ 'summary' => [
+ 'total_objects_created' => 0,
+ 'total_objects_updated' => 0,
+ 'total_objects_deleted' => 0,
'total_objects_unchanged' => 0,
- 'total_errors' => 0
- ]
+ 'total_errors' => 0,
+ ],
];
// If no save result available, return empty statistics.
@@ -2018,38 +2212,41 @@ private function buildStatisticsFromSaveResult(): array
$saveResult = $this->lastSaveResult;
- // Use per-schema counts if available (reliable — objects are saved per-schema-group,
+ // Use per-schema counts if available (reliable — objects are saved per-schema-group,.
// so we know which schema each count belongs to without inspecting serialized objects).
- if (!empty($saveResult['countsBySchema']) && $this->cachedConfig !== null) {
+ if (empty($saveResult['countsBySchema']) === false && $this->cachedConfig !== null) {
$sectionMap = [
- 'model' => 'elements',
- 'element' => 'elements',
- 'relationship' => 'relationships',
- 'organization' => 'organizations',
- 'view' => 'views',
+ 'model' => 'elements',
+ 'element' => 'elements',
+ 'relationship' => 'relationships',
+ 'organization' => 'organizations',
+ 'view' => 'views',
'property_definition' => 'property_definitions',
];
- // Build reverse map: schema ID → plural section name
+ // Build reverse map: schema ID → plural section name.
$schemaToSection = [];
foreach ($this->cachedConfig['schemaIds'] as $type => $schemaId) {
- $schemaToSection[(int)$schemaId] = $sectionMap[$type] ?? 'elements';
+ $schemaToSection[(int) $schemaId] = $sectionMap[$type] ?? 'elements';
}
foreach ($saveResult['countsBySchema'] as $schemaId => $counts) {
- $sectionKey = $schemaToSection[(int)$schemaId] ?? 'elements';
- $statistics[$sectionKey]['created'] += $counts['saved'] ?? 0;
- $statistics[$sectionKey]['updated'] += $counts['updated'] ?? 0;
+ $sectionKey = $schemaToSection[(int) $schemaId] ?? 'elements';
+ $statistics[$sectionKey]['created'] += $counts['saved'] ?? 0;
+ $statistics[$sectionKey]['updated'] += $counts['updated'] ?? 0;
$statistics[$sectionKey]['unchanged'] += $counts['unchanged'] ?? 0;
if (($counts['invalid'] ?? 0) > 0) {
$statistics[$sectionKey]['errors'][] = "{$counts['invalid']} validation error(s)";
}
}
- }
+ }//end if
- // Calculate summary totals
+ // Calculate summary totals.
foreach ($statistics as $section => $sectionStats) {
- if ($section === 'summary') continue;
+ if ($section === 'summary') {
+ continue;
+ }
+
$statistics['summary']['total_objects_created'] += $sectionStats['created'];
$statistics['summary']['total_objects_updated'] += $sectionStats['updated'];
$statistics['summary']['total_objects_unchanged'] += $sectionStats['unchanged'];
@@ -2057,214 +2254,216 @@ private function buildStatisticsFromSaveResult(): array
}
return $statistics;
- }
+ }//end buildStatisticsFromSaveResult()
/**
* Calculate optimized statistics for performance reporting
*
- * @param array $savedObjects Saved objects from ObjectService::saveObjects
+ * @param array $savedObjects Saved objects from ObjectService::saveObjects
+ *
* @return array Statistics array
*/
private function calculateOptimizedStatistics(array $savedObjects): array
{
// Initialize statistics structure for detailed error extraction.
$statistics = [
- 'elements' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []],
- 'organizations' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []],
- 'relationships' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []],
- 'views' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []],
+ 'elements' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []],
+ 'organizations' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []],
+ 'relationships' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []],
+ 'views' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []],
'property_definitions' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []],
- 'summary' => [
- 'total_objects_created' => 0,
- 'total_objects_updated' => 0,
- 'total_objects_deleted' => 0,
+ 'summary' => [
+ 'total_objects_created' => 0,
+ 'total_objects_updated' => 0,
+ 'total_objects_deleted' => 0,
'total_objects_unchanged' => 0,
- 'total_errors' => 0
- ]
+ 'total_errors' => 0,
+ ],
];
-
+
if ($this->lastSaveResult !== null) {
$saveResult = $this->lastSaveResult;
- // Use per-schema counts if available (reliable — doesn't depend on serialized object fields)
- if (!empty($saveResult['countsBySchema']) && $this->cachedConfig !== null) {
+ // Use per-schema counts if available (reliable — doesn't depend on serialized object fields).
+ if (empty($saveResult['countsBySchema']) === false && $this->cachedConfig !== null) {
$sectionMap = [
- 'model' => 'elements',
- 'element' => 'elements',
- 'relationship' => 'relationships',
- 'organization' => 'organizations',
- 'view' => 'views',
+ 'model' => 'elements',
+ 'element' => 'elements',
+ 'relationship' => 'relationships',
+ 'organization' => 'organizations',
+ 'view' => 'views',
'property_definition' => 'property_definitions',
];
$schemaToSection = [];
foreach ($this->cachedConfig['schemaIds'] as $type => $schemaId) {
- $schemaToSection[(int)$schemaId] = $sectionMap[$type] ?? 'elements';
+ $schemaToSection[(int) $schemaId] = $sectionMap[$type] ?? 'elements';
}
foreach ($saveResult['countsBySchema'] as $schemaId => $counts) {
- $sectionKey = $schemaToSection[(int)$schemaId] ?? 'elements';
- $statistics[$sectionKey]['created'] += $counts['saved'] ?? 0;
- $statistics[$sectionKey]['updated'] += $counts['updated'] ?? 0;
+ $sectionKey = $schemaToSection[(int) $schemaId] ?? 'elements';
+ $statistics[$sectionKey]['created'] += $counts['saved'] ?? 0;
+ $statistics[$sectionKey]['updated'] += $counts['updated'] ?? 0;
$statistics[$sectionKey]['unchanged'] += $counts['unchanged'] ?? 0;
if (($counts['invalid'] ?? 0) > 0) {
$statistics[$sectionKey]['errors'][] = "{$counts['invalid']} validation error(s)";
}
}
- }
+ }//end if
- // Calculate summary totals
+ // Calculate summary totals.
$summary = [
- 'total_objects_created' => 0,
- 'total_objects_updated' => 0,
- 'total_objects_deleted' => 0,
+ 'total_objects_created' => 0,
+ 'total_objects_updated' => 0,
+ 'total_objects_deleted' => 0,
'total_objects_unchanged' => 0,
- 'total_errors' => 0
+ 'total_errors' => 0,
];
foreach ($statistics as $section => $sectionStats) {
if ($section !== 'summary') {
- $summary['total_objects_created'] += $sectionStats['created'];
- $summary['total_objects_updated'] += $sectionStats['updated'];
+ $summary['total_objects_created'] += $sectionStats['created'];
+ $summary['total_objects_updated'] += $sectionStats['updated'];
$summary['total_objects_unchanged'] += $sectionStats['unchanged'];
- $summary['total_errors'] += count($sectionStats['errors']);
+ $summary['total_errors'] += count($sectionStats['errors']);
}
}
$statistics['summary'] = $summary;
- }
+ }//end if
return $statistics;
- }
+ }//end calculateOptimizedStatistics()
/**
* Get section structure configuration for XML parsing
*
- * @param string $sectionName The name of the section (e.g., 'elements', 'relationships', 'views', etc.)
+ * @param string $sectionName The name of the section (e.g., 'elements', 'relationships', 'views', etc.)
+ *
* @return array Configuration with direct_tags and nested_paths for finding items
*/
private function getSectionStructureConfig(string $sectionName): array
{
- // Define the structure configuration for each section type
+ // Define the structure configuration for each section type.
$configs = [
- 'elements' => [
- 'direct_tags' => ['element', 'elements'],
+ 'elements' => [
+ 'direct_tags' => ['element', 'elements'],
'nested_paths' => [
['model', 'elements', 'element'],
['model', 'elements'],
['elements', 'element'],
- ['elements']
- ]
+ ['elements'],
+ ],
],
- 'relationships' => [
- 'direct_tags' => ['relationship', 'relationships'],
+ 'relationships' => [
+ 'direct_tags' => ['relationship', 'relationships'],
'nested_paths' => [
['model', 'relationships', 'relationship'],
['model', 'relationships'],
['relationships', 'relationship'],
- ['relationships']
- ]
+ ['relationships'],
+ ],
],
- 'views' => [
- 'direct_tags' => ['view', 'views', 'diagram', 'diagrams'],
+ 'views' => [
+ 'direct_tags' => ['view', 'views', 'diagram', 'diagrams'],
'nested_paths' => [
['model', 'views', 'diagrams', 'view'],
['model', 'views', 'diagrams'],
['model', 'views'],
['views', 'diagrams', 'view'],
['views', 'diagrams'],
- ['views']
- ]
+ ['views'],
+ ],
],
- 'organizations' => [
- 'direct_tags' => ['item', 'items'],
+ 'organizations' => [
+ 'direct_tags' => ['item', 'items'],
'nested_paths' => [
['model', 'organizations', 'item'],
['model', 'organizations'],
['organizations', 'item'],
- ['organizations']
- ]
+ ['organizations'],
+ ],
],
'property_definitions' => [
- 'direct_tags' => ['propertyDefinition', 'propertyDefinitions'],
+ 'direct_tags' => ['propertyDefinition', 'propertyDefinitions'],
'nested_paths' => [
['model', 'propertyDefinitions', 'propertyDefinition'],
['model', 'propertyDefinitions'],
['propertyDefinitions', 'propertyDefinition'],
- ['propertyDefinitions']
- ]
- ]
+ ['propertyDefinitions'],
+ ],
+ ],
];
return $configs[$sectionName] ?? [
- 'direct_tags' => [$sectionName],
- 'nested_paths' => [[$sectionName]]
+ 'direct_tags' => [$sectionName],
+ 'nested_paths' => [[$sectionName]],
];
- }
+ }//end getSectionStructureConfig()
/**
- * Check if an array is associative (has string keys)
+ * Check if an array is associative (has string keys).
+ *
+ * @param mixed $value The value to check.
*
- * @param array $array The array to check
* @return bool True if associative, false if indexed
*/
private function isAssociativeArray(array $array): bool
{
return count(array_filter(array_keys($array), 'is_string')) > 0;
- }
+ }//end isAssociativeArray()
/**
* Find items within a specific section using AMEF configuration
*
- * @param array $sectionData The section data to search
- * @param string $sectionName The name of the section
+ * @param array $sectionData The section data to search
+ * @param string $sectionName The name of the section
+ *
* @return array Array of items found
*/
private function findItemsInSection(array $sectionData, string $sectionName): array
{
- // OPTIMIZATION: Removed debug logging from section processing
-
+ // OPTIMIZATION: Removed debug logging from section processing.
$items = [];
- // Safety check: ensure sectionData is an array
- if (!is_array($sectionData)) {
+ // Safety check: ensure sectionData is an array.
+ if (is_array($sectionData) === false) {
return [];
}
- // Get section structure configuration from AMEF config
- $config = $this->getSectionStructureConfig($sectionName);
+ // Get section structure configuration from AMEF config.
+ $config = $this->getSectionStructureConfig(sectionName: $sectionName);
- // Special handling for views with diagrams structure
+ // Special handling for views with diagrams structure.
if ($sectionName === 'views') {
-
- // Handle nested structure:
- if (isset($sectionData['diagrams'])) {
- if (isset($sectionData['diagrams']['view'])) {
+ // Handle nested structure: .
+ if (isset($sectionData['diagrams']) === true) {
+ if (isset($sectionData['diagrams']['view']) === true) {
$viewArray = $sectionData['diagrams']['view'];
- // Handle single view vs array of views
- if (!isset($viewArray[0]) && isset($viewArray['_attributes'])) {
- // Single view
+ // Handle single view vs array of views.
+ if (isset($viewArray[0]) === false && isset($viewArray['_attributes']) === true) {
+ // Single view.
$items = [$viewArray];
} else {
- // Array of views
+ // Array of views.
$items = $viewArray;
}
}
} else {
- // Direct views structure (fallback)
- if (isset($sectionData['view'])) {
+ // Direct views structure (fallback).
+ if (isset($sectionData['view']) === true) {
$items = $sectionData['view'];
}
}
} else {
- // Try to find items using the configured paths for other sections
+ // Try to find items using the configured paths for other sections.
foreach ($config['nested_paths'] as $path) {
$currentData = $sectionData;
- $pathValid = true;
+ $pathValid = true;
foreach ($path as $key) {
- if (isset($currentData[$key])) {
+ if (isset($currentData[$key]) === true) {
$currentData = $currentData[$key];
} else {
$pathValid = false;
@@ -2272,86 +2471,88 @@ private function findItemsInSection(array $sectionData, string $sectionName): ar
}
}
- if ($pathValid && is_array($currentData)) {
- // Check if this is a direct array of items or needs further processing
- if (isset($currentData[0]) || $this->isAssociativeArray($currentData)) {
+ if ($pathValid !== false && is_array($currentData) === true) {
+ // Check if this is a direct array of items or needs further processing.
+ if (isset($currentData[0]) === true || $this->isAssociativeArray(array: $currentData) === true) {
$items = $currentData;
break;
}
}
- }
- }
+ }//end foreach
+ }//end if
- // If no items found through nested paths, try direct tags
- if (empty($items)) {
+ // If no items found through nested paths, try direct tags.
+ if (empty($items) === true) {
foreach ($config['direct_tags'] as $tag) {
- if (isset($sectionData[$tag])) {
+ if (isset($sectionData[$tag]) === true) {
$items = $sectionData[$tag];
break;
}
}
}
- // If still no items found, treat the section itself as items
- if (empty($items)) {
+ // If still no items found, treat the section itself as items.
+ if (empty($items) === true) {
$items = [$sectionData];
}
- // Ensure items is always an array
- if (!is_array($items)) {
+ // Ensure items is always an array.
+ if (is_array($items) === false) {
$items = [$items];
}
- // If items is an associative array with numeric keys, convert to indexed array
- if ($this->isAssociativeArray($items)) {
+ // If items is an associative array with numeric keys, convert to indexed array.
+ if ($this->isAssociativeArray(array: $items) === true) {
$items = array_values($items);
}
return $items;
- }
+ }//end findItemsInSection()
/**
* Extract identifier from item data
*
- * @param array $item Item data
- * @param string $sectionName The section name for special handling
+ * @param array $item Item data
+ * @param string $sectionName The section name for special handling
+ *
* @return string|null Identifier or null if not found
*/
- private function extractIdentifier(array $item, string $sectionName = ''): ?string
+ private function extractIdentifier(array $item, string $sectionName=''): ?string
{
- // OPTIMIZATION: Use cached patterns for section-specific identifier extraction
- if (isset($this->identifierPatternCache[$sectionName])) {
+ // OPTIMIZATION: Use cached patterns for section-specific identifier extraction.
+ if (isset($this->identifierPatternCache[$sectionName]) === true) {
$patterns = $this->identifierPatternCache[$sectionName];
- // Try cached patterns in order of success frequency
+ // Try cached patterns in order of success frequency.
foreach ($patterns as $pattern) {
- $result = $this->extractIdentifierByPattern($item, $pattern);
+ $result = $this->extractIdentifierByPattern(item: $item, pattern: $pattern);
if ($result !== null) {
return $result;
}
}
}
- // OPTIMIZATION: Build pattern cache on first encounter of section type
- $patterns = $this->buildIdentifierPatternsForSection($sectionName);
+ // OPTIMIZATION: Build pattern cache on first encounter of section type.
+ $patterns = $this->buildIdentifierPatternsForSection(sectionName: $sectionName);
$this->identifierPatternCache[$sectionName] = $patterns;
- // Try all patterns and return first successful match
+ // Try all patterns and return first successful match.
foreach ($patterns as $pattern) {
- $result = $this->extractIdentifierByPattern($item, $pattern);
+ $result = $this->extractIdentifierByPattern(item: $item, pattern: $pattern);
if ($result !== null) {
return $result;
}
}
return null;
- }
+ }//end extractIdentifier()
/**
* OPTIMIZATION: Extract identifier using a specific pattern
*
- * @param array $item The item to extract from
- * @param array $pattern The extraction pattern ['path' => string[], 'type' => string]
+ * @param array $item The item to extract from
+ * @param array $pattern The extraction pattern ['path' => string[], 'type' => string]
+ *
* @return string|null The extracted identifier or null
*/
private function extractIdentifierByPattern(array $item, array $pattern): ?string
@@ -2359,25 +2560,36 @@ private function extractIdentifierByPattern(array $item, array $pattern): ?strin
$path = $pattern['path'];
$type = $pattern['type'];
- // Navigate to the target location
+ // Navigate to the target location.
$current = $item;
foreach ($path as $key) {
- if (!isset($current[$key])) {
+ if (isset($current[$key]) === false) {
return null;
}
+
$current = $current[$key];
}
- // Extract based on type
+ // Extract based on type.
switch ($type) {
case 'direct':
- return is_string($current) ? $current : null;
+ if (is_string($current) === true) {
+ return $current;
+ } else {
+ return null;
+ }
+
case 'value':
- return is_array($current) && isset($current['_value']) ? (string) $current['_value'] : null;
+ if (is_array($current) === true && isset($current['_value']) === true) {
+ return (string) $current['_value'];
+ } else {
+ return null;
+ }
+
case 'array_search':
- if (is_array($current)) {
+ if (is_array($current) === true) {
foreach ($current as $childItem) {
- if (isset($childItem['_attributes']['identifierRef'])) {
+ if (isset($childItem['_attributes']['identifierRef']) === true) {
return (string) $childItem['_attributes']['identifierRef'];
}
}
@@ -2385,27 +2597,28 @@ private function extractIdentifierByPattern(array $item, array $pattern): ?strin
return null;
default:
return null;
- }
- }
+ }//end switch
+ }//end extractIdentifierByPattern()
/**
* OPTIMIZATION: Build identifier extraction patterns for a section type
*
- * @param string $sectionName The section name
+ * @param string $sectionName The section name
+ *
* @return array Array of extraction patterns ordered by likelihood of success
*/
private function buildIdentifierPatternsForSection(string $sectionName): array
{
$patterns = [];
- // Special handling for organizations
+ // Special handling for organizations.
if ($sectionName === 'organizations') {
$patterns[] = ['path' => ['_attributes', 'identifierRef'], 'type' => 'direct'];
$patterns[] = ['path' => ['item'], 'type' => 'array_search'];
$patterns[] = ['path' => ['label'], 'type' => 'value'];
$patterns[] = ['path' => ['label'], 'type' => 'direct'];
} else {
- // Standard patterns for other sections (ordered by frequency in ArchiMate)
+ // Standard patterns for other sections (ordered by frequency in ArchiMate).
$patterns[] = ['path' => ['_attributes', 'identifier'], 'type' => 'direct'];
$patterns[] = ['path' => ['_attributes', 'id'], 'type' => 'direct'];
$patterns[] = ['path' => ['identifier'], 'type' => 'value'];
@@ -2418,7 +2631,7 @@ private function buildIdentifierPatternsForSection(string $sectionName): array
}
return $patterns;
- }
+ }//end buildIdentifierPatternsForSection()
/**
* OPTIMIZATION: Extract only essential XML data to reduce memory usage by 20-30%
@@ -2427,71 +2640,73 @@ private function buildIdentifierPatternsForSection(string $sectionName): array
* the essential data needed for round-trip fidelity and export functionality.
* For view objects, element splicing is performed if elements lookup is provided.
*
- * @param array $item The complete XML item data
- * @param array $elementsLookup Optional elements lookup for view processing
- * @param string $schemaType Schema type for conditional processing
+ * @param array $item The complete XML item data
+ * @param array $elementsLookup Optional elements lookup for view processing
+ * @param string $schemaType Schema type for conditional processing
+ *
* @return array Essential XML data for storage
*/
- private function extractEssentialXmlData(array $item, array $elementsLookup = [], string $schemaType = ''): array
+ private function extractEssentialXmlData(array $item, array $elementsLookup=[], string $schemaType=''): array
{
$essential = [];
- // Always preserve core attributes (needed for export)
- if (isset($item['_attributes'])) {
+ // Always preserve core attributes (needed for export).
+ if (isset($item['_attributes']) === true) {
$essential['_attributes'] = $item['_attributes'];
}
- // Preserve name and documentation (already extracted to root level but needed for export)
- if (isset($item['name'])) {
+ // Preserve name and documentation (already extracted to root level but needed for export).
+ if (isset($item['name']) === true) {
$essential['name'] = $item['name'];
}
- if (isset($item['documentation'])) {
+ if (isset($item['documentation']) === true) {
$essential['documentation'] = $item['documentation'];
}
- // Preserve properties structure (needed for property mapping)
- if (isset($item['properties'])) {
+ // Preserve properties structure (needed for property mapping).
+ if (isset($item['properties']) === true) {
$essential['properties'] = $item['properties'];
}
- // For relationships, preserve source/target information
- if (isset($item['source'])) {
+ // For relationships, preserve source/target information.
+ if (isset($item['source']) === true) {
$essential['source'] = $item['source'];
}
- if (isset($item['target'])) {
+ if (isset($item['target']) === true) {
$essential['target'] = $item['target'];
}
- // Preserve any other critical ArchiMate-specific fields
+ // Preserve any other critical ArchiMate-specific fields.
$criticalFields = ['type', 'viewpoint', 'accessType', 'isDirected'];
foreach ($criticalFields as $field) {
- if (isset($item[$field])) {
+ if (isset($item[$field]) === true) {
$essential[$field] = $item[$field];
}
}
- // Preserve original node/connection arrays for views (needed by export's addViewDataToXmlNode)
- if (isset($item['node'])) {
+ // Preserve original node/connection arrays for views (needed by export's addViewDataToXmlNode).
+ if (isset($item['node']) === true) {
$essential['node'] = $item['node'];
}
- if (isset($item['connection'])) {
+
+ if (isset($item['connection']) === true) {
$essential['connection'] = $item['connection'];
}
- // Extract flattened viewNodes/viewRelationships for frontend consumption
+ // Extract flattened viewNodes/viewRelationships for frontend consumption.
if ($schemaType === 'view') {
- $this->extractViewNodesAndConnections($item, $essential, $elementsLookup);
+ $this->extractViewNodesAndConnections(item: $item, essential: $essential, elementsLookup: $elementsLookup);
} else {
- $this->extractViewNodesAndConnections($item, $essential);
+ $this->extractViewNodesAndConnections(item: $item, essential: $essential);
}
- // Add a marker to indicate this is essential data (for debugging)
+ // Add a marker to indicate this is essential data (for debugging).
$essential['_essential_data'] = true;
return $essential;
- }
+ }//end extractEssentialXmlData()
/**
* Extract view nodes and relationships for view objects with proper JSON structure
@@ -2499,28 +2714,29 @@ private function extractEssentialXmlData(array $item, array $elementsLookup = []
* This method extracts and transforms nodes and connections from view XML data into
* the standardized viewNodes and viewRelationships format used by the frontend.
*
- * @param array $item The complete XML item data
- * @param array &$essential Essential XML data to add viewNodes/viewRelationships to (by reference)
- * @param array $elementsLookup Optional lookup array of elements by identifier for enrichment
+ * @param array $item The complete XML item data
+ * @param array &$essential Essential XML data to add viewNodes/viewRelationships to (by reference)
+ * @param array $elementsLookup Optional lookup array of elements by identifier for enrichment
+ *
* @return void
*/
- private function extractViewNodesAndConnections(array $item, array &$essential, array $elementsLookup = []): void
+ private function extractViewNodesAndConnections(array $item, array &$essential, array $elementsLookup=[]): void
{
- // Only process if this looks like a view object (has nodes or connections)
- if (!isset($item['node']) && !isset($item['connection'])) {
+ // Only process if this looks like a view object (has nodes or connections).
+ if (isset($item['node']) === false && isset($item['connection']) === false) {
return;
}
- // Extract viewNodes array with proper JSON structure
- if (isset($item['node'])) {
- $essential['viewNodes'] = $this->extractViewNodesRecursively($item['node'], $elementsLookup);
+ // Extract viewNodes array with proper JSON structure.
+ if (isset($item['node']) === true) {
+ $essential['viewNodes'] = $this->extractViewNodesRecursively(nodeData: $item['node'], elementsLookup: $elementsLookup);
}
- // Extract viewRelationships array with proper JSON structure
- if (isset($item['connection'])) {
- $essential['viewRelationships'] = $this->extractViewRelationshipsRecursively($item['connection']);
+ // Extract viewRelationships array with proper JSON structure.
+ if (isset($item['connection']) === true) {
+ $essential['viewRelationships'] = $this->extractViewRelationshipsRecursively(connectionData: $item['connection']);
}
- }
+ }//end extractViewNodesAndConnections()
/**
* Extract view nodes with proper JSON structure for frontend consumption
@@ -2544,135 +2760,167 @@ private function extractViewNodesAndConnections(array $item, array &$essential,
* ]
* ```
*
- * @param array $nodeData Node data (can be single node or array of nodes)
- * @param array $elementsLookup Lookup array of elements by identifier for enrichment
+ * @param array $nodeData Node data (can be single node or array of nodes)
+ * @param array $elementsLookup Lookup array of elements by identifier for enrichment
+ *
* @return array Array of viewNodes with standardized structure including parent references
*/
- private function extractViewNodesRecursively($nodeData, array $elementsLookup = []): array
+ private function extractViewNodesRecursively($nodeData, array $elementsLookup=[]): array
{
$viewNodes = [];
- // Handle both single node and array of nodes
- if (!isset($nodeData[0])) {
- // Single node
+ // Handle both single node and array of nodes.
+ if (isset($nodeData[0]) === false) {
+ // Single node.
$nodeData = [$nodeData];
}
foreach ($nodeData as $node) {
- if (!isset($node['_attributes'])) {
+ if (isset($node['_attributes']) === false) {
continue;
}
- $nodeId = $node['_attributes']['identifier'] ?? null;
+ $nodeId = $node['_attributes']['identifier'] ?? null;
$elementRef = $node['_attributes']['elementRef'] ?? null;
- if (!$nodeId) {
+ if ($nodeId === null) {
continue;
}
- // Create viewNode with standardized structure
+ // Create viewNode with standardized structure.
+ if (isset($node['_attributes']['x']) === true) {
+ $xValue = (int) $node['_attributes']['x'];
+ } else {
+ $xValue = 0;
+ }
+
+ if (isset($node['_attributes']['y']) === true) {
+ $yValue = (int) $node['_attributes']['y'];
+ } else {
+ $yValue = 0;
+ }
+
+ if (isset($node['_attributes']['w']) === true) {
+ $widthValue = (int) $node['_attributes']['w'];
+ } else {
+ $widthValue = 100;
+ }
+
+ if (isset($node['_attributes']['h']) === true) {
+ $heightValue = (int) $node['_attributes']['h'];
+ } else {
+ $heightValue = 50;
+ }
+
$viewNode = [
- 'modelNodeId' => $elementRef, // Reference to the ArchiMate element
- 'viewNodeId' => $nodeId, // Unique identifier within this view
- 'x' => isset($node['_attributes']['x']) ? (int)$node['_attributes']['x'] : 0,
- 'y' => isset($node['_attributes']['y']) ? (int)$node['_attributes']['y'] : 0,
- 'width' => isset($node['_attributes']['w']) ? (int)$node['_attributes']['w'] : 100,
- 'height' => isset($node['_attributes']['h']) ? (int)$node['_attributes']['h'] : 50,
- 'parent' => null, // Will be set to parent nodeId for nested nodes (see recursive processing below)
- 'name' => null,
- 'type' => $this->extractNodeType($node), // Extract type directly from node XML
- 'color' => 'rgb(255, 255, 255)', // Default white background
- 'borderColor' => 'rgb(0, 0, 0)', // Default black border
- 'font' => [
- 'name' => 'Segoe UI, Arial',
- 'size' => 12,
+ 'modelNodeId' => $elementRef,
+ // Reference to the ArchiMate element.
+ 'viewNodeId' => $nodeId,
+ // Unique identifier within this view.
+ 'x' => $xValue,
+ 'y' => $yValue,
+ 'width' => $widthValue,
+ 'height' => $heightValue,
+ 'parent' => null,
+ // Will be set to parent nodeId for nested nodes (see recursive processing below).
+ 'name' => null,
+ 'type' => $this->extractNodeType(node: $node),
+ // Extract type directly from node XML.
+ 'color' => 'rgb(255, 255, 255)',
+ // Default white background.
+ 'borderColor' => 'rgb(0, 0, 0)',
+ // Default black border.
+ 'font' => [
+ 'name' => 'Segoe UI, Arial',
+ 'size' => 12,
'style' => 'normal',
- 'color' => 'rgb(0, 0, 0)'
+ 'color' => 'rgb(0, 0, 0)',
],
'description' => null,
- 'elementRef' => $elementRef
+ 'elementRef' => $elementRef,
];
- // Extract style information if present
- if (isset($node['style'])) {
- $this->applyNodeStyle($viewNode, $node['style']);
+ // Extract style information if present.
+ if (isset($node['style']) === true) {
+ $this->applyNodeStyle(viewNode: $viewNode, style: $node['style']);
}
- // Extract label text for Label type nodes
- if (isset($node['label'])) {
- if (is_array($node['label']) && isset($node['label']['_value'])) {
+ // Extract label text for Label type nodes.
+ if (isset($node['label']) === true) {
+ if (is_array($node['label']) === true && isset($node['label']['_value']) === true) {
$viewNode['name'] = $node['label']['_value'];
- } elseif (is_string($node['label'])) {
+ } else if (is_string($node['label']) === true) {
$viewNode['name'] = $node['label'];
}
}
- // Enrich with element data if available and elementRef exists
- if ($elementRef && isset($elementsLookup[$elementRef])) {
+ // Enrich with element data if available and elementRef exists.
+ if ($elementRef !== false && isset($elementsLookup[$elementRef]) === true) {
$element = $elementsLookup[$elementRef];
- // Use element name if node doesn't have its own label
- if ($viewNode['name'] === null && isset($element['name'])) {
+ // Use element name if node doesn't have its own label.
+ if ($viewNode['name'] === null && isset($element['name']) === true) {
$viewNode['name'] = $element['name'];
}
- // Set description from element documentation
- if (isset($element['summary'])) {
+ // Set description from element documentation.
+ if (isset($element['summary']) === true) {
$viewNode['description'] = $element['summary'];
- } elseif (isset($element['documentation'])) {
+ } else if (isset($element['documentation']) === true) {
$viewNode['description'] = $element['documentation'];
}
- // Enhance type with element data if node type wasn't fully extracted
+ // Enhance type with element data if node type wasn't fully extracted.
if ($viewNode['type'] === null || $viewNode['type'] === 'element') {
- if (isset($element['gemmaType'])) {
+ if (isset($element['gemmaType']) === true) {
$gemmaType = $element['gemmaType'];
- // Handle case where gemmaType might be an array
- if (is_array($gemmaType)) {
+ // Handle case where gemmaType might be an array.
+ if (is_array($gemmaType) === true) {
$gemmaType = $gemmaType['_value'] ?? $gemmaType[0] ?? 'unknown';
}
- $viewNode['type'] = strtolower((string)$gemmaType);
- } elseif (isset($element['xml']['_attributes']['xsi:type'])) {
+
+ $viewNode['type'] = strtolower((string) $gemmaType);
+ } else if (isset($element['xml']['_attributes']['xsi:type']) === true) {
$archiType = $element['xml']['_attributes']['xsi:type'];
- // Convert ArchiMate type to simplified type (e.g., "archimate:BusinessService" -> "businessservice")
+ // Convert ArchiMate type to simplified type (e.g., "archimate:BusinessService" -> "businessservice").
$viewNode['type'] = strtolower(preg_replace('/^archimate:|^[a-z]+:/', '', $archiType));
}
}
- // Add all element properties to view node for full data access
- $viewNode['elementProperties'] = $this->extractElementProperties($element);
+ // Add all element properties to view node for full data access.
+ $viewNode['elementProperties'] = $this->extractElementProperties(element: $element);
- // Add specific useful properties directly to the node
- if (isset($element['objectId'])) {
+ // Add specific useful properties directly to the node.
+ if (isset($element['objectId']) === true) {
$viewNode['objectId'] = $element['objectId'];
}
- // Add GEMMA-specific properties if they exist
+ // Add GEMMA-specific properties if they exist.
$gemmaProperties = ['gemmaType', 'bivScoreBbn', 'belangrijksteReden', 'beschikbaarheid', 'integriteit', 'vertrouwelijkheid'];
foreach ($gemmaProperties as $prop) {
- if (isset($element[$prop])) {
+ if (isset($element[$prop]) === true) {
$viewNode[$prop] = $element[$prop];
}
}
- // Store element section for reference
- if (isset($element['section'])) {
+ // Store element section for reference.
+ if (isset($element['section']) === true) {
$viewNode['elementSection'] = $element['section'];
}
- // Store model identifier for linking
- if (isset($element['model_identifier'])) {
+ // Store model identifier for linking.
+ if (isset($element['model_identifier']) === true) {
$viewNode['modelIdentifier'] = $element['model_identifier'];
}
- }
+ }//end if
- // Add parent node BEFORE its children so the frontend rendering
+ // Add parent node BEFORE its children so the frontend rendering.
// engine can look up parents via graph.getCell(parentId).
$viewNodes[] = $viewNode;
- // Handle child nodes recursively (flatten hierarchy into single array while preserving parent-child relationships)
- if (isset($node['node'])) {
- $childNodes = $this->extractViewNodesRecursively($node['node'], $elementsLookup);
+ // Handle child nodes recursively (flatten hierarchy into single array while preserving parent-child relationships).
+ if (isset($node['node']) === true) {
+ $childNodes = $this->extractViewNodesRecursively(nodeData: $node['node'], elementsLookup: $elementsLookup);
// Set parent reference only for DIRECT children (those with parent === null).
// Grandchildren already have their parent set by the recursive call.
@@ -2681,38 +2929,43 @@ private function extractViewNodesRecursively($nodeData, array $elementsLookup =
$childNode['parent'] = $nodeId;
}
}
+
unset($childNode);
- // Add child nodes to the main flattened array (maintaining parent references)
+ // Add child nodes to the main flattened array (maintaining parent references).
$viewNodes = array_merge($viewNodes, $childNodes);
}
- }
+ }//end foreach
return $viewNodes;
- }
+ }//end extractViewNodesRecursively()
/**
* Debug helper: Log parent-child relationships in view nodes
*
- * @param array $viewNodes Array of view nodes with parent references
- * @param string $viewId View identifier for logging context
+ * @param array $viewNodes Array of view nodes with parent references
+ * @param string $viewId View identifier for logging context
+ *
* @return void
*/
private function debugViewNodeHierarchy(array $viewNodes, string $viewId): void
{
- $rootNodes = array_filter($viewNodes, fn($node) => $node['parent'] === null);
+ $rootNodes = array_filter($viewNodes, fn($node) => $node['parent'] === null);
$childNodes = array_filter($viewNodes, fn($node) => $node['parent'] !== null);
- $this->logger->debug("View node hierarchy for view: {$viewId}", [
- 'total_nodes' => count($viewNodes),
- 'root_nodes' => count($rootNodes),
- 'child_nodes' => count($childNodes),
- 'parent_child_pairs' => array_map(
+ $this->logger->debug(
+ "View node hierarchy for view: {$viewId}",
+ [
+ 'total_nodes' => count($viewNodes),
+ 'root_nodes' => count($rootNodes),
+ 'child_nodes' => count($childNodes),
+ 'parent_child_pairs' => array_map(
fn($node) => ['child' => $node['viewNodeId'], 'parent' => $node['parent']],
$childNodes
- )
- ]);
- }
+ ),
+ ]
+ );
+ }//end debugViewNodeHierarchy()
/**
* Recursively extract nested nodes with full hierarchy and element splicing (LEGACY - for backward compatibility)
@@ -2722,85 +2975,111 @@ private function debugViewNodeHierarchy(array $viewNodes, string $viewId): void
* via elementRef, the actual element data (minus _xml) is spliced into the node's
* 'element' property.
*
- * @param array $nodeData Node data (can be single node or array of nodes)
- * @param array $elementsLookup Lookup array of elements by identifier for splicing
+ * @param array $nodeData Node data (can be single node or array of nodes)
+ * @param array $elementsLookup Lookup array of elements by identifier for splicing
+ *
* @return array Array of processed nodes with nested children and spliced elements
*/
- private function extractNodesRecursively($nodeData, array $elementsLookup = []): array
+ private function extractNodesRecursively($nodeData, array $elementsLookup=[]): array
{
$nodes = [];
- // Handle both single node and array of nodes
- if (!isset($nodeData[0])) {
- // Single node
+ // Handle both single node and array of nodes.
+ if (isset($nodeData[0]) === false) {
+ // Single node.
$nodeData = [$nodeData];
}
foreach ($nodeData as $node) {
- if (isset($node['_attributes'])) {
+ if (isset($node['_attributes']) === true) {
+ if (isset($node['_attributes']['x']) === true) {
+ $xValue = (int) $node['_attributes']['x'];
+ } else {
+ $xValue = null;
+ }
+
+ if (isset($node['_attributes']['y']) === true) {
+ $yValue = (int) $node['_attributes']['y'];
+ } else {
+ $yValue = null;
+ }
+
+ if (isset($node['_attributes']['w']) === true) {
+ $wValue = (int) $node['_attributes']['w'];
+ } else {
+ $wValue = null;
+ }
+
+ if (isset($node['_attributes']['h']) === true) {
+ $hValue = (int) $node['_attributes']['h'];
+ } else {
+ $hValue = null;
+ }
+
$processedNode = [
'identifier' => $node['_attributes']['identifier'] ?? null,
'elementRef' => $node['_attributes']['elementRef'] ?? null,
- 'type' => $node['_attributes']['xsi:type'] ?? 'Element',
- 'x' => isset($node['_attributes']['x']) ? (int)$node['_attributes']['x'] : null,
- 'y' => isset($node['_attributes']['y']) ? (int)$node['_attributes']['y'] : null,
- 'w' => isset($node['_attributes']['w']) ? (int)$node['_attributes']['w'] : null,
- 'h' => isset($node['_attributes']['h']) ? (int)$node['_attributes']['h'] : null
+ 'type' => $node['_attributes']['xsi:type'] ?? 'Element',
+ 'x' => $xValue,
+ 'y' => $yValue,
+ 'w' => $wValue,
+ 'h' => $hValue,
];
- // Extract style information if present
- if (isset($node['style'])) {
- $processedNode['style'] = $this->extractNodeStyle($node['style']);
+ // Extract style information if present.
+ if (isset($node['style']) === true) {
+ $processedNode['style'] = $this->extractNodeStyle(style: $node['style']);
}
- // Extract label text for Label type nodes
- if (isset($node['label'])) {
- if (is_array($node['label']) && isset($node['label']['_value'])) {
+ // Extract label text for Label type nodes.
+ if (isset($node['label']) === true) {
+ if (is_array($node['label']) === true && isset($node['label']['_value']) === true) {
$processedNode['label'] = $node['label']['_value'];
- } elseif (is_string($node['label'])) {
+ } else if (is_string($node['label']) === true) {
$processedNode['label'] = $node['label'];
}
}
- // ELEMENT SPLICING: If node references an element, splice it in
- if (!empty($processedNode['elementRef']) && !empty($elementsLookup)) {
+ // ELEMENT SPLICING: If node references an element, splice it in.
+ if (empty($processedNode['elementRef']) === false && empty($elementsLookup) === false) {
$elementRef = $processedNode['elementRef'];
- if (isset($elementsLookup[$elementRef])) {
- // Splice element data (minus _xml and other metadata) into the node
+ if (isset($elementsLookup[$elementRef]) === true) {
+ // Splice element data (minus _xml and other metadata) into the node.
$element = $elementsLookup[$elementRef];
- $processedNode['element'] = $this->prepareElementForSplicing($element);
+ $processedNode['element'] = $this->prepareElementForSplicing(element: $element);
}
}
- // RECURSIVE: Extract child nodes if they exist (with element splicing)
- if (isset($node['node'])) {
- $processedNode['children'] = $this->extractNodesRecursively($node['node'], $elementsLookup);
+ // RECURSIVE: Extract child nodes if they exist (with element splicing).
+ if (isset($node['node']) === true) {
+ $processedNode['children'] = $this->extractNodesRecursively(nodeData: $node['node'], elementsLookup: $elementsLookup);
}
- // RECURSIVE: Extract child connections if they exist
- if (isset($node['connection'])) {
- $processedNode['connections'] = $this->extractConnectionsRecursively($node['connection']);
+ // RECURSIVE: Extract child connections if they exist.
+ if (isset($node['connection']) === true) {
+ $processedNode['connections'] = $this->extractConnectionsRecursively(connectionData: $node['connection']);
}
$nodes[] = $processedNode;
- }
- }
+ }//end if
+ }//end foreach
return $nodes;
- }
+ }//end extractNodesRecursively()
/**
* Prepare element data for splicing by removing internal metadata
*
- * @param array $element The complete element object
+ * @param array $element The complete element object
+ *
* @return array Element data suitable for splicing (without _xml, @self, etc.)
*/
private function prepareElementForSplicing(array $element): array
{
- // Start with a copy of the element
+ // Start with a copy of the element.
$splicedElement = $element;
- // Remove internal metadata fields that shouldn't be in spliced data
+ // Remove internal metadata fields that shouldn't be in spliced data.
$fieldsToRemove = ['@self', 'xml', '_xml', 'section', 'model_identifier', 'extracted_at', '_propertyMapping'];
foreach ($fieldsToRemove as $field) {
@@ -2808,37 +3087,38 @@ private function prepareElementForSplicing(array $element): array
}
return $splicedElement;
- }
+ }//end prepareElementForSplicing()
/**
* Extract connections recursively
*
- * @param array $connectionData Connection data (can be single connection or array)
+ * @param array $connectionData Connection data (can be single connection or array)
+ *
* @return array Array of processed connections
*/
private function extractConnectionsRecursively($connectionData): array
{
$connections = [];
- // Handle both single connection and array of connections
- if (!isset($connectionData[0])) {
- // Single connection
+ // Handle both single connection and array of connections.
+ if (isset($connectionData[0]) === false) {
+ // Single connection.
$connectionData = [$connectionData];
}
foreach ($connectionData as $connection) {
- if (isset($connection['_attributes'])) {
+ if (isset($connection['_attributes']) === true) {
$processedConnection = [
- 'identifier' => $connection['_attributes']['identifier'] ?? null,
+ 'identifier' => $connection['_attributes']['identifier'] ?? null,
'relationshipRef' => $connection['_attributes']['relationshipRef'] ?? null,
- 'type' => $connection['_attributes']['xsi:type'] ?? 'Relationship',
- 'source' => $connection['_attributes']['source'] ?? null,
- 'target' => $connection['_attributes']['target'] ?? null
+ 'type' => $connection['_attributes']['xsi:type'] ?? 'Relationship',
+ 'source' => $connection['_attributes']['source'] ?? null,
+ 'target' => $connection['_attributes']['target'] ?? null,
];
- // Extract style information if present
- if (isset($connection['style'])) {
- $processedConnection['style'] = $this->extractConnectionStyle($connection['style']);
+ // Extract style information if present.
+ if (isset($connection['style']) === true) {
+ $processedConnection['style'] = $this->extractConnectionStyle(style: $connection['style']);
}
$connections[] = $processedConnection;
@@ -2846,115 +3126,174 @@ private function extractConnectionsRecursively($connectionData): array
}
return $connections;
- }
+ }//end extractConnectionsRecursively()
/**
* Extract all properties from an element for view node enrichment
*
- * @param array $element Element data containing properties
+ * @param array $element Element data containing properties
+ *
* @return array Clean array of element properties
*/
private function extractElementProperties(array $element): array
{
$properties = [];
- // Extract basic element properties
+ // Extract basic element properties.
$basicProperties = ['identifier', 'name', 'summary', 'section', 'model_identifier'];
foreach ($basicProperties as $prop) {
- if (isset($element[$prop])) {
+ if (isset($element[$prop]) === true) {
$properties[$prop] = $element[$prop];
}
}
- // Extract all flattened properties (camelCase converted properties)
+ // Extract all flattened properties (camelCase converted properties).
$excludedKeys = ['@self', 'xml', '_propertyMapping', '_slug', '_essential_data'];
foreach ($element as $key => $value) {
- if (!in_array($key, $excludedKeys) && !in_array($key, $basicProperties)) {
- // Only include non-object values or simple arrays
- if (is_scalar($value) || (is_array($value) && !$this->isComplexArray($value))) {
+ if (in_array($key, $excludedKeys) === false && in_array($key, $basicProperties) === false) {
+ // Only include non-object values or simple arrays.
+ if (is_scalar($value) === true || (is_array($value) === true && $this->isComplexArra === falsey(array: $value))) {
$properties[$key] = $value;
}
}
}
- // Add property mapping for reference if available
- if (isset($element['_propertyMapping'])) {
+ // Add property mapping for reference if available.
+ if (isset($element['_propertyMapping']) === true) {
$properties['_propertyMapping'] = $element['_propertyMapping'];
}
return $properties;
- }
+ }//end extractElementProperties()
/**
* Check if an array contains complex nested structures
*
- * @param array $array Array to check
+ * @param array $array Array to check
+ *
* @return bool True if array contains complex nested structures
*/
private function isComplexArray(array $array): bool
{
foreach ($array as $value) {
- if (is_array($value) || is_object($value)) {
+ if (is_array($value) === true || is_object($value) === true) {
return true;
}
}
+
return false;
- }
+ }//end isComplexArray()
/**
* Apply style information to a viewNode structure
*
- * @param array &$viewNode ViewNode structure to apply styles to (by reference)
- * @param array $style Style data from XML
+ * @param array &$viewNode ViewNode structure to apply styles to (by reference)
+ * @param array $style Style data from XML
+ *
* @return void
*/
private function applyNodeStyle(array &$viewNode, array $style): void
{
- // Extract fillColor
- if (isset($style['fillColor']['_attributes'])) {
+ // Extract fillColor.
+ if (isset($style['fillColor']['_attributes']) === true) {
$fillColor = $style['fillColor']['_attributes'];
- $r = isset($fillColor['r']) ? (int)$fillColor['r'] : 255;
- $g = isset($fillColor['g']) ? (int)$fillColor['g'] : 255;
- $b = isset($fillColor['b']) ? (int)$fillColor['b'] : 255;
+ if (isset($fillColor['r']) === true) {
+ $r = (int) $fillColor['r'];
+ } else {
+ $r = 255;
+ }
+
+ if (isset($fillColor['g']) === true) {
+ $g = (int) $fillColor['g'];
+ } else {
+ $g = 255;
+ }
+
+ if (isset($fillColor['b']) === true) {
+ $b = (int) $fillColor['b'];
+ } else {
+ $b = 255;
+ }
+
$viewNode['color'] = "rgb($r, $g, $b)";
- }
+ }//end if
- // Extract lineColor (including alpha for border visibility)
- if (isset($style['lineColor']['_attributes'])) {
+ // Extract lineColor (including alpha for border visibility).
+ if (isset($style['lineColor']['_attributes']) === true) {
$lineColor = $style['lineColor']['_attributes'];
- $r = isset($lineColor['r']) ? (int)$lineColor['r'] : 0;
- $g = isset($lineColor['g']) ? (int)$lineColor['g'] : 0;
- $b = isset($lineColor['b']) ? (int)$lineColor['b'] : 0;
- $a = isset($lineColor['a']) ? (int)$lineColor['a'] : 100;
+ if (isset($lineColor['r']) === true) {
+ $r = (int) $lineColor['r'];
+ } else {
+ $r = 0;
+ }
+
+ if (isset($lineColor['g']) === true) {
+ $g = (int) $lineColor['g'];
+ } else {
+ $g = 0;
+ }
+
+ if (isset($lineColor['b']) === true) {
+ $b = (int) $lineColor['b'];
+ } else {
+ $b = 0;
+ }
+
+ if (isset($lineColor['a']) === true) {
+ $a = (int) $lineColor['a'];
+ } else {
+ $a = 100;
+ }
+
if ($a < 100) {
- $viewNode['borderColor'] = "rgba($r, $g, $b, " . round($a / 100, 2) . ")";
+ $viewNode['borderColor'] = "rgba($r, $g, $b, ".round($a / 100, 2).")";
} else {
$viewNode['borderColor'] = "rgb($r, $g, $b)";
}
- }
+ }//end if
- // Extract font information
- if (isset($style['font'])) {
+ // Extract font information.
+ if (isset($style['font']) === true) {
$font = [];
- if (isset($style['font']['_attributes'])) {
+ if (isset($style['font']['_attributes']) === true) {
$font['name'] = $style['font']['_attributes']['name'] ?? 'Segoe UI, Arial';
- $font['size'] = isset($style['font']['_attributes']['size']) ? (int)$style['font']['_attributes']['size'] : 12;
+ if (isset($style['font']['_attributes']['size']) === true) {
+ $font['size'] = (int) $style['font']['_attributes']['size'];
+ } else {
+ $font['size'] = 12;
+ }
+
$font['style'] = 'normal';
}
- if (isset($style['font']['color']['_attributes'])) {
+ if (isset($style['font']['color']['_attributes']) === true) {
$fontColor = $style['font']['color']['_attributes'];
- $r = isset($fontColor['r']) ? (int)$fontColor['r'] : 0;
- $g = isset($fontColor['g']) ? (int)$fontColor['g'] : 0;
- $b = isset($fontColor['b']) ? (int)$fontColor['b'] : 0;
+ if (isset($fontColor['r']) === true) {
+ $r = (int) $fontColor['r'];
+ } else {
+ $r = 0;
+ }
+
+ if (isset($fontColor['g']) === true) {
+ $g = (int) $fontColor['g'];
+ } else {
+ $g = 0;
+ }
+
+ if (isset($fontColor['b']) === true) {
+ $b = (int) $fontColor['b'];
+ } else {
+ $b = 0;
+ }
+
$font['color'] = "rgb($r, $g, $b)";
- }
+ }//end if
- if (!empty($font)) {
+ if (empty($font) === false) {
$viewNode['font'] = array_merge($viewNode['font'], $font);
}
- }
- }
+ }//end if
+ }//end applyNodeStyle()
/**
* Extract view relationships with proper JSON structure for frontend consumption
@@ -2962,298 +3301,464 @@ private function applyNodeStyle(array &$viewNode, array $style): void
* This method transforms ArchiMate XML connection data into the standardized viewRelationships
* format expected by the frontend visualization components.
*
- * @param array $connectionData Connection data (can be single connection or array)
+ * @param array $connectionData Connection data (can be single connection or array)
+ *
* @return array Array of viewRelationships with standardized structure
*/
private function extractViewRelationshipsRecursively($connectionData): array
{
$viewRelationships = [];
- // Handle both single connection and array of connections
- if (!isset($connectionData[0])) {
- // Single connection
+ // Handle both single connection and array of connections.
+ if (isset($connectionData[0]) === false) {
+ // Single connection.
$connectionData = [$connectionData];
}
foreach ($connectionData as $connection) {
- if (!isset($connection['_attributes'])) {
+ if (isset($connection['_attributes']) === false) {
continue;
}
- $connectionId = $connection['_attributes']['identifier'] ?? null;
+ $connectionId = $connection['_attributes']['identifier'] ?? null;
$relationshipRef = $connection['_attributes']['relationshipRef'] ?? null;
- $source = $connection['_attributes']['source'] ?? null;
- $target = $connection['_attributes']['target'] ?? null;
+ $source = $connection['_attributes']['source'] ?? null;
+ $target = $connection['_attributes']['target'] ?? null;
- if (!$connectionId || !$source || !$target) {
+ if ($connectionId === null || $source === null || $target === false) {
continue;
}
- // Create viewRelationship with standardized structure
+ // Create viewRelationship with standardized structure.
$viewRelationship = [
- 'modelRelationshipId' => $relationshipRef, // Reference to the ArchiMate relationship
- 'sourceId' => $source, // Source node viewNodeId
- 'targetId' => $target, // Target node viewNodeId
- 'viewRelationshipId' => $connectionId, // Unique identifier within this view
- 'type' => $this->extractConnectionType($connection), // Extract type directly from connection XML
- 'bendpoints' => [], // Array of bend points
- 'label' => [] // Label information
+ 'modelRelationshipId' => $relationshipRef,
+ // Reference to the ArchiMate relationship.
+ 'sourceId' => $source,
+ // Source node viewNodeId.
+ 'targetId' => $target,
+ // Target node viewNodeId.
+ 'viewRelationshipId' => $connectionId,
+ // Unique identifier within this view.
+ 'type' => $this->extractConnectionType(connection: $connection),
+ // Extract type directly from connection XML.
+ 'bendpoints' => [],
+ // Array of bend points.
+ 'label' => [],
+ // Label information.
];
- // Extract bend points if present
- if (isset($connection['bendpoint'])) {
- $bendpoints = isset($connection['bendpoint'][0]) ? $connection['bendpoint'] : [$connection['bendpoint']];
+ // Extract bend points if present.
+ if (isset($connection['bendpoint']) === true) {
+ if (isset($connection['bendpoint'][0]) === true) {
+ $bendpoints = $connection['bendpoint'];
+ } else {
+ $bendpoints = [$connection['bendpoint']];
+ }
foreach ($bendpoints as $bendpoint) {
- if (isset($bendpoint['_attributes'])) {
+ if (isset($bendpoint['_attributes']) === true) {
+ if (isset($bendpoint['_attributes']['x']) === true) {
+ $xValue = (float) $bendpoint['_attributes']['x'];
+ } else {
+ $xValue = 0;
+ }
+
+ if (isset($bendpoint['_attributes']['y']) === true) {
+ $yValue = (float) $bendpoint['_attributes']['y'];
+ } else {
+ $yValue = 0;
+ }
+
$viewRelationship['bendpoints'][] = [
- 'x' => isset($bendpoint['_attributes']['x']) ? (float)$bendpoint['_attributes']['x'] : 0,
- 'y' => isset($bendpoint['_attributes']['y']) ? (float)$bendpoint['_attributes']['y'] : 0
+ 'x' => $xValue,
+ 'y' => $yValue,
];
}
}
- }
+ }//end if
- // Extract label information if present
- if (isset($connection['label'])) {
+ // Extract label information if present.
+ if (isset($connection['label']) === true) {
$label = [];
- if (is_array($connection['label']) && isset($connection['label']['_value'])) {
+ if (is_array($connection['label']) === true && isset($connection['label']['_value']) === true) {
$label['text'] = $connection['label']['_value'];
- } elseif (is_string($connection['label'])) {
+ } else if (is_string($connection['label']) === true) {
$label['text'] = $connection['label'];
}
- // Extract label markup/style if present
- if (isset($connection['style'])) {
- $label['markup'] = [$this->extractLabelMarkup($connection['style'])];
+ // Extract label markup/style if present.
+ if (isset($connection['style']) === true) {
+ $label['markup'] = [$this->extractLabelMarkup(style: $connection['style'])];
}
$viewRelationship['label'] = $label;
}
$viewRelationships[] = $viewRelationship;
- }
+ }//end foreach
return $viewRelationships;
- }
+ }//end extractViewRelationshipsRecursively()
/**
* Extract label markup information from connection style
*
- * @param array $style Style data from XML
+ * @param array $style Style data from XML
+ *
* @return array Label markup structure
*/
private function extractLabelMarkup(array $style): array
{
$markup = [
'style' => [
- 'fontSize' => 11,
+ 'fontSize' => 11,
'fontFamily' => 'Segoe UI, Arial',
- 'fontColor' => 'rgba(0, 0, 0, 1)',
- 'fontStyle' => 'normal',
- 'fontWeight' => 'normal'
- ]
+ 'fontColor' => 'rgba(0, 0, 0, 1)',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => 'normal',
+ ],
];
- // Extract font information if present
- if (isset($style['font'])) {
- if (isset($style['font']['_attributes'])) {
+ // Extract font information if present.
+ if (isset($style['font']) === true) {
+ if (isset($style['font']['_attributes']) === true) {
$markup['style']['fontFamily'] = $style['font']['_attributes']['name'] ?? 'Segoe UI, Arial';
- $markup['style']['fontSize'] = isset($style['font']['_attributes']['size']) ? (int)$style['font']['_attributes']['size'] : 11;
+ if (isset($style['font']['_attributes']['size']) === true) {
+ $markup['style']['fontSize'] = (int) $style['font']['_attributes']['size'];
+ } else {
+ $markup['style']['fontSize'] = 11;
+ }
}
- if (isset($style['font']['color']['_attributes'])) {
+ if (isset($style['font']['color']['_attributes']) === true) {
$fontColor = $style['font']['color']['_attributes'];
- $r = isset($fontColor['r']) ? (int)$fontColor['r'] : 0;
- $g = isset($fontColor['g']) ? (int)$fontColor['g'] : 0;
- $b = isset($fontColor['b']) ? (int)$fontColor['b'] : 0;
- $a = isset($fontColor['a']) ? ((int)$fontColor['a'] / 100) : 1; // Convert percentage to decimal
+ if (isset($fontColor['r']) === true) {
+ $r = (int) $fontColor['r'];
+ } else {
+ $r = 0;
+ }
+
+ if (isset($fontColor['g']) === true) {
+ $g = (int) $fontColor['g'];
+ } else {
+ $g = 0;
+ }
+
+ if (isset($fontColor['b']) === true) {
+ $b = (int) $fontColor['b'];
+ } else {
+ $b = 0;
+ }
+
+ if (isset($fontColor['a']) === true) {
+ $a = ((int) $fontColor['a'] / 100);
+ } else {
+ $a = 1;
+ }
+
+ // Convert percentage to decimal.
$markup['style']['fontColor'] = "rgba($r, $g, $b, $a)";
- }
- }
+ }//end if
+ }//end if
return $markup;
- }
+ }//end extractLabelMarkup()
/**
* Extract node type directly from node XML attributes
*
- * @param array $node Node data from XML
+ * @param array $node Node data from XML
+ *
* @return string|null Node type extracted from XML or null if not found
*/
private function extractNodeType(array $node): ?string
{
- // Priority 1: Check xsi:type attribute (most specific)
- if (isset($node['_attributes']['xsi:type'])) {
+ // Priority 1: Check xsi:type attribute (most specific).
+ if (isset($node['_attributes']['xsi:type']) === true) {
$xsiType = $node['_attributes']['xsi:type'];
- // Handle different xsi:type formats
+ // Handle different xsi:type formats.
if ($xsiType === 'Label') {
return 'label';
- } elseif ($xsiType === 'Element') {
+ } else if ($xsiType === 'Element') {
return 'element';
- } elseif (str_contains($xsiType, ':')) {
- // Handle namespaced types like "archimate:BusinessService"
+ } else if (str_contains($xsiType, ':') === true) {
+ // Handle namespaced types like "archimate:BusinessService".
return strtolower(preg_replace('/^[a-z]+:/', '', $xsiType));
} else {
return strtolower($xsiType);
}
}
- // Priority 2: Check if this is a Label node (has label content)
- if (isset($node['label'])) {
+ // Priority 2: Check if this is a Label node (has label content).
+ if (isset($node['label']) === true) {
return 'label';
}
- // Priority 3: Check if this has an elementRef (it's an Element node)
- if (isset($node['_attributes']['elementRef'])) {
+ // Priority 3: Check if this has an elementRef (it's an Element node).
+ if (isset($node['_attributes']['elementRef']) === true) {
return 'element';
}
- // Fallback: Return null to allow element lookup to fill in the type
+ // Fallback: Return null to allow element lookup to fill in the type.
return null;
- }
+ }//end extractNodeType()
/**
* Extract connection type directly from connection XML attributes
*
- * @param array $connection Connection data from XML
+ * @param array $connection Connection data from XML
+ *
* @return string Connection type extracted from XML or default 'association'
*/
private function extractConnectionType(array $connection): string
{
- // Priority 1: Check xsi:type attribute (most specific)
- if (isset($connection['_attributes']['xsi:type'])) {
+ // Priority 1: Check xsi:type attribute (most specific).
+ if (isset($connection['_attributes']['xsi:type']) === true) {
$xsiType = $connection['_attributes']['xsi:type'];
- // Handle different connection type formats
- if (str_contains($xsiType, 'Relationship')) {
- // Remove "Relationship" suffix and namespace prefix
- // e.g., "archimate:ServingRelationship" -> "serving"
- $type = preg_replace('/^[a-z]+:/', '', $xsiType); // Remove namespace
- $type = preg_replace('/relationship$/i', '', $type); // Remove "Relationship" suffix
+ // Handle different connection type formats.
+ if (str_contains($xsiType, 'Relationship') === true) {
+ // Remove "Relationship" suffix and namespace prefix.
+ // e.g., "archimate:ServingRelationship" -> "serving".
+ $type = preg_replace('/^[a-z]+:/', '', $xsiType);
+ // Remove namespace.
+ $type = preg_replace('/relationship$/i', '', $type);
+ // Remove "Relationship" suffix.
return strtolower($type);
- } elseif (str_contains($xsiType, ':')) {
- // Handle other namespaced types
+ } else if (str_contains($xsiType, ':') === true) {
+ // Handle other namespaced types.
return strtolower(preg_replace('/^[a-z]+:/', '', $xsiType));
} else {
return strtolower($xsiType);
}
}
- // Priority 2: Check if this has a relationshipRef (use that to determine type if possible)
- if (isset($connection['_attributes']['relationshipRef'])) {
- // We have a relationship reference, but we can't determine the type from just the ID
- // Return generic 'relationship' type
+ // Priority 2: Check if this has a relationshipRef (use that to determine type if possible).
+ if (isset($connection['_attributes']['relationshipRef']) === true) {
+ // We have a relationship reference, but we can't determine the type from just the ID.
+ // Return generic 'relationship' type.
return 'relationship';
}
- // Fallback: Default association type
+ // Fallback: Default association type.
return 'association';
- }
+ }//end extractConnectionType()
/**
* Extract style information from a node (LEGACY - for backward compatibility)
*
- * @param array $style Style data from XML
+ * @param array $style Style data from XML
+ *
* @return array Processed style information
*/
private function extractNodeStyle(array $style): array
{
$processedStyle = [];
- // Extract fillColor
- if (isset($style['fillColor']['_attributes'])) {
+ // Extract fillColor.
+ if (isset($style['fillColor']['_attributes']) === true) {
$fillColor = $style['fillColor']['_attributes'];
+ if (isset($fillColor['r']) === true) {
+ $rValue = (int) $fillColor['r'];
+ } else {
+ $rValue = 255;
+ }
+
+ if (isset($fillColor['g']) === true) {
+ $gValue = (int) $fillColor['g'];
+ } else {
+ $gValue = 255;
+ }
+
+ if (isset($fillColor['b']) === true) {
+ $bValue = (int) $fillColor['b'];
+ } else {
+ $bValue = 255;
+ }
+
+ if (isset($fillColor['a']) === true) {
+ $aValue = (int) $fillColor['a'];
+ } else {
+ $aValue = 100;
+ }
+
$processedStyle['fillColor'] = [
- 'r' => isset($fillColor['r']) ? (int)$fillColor['r'] : 255,
- 'g' => isset($fillColor['g']) ? (int)$fillColor['g'] : 255,
- 'b' => isset($fillColor['b']) ? (int)$fillColor['b'] : 255,
- 'a' => isset($fillColor['a']) ? (int)$fillColor['a'] : 100
+ 'r' => $rValue,
+ 'g' => $gValue,
+ 'b' => $bValue,
+ 'a' => $aValue,
];
- }
+ }//end if
- // Extract lineColor
- if (isset($style['lineColor']['_attributes'])) {
+ // Extract lineColor.
+ if (isset($style['lineColor']['_attributes']) === true) {
$lineColor = $style['lineColor']['_attributes'];
+ if (isset($lineColor['r']) === true) {
+ $rValue = (int) $lineColor['r'];
+ } else {
+ $rValue = 0;
+ }
+
+ if (isset($lineColor['g']) === true) {
+ $gValue = (int) $lineColor['g'];
+ } else {
+ $gValue = 0;
+ }
+
+ if (isset($lineColor['b']) === true) {
+ $bValue = (int) $lineColor['b'];
+ } else {
+ $bValue = 0;
+ }
+
+ if (isset($lineColor['a']) === true) {
+ $aValue = (int) $lineColor['a'];
+ } else {
+ $aValue = 100;
+ }
+
$processedStyle['lineColor'] = [
- 'r' => isset($lineColor['r']) ? (int)$lineColor['r'] : 0,
- 'g' => isset($lineColor['g']) ? (int)$lineColor['g'] : 0,
- 'b' => isset($lineColor['b']) ? (int)$lineColor['b'] : 0,
- 'a' => isset($lineColor['a']) ? (int)$lineColor['a'] : 100
+ 'r' => $rValue,
+ 'g' => $gValue,
+ 'b' => $bValue,
+ 'a' => $aValue,
];
- }
+ }//end if
- // Extract font information
- if (isset($style['font'])) {
+ // Extract font information.
+ if (isset($style['font']) === true) {
$font = [];
- if (isset($style['font']['_attributes'])) {
+ if (isset($style['font']['_attributes']) === true) {
$font['name'] = $style['font']['_attributes']['name'] ?? 'Arial';
- $font['size'] = isset($style['font']['_attributes']['size']) ? (int)$style['font']['_attributes']['size'] : 12;
+ if (isset($style['font']['_attributes']['size']) === true) {
+ $font['size'] = (int) $style['font']['_attributes']['size'];
+ } else {
+ $font['size'] = 12;
+ }
}
- if (isset($style['font']['color']['_attributes'])) {
+ if (isset($style['font']['color']['_attributes']) === true) {
$fontColor = $style['font']['color']['_attributes'];
+ if (isset($fontColor['r']) === true) {
+ $rValue = (int) $fontColor['r'];
+ } else {
+ $rValue = 0;
+ }
+
+ if (isset($fontColor['g']) === true) {
+ $gValue = (int) $fontColor['g'];
+ } else {
+ $gValue = 0;
+ }
+
+ if (isset($fontColor['b']) === true) {
+ $bValue = (int) $fontColor['b'];
+ } else {
+ $bValue = 0;
+ }
+
$font['color'] = [
- 'r' => isset($fontColor['r']) ? (int)$fontColor['r'] : 0,
- 'g' => isset($fontColor['g']) ? (int)$fontColor['g'] : 0,
- 'b' => isset($fontColor['b']) ? (int)$fontColor['b'] : 0
+ 'r' => $rValue,
+ 'g' => $gValue,
+ 'b' => $bValue,
];
- }
+ }//end if
- if (!empty($font)) {
+ if (empty($font) === false) {
$processedStyle['font'] = $font;
}
- }
+ }//end if
return $processedStyle;
- }
+ }//end extractNodeStyle()
/**
* Extract style information from a connection
*
- * @param array $style Style data from XML
+ * @param array $style Style data from XML
+ *
* @return array Processed style information
*/
private function extractConnectionStyle(array $style): array
{
$processedStyle = [];
- // Extract lineColor
- if (isset($style['lineColor']['_attributes'])) {
+ // Extract lineColor.
+ if (isset($style['lineColor']['_attributes']) === true) {
$lineColor = $style['lineColor']['_attributes'];
+ if (isset($lineColor['r']) === true) {
+ $rValue = (int) $lineColor['r'];
+ } else {
+ $rValue = 0;
+ }
+
+ if (isset($lineColor['g']) === true) {
+ $gValue = (int) $lineColor['g'];
+ } else {
+ $gValue = 0;
+ }
+
+ if (isset($lineColor['b']) === true) {
+ $bValue = (int) $lineColor['b'];
+ } else {
+ $bValue = 0;
+ }
+
$processedStyle['lineColor'] = [
- 'r' => isset($lineColor['r']) ? (int)$lineColor['r'] : 0,
- 'g' => isset($lineColor['g']) ? (int)$lineColor['g'] : 0,
- 'b' => isset($lineColor['b']) ? (int)$lineColor['b'] : 0
+ 'r' => $rValue,
+ 'g' => $gValue,
+ 'b' => $bValue,
];
- }
+ }//end if
- // Extract font information
- if (isset($style['font'])) {
+ // Extract font information.
+ if (isset($style['font']) === true) {
$font = [];
- if (isset($style['font']['_attributes'])) {
+ if (isset($style['font']['_attributes']) === true) {
$font['name'] = $style['font']['_attributes']['name'] ?? 'Arial';
- $font['size'] = isset($style['font']['_attributes']['size']) ? (int)$style['font']['_attributes']['size'] : 12;
+ if (isset($style['font']['_attributes']['size']) === true) {
+ $font['size'] = (int) $style['font']['_attributes']['size'];
+ } else {
+ $font['size'] = 12;
+ }
}
- if (isset($style['font']['color']['_attributes'])) {
+ if (isset($style['font']['color']['_attributes']) === true) {
$fontColor = $style['font']['color']['_attributes'];
+ if (isset($fontColor['r']) === true) {
+ $rValue = (int) $fontColor['r'];
+ } else {
+ $rValue = 0;
+ }
+
+ if (isset($fontColor['g']) === true) {
+ $gValue = (int) $fontColor['g'];
+ } else {
+ $gValue = 0;
+ }
+
+ if (isset($fontColor['b']) === true) {
+ $bValue = (int) $fontColor['b'];
+ } else {
+ $bValue = 0;
+ }
+
$font['color'] = [
- 'r' => isset($fontColor['r']) ? (int)$fontColor['r'] : 0,
- 'g' => isset($fontColor['g']) ? (int)$fontColor['g'] : 0,
- 'b' => isset($fontColor['b']) ? (int)$fontColor['b'] : 0
+ 'r' => $rValue,
+ 'g' => $gValue,
+ 'b' => $bValue,
];
- }
+ }//end if
- if (!empty($font)) {
+ if (empty($font) === false) {
$processedStyle['font'] = $font;
}
- }
+ }//end if
return $processedStyle;
- }
+ }//end extractConnectionStyle()
/**
* Extract GEMMA type from an object using multiple possible property names
@@ -3261,60 +3766,76 @@ private function extractConnectionStyle(array $style): array
* This method tries different variations of GEMMA type property names to ensure
* compatibility with different ArchiMate model variations.
*
- * @param array $object The object to extract GEMMA type from
+ * @param array $object The object to extract GEMMA type from
+ *
* @return string|null The GEMMA type value or null if not found
*/
private function extractGemmaType(array $object): ?string
{
- // Try various possible property names for GEMMA type
+ // Try various possible property names for GEMMA type.
$possiblePropertyNames = [
- 'gemmaType', // Standard camelCase conversion of "GEMMA Type"
- 'gemmatype', // Lowercase version
- 'GemmaType', // PascalCase version
- 'GEMMA_Type', // Underscore version
- 'gemma_type', // Lowercase underscore version
- 'GEMMAType', // All caps first word
- 'type', // Sometimes just "Type" in models
- 'elementType', // Alternative naming
- 'componentType' // Another alternative
+ 'gemmaType',
+ // Standard camelCase conversion of "GEMMA Type".
+ 'gemmatype',
+ // Lowercase version.
+ 'GemmaType',
+ // PascalCase version.
+ 'GEMMA_Type',
+ // Underscore version.
+ 'gemma_type',
+ // Lowercase underscore version.
+ 'GEMMAType',
+ // All caps first word.
+ 'type',
+ // Sometimes just "Type" in models.
+ 'elementType',
+ // Alternative naming.
+ 'componentType',
+ // Another alternative.
];
foreach ($possiblePropertyNames as $propertyName) {
- if (isset($object[$propertyName]) && !empty($object[$propertyName])) {
+ if (isset($object[$propertyName]) === true && empty($object[$propertyName]) === false) {
$rawValue = $object[$propertyName];
- // Handle case where value might be an array (e.g., from XML parsing with _value key)
- if (is_array($rawValue)) {
+ // Handle case where value might be an array (e.g., from XML parsing with _value key).
+ if (is_array($rawValue) === true) {
$value = $rawValue['_value'] ?? $rawValue[0] ?? '';
} else {
$value = (string) $rawValue;
}
- // Log the first successful match for debugging
- if (!isset($this->gemmaTypePropertyFound)) {
- $this->logger->debug('GEMMA Type property found', [
- 'property_name' => $propertyName,
- 'value' => $value,
- 'object_id' => $object['identifier'] ?? 'unknown'
- ]);
+ // Log the first successful match for debugging.
+ if (isset($this->gemmaTypePropertyFound) === false) {
+ $this->logger->debug(
+ 'GEMMA Type property found',
+ [
+ 'property_name' => $propertyName,
+ 'value' => $value,
+ 'object_id' => $object['identifier'] ?? 'unknown',
+ ]
+ );
$this->gemmaTypePropertyFound = true;
}
return $value;
- }
- }
+ }//end if
+ }//end foreach
- // If no direct property found, check _propertyMapping for original property names
- if (isset($object['_propertyMapping'])) {
+ // If no direct property found, check _propertyMapping for original property names.
+ if (isset($object['_propertyMapping']) === true) {
foreach ($object['_propertyMapping'] as $camelCase => $original) {
- // Check if the original property name contains "gemma" or "type"
+ // Check if the original property name contains "gemma" or "type".
if (stripos($original, 'gemma') !== false && stripos($original, 'type') !== false) {
- if (isset($object[$camelCase]) && !empty($object[$camelCase])) {
- $this->logger->debug('GEMMA Type found via property mapping', [
- 'camel_case_name' => $camelCase,
- 'original_name' => $original,
- 'value' => $object[$camelCase],
- 'object_id' => $object['identifier'] ?? 'unknown'
- ]);
+ if (isset($object[$camelCase]) === true && empty($object[$camelCase]) === false) {
+ $this->logger->debug(
+ 'GEMMA Type found via property mapping',
+ [
+ 'camel_case_name' => $camelCase,
+ 'original_name' => $original,
+ 'value' => $object[$camelCase],
+ 'object_id' => $object['identifier'] ?? 'unknown',
+ ]
+ );
return (string) $object[$camelCase];
}
}
@@ -3322,7 +3843,7 @@ private function extractGemmaType(array $object): ?string
}
return null;
- }
+ }//end extractGemmaType()
/**
* Process GEMMA Referentiecomponent-Standaard relationships with Verbindingsrol support
@@ -3333,161 +3854,175 @@ private function extractGemmaType(array $object): ?string
* - 'aanbevolenStandaarden' array for standards with Verbindingsrol = "Aanbevolen"
* - 'verplichteStandaarden' array for standards with Verbindingsrol = "Verplicht"
*
- * @param array $objects All objects from the import
+ * @param array $objects All objects from the import
+ *
* @return array Objects with enhanced Referentiecomponent data
*/
private function processGemmaReferenceComponentStandards(array $objects): array
{
$this->logger->info('Processing GEMMA Referentiecomponent-Standaard and StandaardVersie relationships with optimized single-pass algorithm');
- // OPTIMIZATION: Single-pass processing - collect all data types at once
+ // OPTIMIZATION: Single-pass processing - collect all data types at once.
$referentieComponenten = [];
- $standaarden = [];
- $standaardVersies = [];
- $gemmaRelationshipMap = [];
- $standaardVersieRelationshipMap = []; // StandaardVersie -> Standaard mappings
-
- // Debug: Count objects and property variations
- $elementCount = 0;
+ $standaarden = [];
+ $standaardVersies = [];
+ $gemmaRelationshipMap = [];
+ $standaardVersieRelationshipMap = [];
+ // StandaardVersie -> Standaard mappings.
+ // Debug: Count objects and property variations.
+ $elementCount = 0;
$elementsWithGemmaType = 0;
- $gemmaTypeVariations = [];
+ $gemmaTypeVariations = [];
- // PASS 1: Collect Referentiecomponenten, Standaarden, and StandaardVersies
+ // PASS 1: Collect Referentiecomponenten, Standaarden, and StandaardVersies.
foreach ($objects as $index => $object) {
- // Debug: Count elements and GEMMA types
- if (isset($object['section']) && $object['section'] === 'element') {
+ // Debug: Count elements and GEMMA types.
+ if (isset($object['section']) === true && $object['section'] === 'element') {
$elementCount++;
- // Check for various possible GEMMA type property names
- $gemmaTypeValue = $this->extractGemmaType($object);
+ // Check for various possible GEMMA type property names.
+ $gemmaTypeValue = $this->extractGemmaType(object: $object);
if ($gemmaTypeValue !== null) {
$elementsWithGemmaType++;
- // Track GEMMA type variations for debugging
- if (!isset($gemmaTypeVariations[$gemmaTypeValue])) {
+ // Track GEMMA type variations for debugging.
+ if (isset($gemmaTypeVariations[$gemmaTypeValue]) === false) {
$gemmaTypeVariations[$gemmaTypeValue] = 0;
}
+
$gemmaTypeVariations[$gemmaTypeValue]++;
if ($gemmaTypeValue === 'Referentiecomponent') {
$referentieComponenten[$object['identifier']] = $index;
- } elseif ($gemmaTypeValue === 'Standaard') {
+ } else if ($gemmaTypeValue === 'Standaard') {
$standaarden[$object['identifier']] = $index;
- } elseif ($gemmaTypeValue === 'Standaardversie') {
+ } else if ($gemmaTypeValue === 'Standaardversie') {
$standaardVersies[$object['identifier']] = $index;
}
}
- }
- }
+ }//end if
+ }//end foreach
- // PASS 2: Process relationships (need all entities collected first for StandaardVersie relationships)
+ // PASS 2: Process relationships (need all entities collected first for StandaardVersie relationships).
foreach ($objects as $object) {
- if (isset($object['section']) && $object['section'] === 'relationship') {
- // Process Referentiecomponent-Standaard relationships
- $this->processRelationshipImmediate($object, $referentieComponenten, $standaarden, $gemmaRelationshipMap);
-
- // Process StandaardVersie-Standaard relationships (Specialization type)
- $this->processStandaardVersieRelationship($object, $standaardVersies, $standaarden, $standaardVersieRelationshipMap);
- }
- }
-
- // Enhanced debug logging
- $this->logger->info('GEMMA objects processing complete', [
- 'total_elements' => $elementCount,
- 'elements_with_gemma_type' => $elementsWithGemmaType,
- 'gemma_type_variations' => $gemmaTypeVariations,
- 'referentiecomponenten_count' => count($referentieComponenten),
- 'standaarden_count' => count($standaarden),
- 'standaardversies_count' => count($standaardVersies),
- 'processed_relationships' => count($gemmaRelationshipMap),
- 'standaardversie_relationships' => count($standaardVersieRelationshipMap)
- ]);
+ if (isset($object['section']) === true && $object['section'] === 'relationship') {
+ // Process Referentiecomponent-Standaard relationships.
+ $this->processRelationshipImmediate(relationship: $object, referentieComponenten: $referentieComponenten, standaarden: $standaarden, gemmaRelationshipMap: $gemmaRelationshipMap);
+
+ // Process StandaardVersie-Standaard relationships (Specialization type).
+ $this->processStandaardVersieRelationship(relationship: $object, standaardVersies: $standaardVersies, standaarden: $standaarden, standaardVersieRelationshipMap: $standaardVersieRelationshipMap);
+ }
+ }
+
+ // Enhanced debug logging.
+ $this->logger->info(
+ 'GEMMA objects processing complete',
+ [
+ 'total_elements' => $elementCount,
+ 'elements_with_gemma_type' => $elementsWithGemmaType,
+ 'gemma_type_variations' => $gemmaTypeVariations,
+ 'referentiecomponenten_count' => count($referentieComponenten),
+ 'standaarden_count' => count($standaarden),
+ 'standaardversies_count' => count($standaardVersies),
+ 'processed_relationships' => count($gemmaRelationshipMap),
+ 'standaardversie_relationships' => count($standaardVersieRelationshipMap),
+ ]
+ );
- // STEP 2: Apply the processed relationship mappings to Referentiecomponenten
+ // STEP 2: Apply the processed relationship mappings to Referentiecomponenten.
$enhancedCount = 0;
foreach ($gemmaRelationshipMap as $referentieComponentId => $standaardenMap) {
- if (isset($referentieComponenten[$referentieComponentId])) {
+ if (isset($referentieComponenten[$referentieComponentId]) === true) {
$objectIndex = $referentieComponenten[$referentieComponentId];
- // Remove duplicates and add the properties
+ // Remove duplicates and add the properties.
$aanbevolenStandaarden = array_unique($standaardenMap['aanbevolen']);
$verplichteStandaarden = array_unique($standaardenMap['verplicht']);
$objects[$objectIndex]['aanbevolenStandaarden'] = $aanbevolenStandaarden;
$objects[$objectIndex]['verplichteStandaarden'] = $verplichteStandaarden;
- // Also add combined array for backward compatibility
+ // Also add combined array for backward compatibility.
$allStandaarden = array_unique(array_merge($aanbevolenStandaarden, $verplichteStandaarden));
$objects[$objectIndex]['standaarden'] = $allStandaarden;
- $this->logger->info('Enhanced Referentiecomponent with categorized standaarden', [
- 'referentiecomponent_id' => $referentieComponentId,
- 'referentiecomponent_name' => $objects[$objectIndex]['name'] ?? 'Unknown',
- 'aanbevolen_count' => count($aanbevolenStandaarden),
- 'verplicht_count' => count($verplichteStandaarden),
- 'aanbevolen_ids' => $aanbevolenStandaarden,
- 'verplicht_ids' => $verplichteStandaarden
- ]);
+ $this->logger->info(
+ 'Enhanced Referentiecomponent with categorized standaarden',
+ [
+ 'referentiecomponent_id' => $referentieComponentId,
+ 'referentiecomponent_name' => $objects[$objectIndex]['name'] ?? 'Unknown',
+ 'aanbevolen_count' => count($aanbevolenStandaarden),
+ 'verplicht_count' => count($verplichteStandaarden),
+ 'aanbevolen_ids' => $aanbevolenStandaarden,
+ 'verplicht_ids' => $verplichteStandaarden,
+ ]
+ );
$enhancedCount++;
- }
- }
-
- $this->logger->info('GEMMA Referentiecomponent-Standaard processing completed', [
- 'referentiecomponenten_enhanced' => $enhancedCount,
- 'total_referentiecomponenten' => count($referentieComponenten),
- 'total_relationships_processed' => count($gemmaRelationshipMap)
- ]);
+ }//end if
+ }//end foreach
+
+ $this->logger->info(
+ 'GEMMA Referentiecomponent-Standaard processing completed',
+ [
+ 'referentiecomponenten_enhanced' => $enhancedCount,
+ 'total_referentiecomponenten' => count($referentieComponenten),
+ 'total_relationships_processed' => count($gemmaRelationshipMap),
+ ]
+ );
- // STEP 3: Apply StandaardVersie-Standaard relationship mappings
- // Only store 'standaard' on StandaardVersie - use inversedBy for reverse lookup
+ // STEP 3: Apply StandaardVersie-Standaard relationship mappings.
+ // Only store 'standaard' on StandaardVersie - use inversedBy for reverse lookup.
$versieEnhancedCount = 0;
foreach ($standaardVersieRelationshipMap as $versieId => $standaardId) {
- // Add standaard reference to StandaardVersie
- if (isset($standaardVersies[$versieId])) {
+ // Add standaard reference to StandaardVersie.
+ if (isset($standaardVersies[$versieId]) === true) {
$versieIndex = $standaardVersies[$versieId];
- // Convert to UUID format (remove "id-" prefix)
+ // Convert to UUID format (remove "id-" prefix).
$standaardUuid = str_replace('id-', '', $standaardId);
$objects[$versieIndex]['standaard'] = $standaardUuid;
$versieEnhancedCount++;
}
}
- $this->logger->info('GEMMA StandaardVersie-Standaard processing completed', [
- 'standaardversies_enhanced' => $versieEnhancedCount,
- 'total_standaardversies' => count($standaardVersies),
- 'total_versie_relationships' => count($standaardVersieRelationshipMap)
- ]);
-
- // STEP 4: Add standaardVersies to ReferentieComponenten
- // This allows querying ?gemmaType=referentiecomponent&_extend[]=gekoppeldeStandaardVersies
- // to get all referentiecomponenten with their related standaardVersies in one call
+ $this->logger->info(
+ 'GEMMA StandaardVersie-Standaard processing completed',
+ [
+ 'standaardversies_enhanced' => $versieEnhancedCount,
+ 'total_standaardversies' => count($standaardVersies),
+ 'total_versie_relationships' => count($standaardVersieRelationshipMap),
+ ]
+ );
- // Build reverse map: Standaard ID -> [StandaardVersie UUIDs]
+ // STEP 4: Add standaardVersies to ReferentieComponenten.
+ // This allows querying ?gemmaType=referentiecomponent&_extend[]=gekoppeldeStandaardVersies.
+ // to get all referentiecomponenten with their related standaardVersies in one call.
+ // Build reverse map: Standaard ID -> [StandaardVersie UUIDs].
$standaardToVersiesMap = [];
foreach ($standaardVersieRelationshipMap as $versieId => $standaardId) {
$versieUuid = str_replace('id-', '', $versieId);
- if (!isset($standaardToVersiesMap[$standaardId])) {
+ if (isset($standaardToVersiesMap[$standaardId]) === false) {
$standaardToVersiesMap[$standaardId] = [];
}
+
$standaardToVersiesMap[$standaardId][] = $versieUuid;
}
- // Add standaardVersies to each ReferentieComponent
+ // Add standaardVersies to each ReferentieComponent.
$refCompWithVersiesCount = 0;
foreach ($referentieComponenten as $refCompId => $objectIndex) {
$standaardVersiesForRefComp = [];
- // Get all standaarden for this referentiecomponent (combined array)
+ // Get all standaarden for this referentiecomponent (combined array).
$refCompStandaarden = $objects[$objectIndex]['standaarden'] ?? [];
- // For each standaard, collect its standaardVersies
+ // For each standaard, collect its standaardVersies.
foreach ($refCompStandaarden as $standaardUuid) {
- // Convert UUID back to identifier format for lookup
- $standaardIdentifier = 'id-' . $standaardUuid;
+ // Convert UUID back to identifier format for lookup.
+ $standaardIdentifier = 'id-'.$standaardUuid;
- if (isset($standaardToVersiesMap[$standaardIdentifier])) {
+ if (isset($standaardToVersiesMap[$standaardIdentifier]) === true) {
$standaardVersiesForRefComp = array_merge(
$standaardVersiesForRefComp,
$standaardToVersiesMap[$standaardIdentifier]
@@ -3495,176 +4030,182 @@ private function processGemmaReferenceComponentStandards(array $objects): array
}
}
- // Remove duplicates and add to referentiecomponent
- // Use 'gekoppeldeStandaardVersies' to avoid conflict with inversedBy on 'standaardVersies'
- if (!empty($standaardVersiesForRefComp)) {
+ // Remove duplicates and add to referentiecomponent.
+ // Use 'gekoppeldeStandaardVersies' to avoid conflict with inversedBy on 'standaardVersies'.
+ if (empty($standaardVersiesForRefComp) === false) {
$objects[$objectIndex]['gekoppeldeStandaardVersies'] = array_values(array_unique($standaardVersiesForRefComp));
$refCompWithVersiesCount++;
}
- }
+ }//end foreach
- $this->logger->info('GEMMA ReferentieComponent-StandaardVersies processing completed', [
- 'referentiecomponenten_with_versies' => $refCompWithVersiesCount,
- 'total_referentiecomponenten' => count($referentieComponenten),
- 'standaard_to_versies_mappings' => count($standaardToVersiesMap)
- ]);
+ $this->logger->info(
+ 'GEMMA ReferentieComponent-StandaardVersies processing completed',
+ [
+ 'referentiecomponenten_with_versies' => $refCompWithVersiesCount,
+ 'total_referentiecomponenten' => count($referentieComponenten),
+ 'standaard_to_versies_mappings' => count($standaardToVersiesMap),
+ ]
+ );
return $objects;
- }
+ }//end processGemmaReferenceComponentStandards()
/**
* Process StandaardVersie-Standaard relationships (Specialization type)
*
- * @param array $relationship The relationship object
- * @param array $standaardVersies Array of StandaardVersie identifiers
- * @param array $standaarden Array of Standaard identifiers
- * @param array &$standaardVersieRelationshipMap Map of StandaardVersie -> Standaard (by reference)
+ * @param array $relationship The relationship object
+ * @param array $standaardVersies Array of StandaardVersie identifiers
+ * @param array $standaarden Array of Standaard identifiers
+ * @param array &$standaardVersieRelationshipMap Map of StandaardVersie -> Standaard (by reference)
+ *
* @return void
*/
private function processStandaardVersieRelationship(array $relationship, array $standaardVersies, array $standaarden, array &$standaardVersieRelationshipMap): void
{
- // Get source and target from relationship
- $source = $this->extractRelationshipEndpoint($relationship, 'source');
- $target = $this->extractRelationshipEndpoint($relationship, 'target');
+ // Get source and target from relationship.
+ $source = $this->extractRelationshipEndpoint(relationship: $relationship, endpoint: 'source');
+ $target = $this->extractRelationshipEndpoint(relationship: $relationship, endpoint: 'target');
- if (!$source || !$target) {
+ if ($source === null || $target === false) {
return;
}
- // Get relationship type (looking for Specialization)
- // Type can be in 'type' (from _xsi__type) or in _attributes['xsi:type']
+ // Get relationship type (looking for Specialization).
+ // Type can be in 'type' (from _xsi__type) or in _attributes['xsi:type'].
$relationType = $relationship['type'] ?? $relationship['_xsi__type'] ?? $relationship['_attributes']['xsi:type'] ?? null;
if ($relationType !== 'Specialization') {
return;
}
- // Check if one end is a StandaardVersie and the other is a Standaard
- $versieId = null;
+ // Check if one end is a StandaardVersie and the other is a Standaard.
+ $versieId = null;
$standaardId = null;
- if (isset($standaardVersies[$source]) && isset($standaarden[$target])) {
- // StandaardVersie -> Standaard
- $versieId = $source;
+ if (isset($standaardVersies[$source]) === true && isset($standaarden[$target]) === true) {
+ // StandaardVersie -> Standaard.
+ $versieId = $source;
$standaardId = $target;
- } elseif (isset($standaarden[$source]) && isset($standaardVersies[$target])) {
- // Standaard -> StandaardVersie (reverse direction)
- $versieId = $target;
+ } else if (isset($standaarden[$source]) === true && isset($standaardVersies[$target]) === true) {
+ // Standaard -> StandaardVersie (reverse direction).
+ $versieId = $target;
$standaardId = $source;
}
- if ($versieId && $standaardId) {
+ if ($versieId !== false && $standaardId === true) {
$standaardVersieRelationshipMap[$versieId] = $standaardId;
}
- }
+ }//end processStandaardVersieRelationship()
/**
* OPTIMIZATION: Process relationship immediately when found (single-pass algorithm)
*
- * @param array $relationship The relationship object
- * @param array $referentieComponenten Array of Referentiecomponent identifiers
- * @param array $standaarden Array of Standaard identifiers
- * @param array &$gemmaRelationshipMap The relationship map to update (by reference)
+ * @param array $relationship The relationship object
+ * @param array $referentieComponenten Array of Referentiecomponent identifiers
+ * @param array $standaarden Array of Standaard identifiers
+ * @param array &$gemmaRelationshipMap The relationship map to update (by reference)
+ *
* @return void
*/
private function processRelationshipImmediate(array $relationship, array $referentieComponenten, array $standaarden, array &$gemmaRelationshipMap): void
{
- // Get source and target from relationship XML or flattened properties
- $source = $this->extractRelationshipEndpoint($relationship, 'source');
- $target = $this->extractRelationshipEndpoint($relationship, 'target');
+ // Get source and target from relationship XML or flattened properties.
+ $source = $this->extractRelationshipEndpoint(relationship: $relationship, endpoint: 'source');
+ $target = $this->extractRelationshipEndpoint(relationship: $relationship, endpoint: 'target');
- if (!$source || !$target) {
+ if ($source === null || $target === false) {
return;
}
- // Get Verbindingsrol from flattened properties (camelCase: verbindingsrol)
+ // Get Verbindingsrol from flattened properties (camelCase: verbindingsrol).
$verbindingsrol = $relationship['verbindingsrol'] ?? null;
- // Skip if no Verbindingsrol is defined
- if (!$verbindingsrol) {
+ // Skip if no Verbindingsrol is defined.
+ if ($verbindingsrol === null) {
return;
}
- // Check if one end is a Referentiecomponent and the other is a Standaard
- $refCompId = null;
+ // Check if one end is a Referentiecomponent and the other is a Standaard.
+ $refCompId = null;
$standaardId = null;
- if (isset($referentieComponenten[$source]) && isset($standaarden[$target])) {
- // Referentiecomponent -> Standaard
- $refCompId = $source;
+ if (isset($referentieComponenten[$source]) === true && isset($standaarden[$target]) === true) {
+ // Referentiecomponent -> Standaard.
+ $refCompId = $source;
$standaardId = $target;
- } elseif (isset($standaarden[$source]) && isset($referentieComponenten[$target])) {
- // Standaard -> Referentiecomponent (reverse direction)
- $refCompId = $target;
+ } else if (isset($standaarden[$source]) === true && isset($referentieComponenten[$target]) === true) {
+ // Standaard -> Referentiecomponent (reverse direction).
+ $refCompId = $target;
$standaardId = $source;
}
- if ($refCompId && $standaardId) {
- // Initialize arrays if not exists
- if (!isset($gemmaRelationshipMap[$refCompId])) {
+ if ($refCompId !== false && $standaardId === true) {
+ // Initialize arrays if not exists.
+ if (isset($gemmaRelationshipMap[$refCompId]) === false) {
$gemmaRelationshipMap[$refCompId] = [
'aanbevolen' => [],
- 'verplicht' => []
+ 'verplicht' => [],
];
}
- if(is_string($verbindingsrol) === false) {
+ if (is_string($verbindingsrol) === false) {
return;
}
- // Add to appropriate array based on Verbindingsrol
- // Convert identifier to UUID format (remove "id-" prefix) for _extend compatibility
+ // Add to appropriate array based on Verbindingsrol.
+ // Convert identifier to UUID format (remove "id-" prefix) for _extend compatibility.
$standaardUuid = str_replace('id-', '', $standaardId);
if (strtolower($verbindingsrol) === 'aanbevolen') {
$gemmaRelationshipMap[$refCompId]['aanbevolen'][] = $standaardUuid;
- } elseif (strtolower($verbindingsrol) === 'verplicht') {
+ } else if (strtolower($verbindingsrol) === 'verplicht') {
$gemmaRelationshipMap[$refCompId]['verplicht'][] = $standaardUuid;
}
- }
- }
+ }//end if
+ }//end processRelationshipImmediate()
/**
* Extract relationship endpoint (source or target) from relationship object
*
- * @param array $relationship The relationship object
- * @param string $endpoint Either 'source' or 'target'
+ * @param array $relationship The relationship object
+ * @param string $endpoint Either 'source' or 'target'
+ *
* @return string|null The endpoint identifier or null if not found
*/
private function extractRelationshipEndpoint(array $relationship, string $endpoint): ?string
{
- // Try flattened camelCase property first
- if (isset($relationship[$endpoint])) {
+ // Try flattened camelCase property first.
+ if (isset($relationship[$endpoint]) === true) {
return $relationship[$endpoint];
}
- // Try XML structure
- if (isset($relationship['xml'][$endpoint])) {
+ // Try XML structure.
+ if (isset($relationship['xml'][$endpoint]) === true) {
$endpointData = $relationship['xml'][$endpoint];
- // Handle different XML structures
- if (is_string($endpointData)) {
+ // Handle different XML structures.
+ if (is_string($endpointData) === true) {
return $endpointData;
- } elseif (is_array($endpointData)) {
- // Try _attributes.href or _value
- if (isset($endpointData['_attributes']['href'])) {
+ } else if (is_array($endpointData) === true) {
+ // Try _attributes.href or _value.
+ if (isset($endpointData['_attributes']['href']) === true) {
return $endpointData['_attributes']['href'];
- } elseif (isset($endpointData['_value'])) {
+ } else if (isset($endpointData['_value']) === true) {
return $endpointData['_value'];
}
}
}
- // Try direct XML access for ArchiMate format
- if (isset($relationship['xml']['_attributes'])) {
+ // Try direct XML access for ArchiMate format.
+ if (isset($relationship['xml']['_attributes']) === true) {
$attr = $relationship['xml']['_attributes'];
- if ($endpoint === 'source' && isset($attr['source'])) {
+ if ($endpoint === 'source' && isset($attr['source']) === true) {
return $attr['source'];
- } elseif ($endpoint === 'target' && isset($attr['target'])) {
+ } else if ($endpoint === 'target' && isset($attr['target']) === true) {
return $attr['target'];
}
}
return null;
- }
+ }//end extractRelationshipEndpoint()
/**
* SPEED OPTIMIZED: Transform ArchiMate XML data with maximum performance focus
@@ -3676,110 +4217,118 @@ private function extractRelationshipEndpoint(array $relationship, string $endpoi
* 4. Eliminate redundant operations through aggressive caching
* 5. Process everything in memory-intensive but fast data structures
*
- * @param array $xmlData Parsed XML data
- * @param string $modelIdentifier Model identifier
+ * @param array $xmlData Parsed XML data
+ * @param string $modelIdentifier Model identifier
+ *
* @return array Array of objects ready for saveObjects()
*/
private function transformArchiMateXmlToObjectsBatch(array $xmlData, string $modelIdentifier): array
{
- $startTime = microtime(true);
+ $startTime = microtime(true);
$allObjects = [];
- // SPEED OPTIMIZATION 1: Pre-extract and cache EVERYTHING
- $cacheStartTime = microtime(true);
- $propertyDefinitionMap = $this->extractPropertyDefinitionMap($xmlData);
+ // SPEED OPTIMIZATION 1: Pre-extract and cache EVERYTHING.
+ $cacheStartTime = microtime(true);
+ $propertyDefinitionMap = $this->extractPropertyDefinitionMap(data: $xmlData);
- // Create model object first
- if (isset($xmlData['_attributes']) || isset($xmlData['name'])) {
+ // Create model object first.
+ if (isset($xmlData['_attributes']) === true || isset($xmlData['name']) === true) {
$modelMetadata = [
- 'identifier' => $modelIdentifier,
- 'name' => $xmlData['name'] ?? '',
- 'documentation' => $xmlData['documentation'] ?? '',
- 'properties' => $xmlData['properties'] ?? [],
- 'propertyDefinitionMap' => $propertyDefinitionMap
+ 'identifier' => $modelIdentifier,
+ 'name' => $xmlData['name'] ?? '',
+ 'documentation' => $xmlData['documentation'] ?? '',
+ 'properties' => $xmlData['properties'] ?? [],
+ 'propertyDefinitionMap' => $propertyDefinitionMap,
];
- if (isset($xmlData['_attributes'])) {
+ if (isset($xmlData['_attributes']) === true) {
$modelMetadata = array_merge($modelMetadata, $xmlData['_attributes']);
}
- $allObjects[] = $this->createModelObjectDirect($modelMetadata, $modelIdentifier);
+ $allObjects[] = $this->createModelObjectDirect(metadata: $modelMetadata, modelIdentifier: $modelIdentifier);
}
- // Process each section type directly (no intermediate normalization)
+ // Process each section type directly (no intermediate normalization).
$sections = [
- 'elements' => 'element',
- 'relationships' => 'relationship',
- 'organizations' => 'organization',
- 'views' => 'view',
- 'property_definitions' => 'property_definition'
+ 'elements' => 'element',
+ 'relationships' => 'relationship',
+ 'organizations' => 'organization',
+ 'views' => 'view',
+ 'property_definitions' => 'property_definition',
];
- // SPEED OPTIMIZATION 2: Pre-build ALL section lookups simultaneously
- $allLookups = $this->buildAllLookupsSimultaneously($xmlData);
- $elementsLookup = $this->buildElementsLookup($allObjects); // Will be rebuilt from processed objects
-
+ // SPEED OPTIMIZATION 2: Pre-build ALL section lookups simultaneously.
+ $allLookups = $this->buildAllLookupsSimultaneously(xmlData: $xmlData);
+ $elementsLookup = $this->buildElementsLookup(elementObjects: $allObjects);
+ // Will be rebuilt from processed objects.
$cacheTime = microtime(true) - $cacheStartTime;
- $this->logger->info('Pre-built all lookups', [
- 'cache_build_time' => round($cacheTime, 3),
- 'elements_count' => count($allLookups['elements']),
- 'relationships_count' => count($allLookups['relationships']),
- 'organizations_count' => count($allLookups['organizations']),
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 1)
- ]);
-
- // SPEED OPTIMIZATION 3: Process all non-view sections in bulk
+ $this->logger->info(
+ 'Pre-built all lookups',
+ [
+ 'cache_build_time' => round($cacheTime, 3),
+ 'elements_count' => count($allLookups['elements']),
+ 'relationships_count' => count($allLookups['relationships']),
+ 'organizations_count' => count($allLookups['organizations']),
+ 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 1),
+ ]
+ );
+
+ // SPEED OPTIMIZATION 3: Process all non-view sections in bulk.
$bulkProcessingStart = microtime(true);
- $nonViewObjects = $this->bulkProcessNonViewSections(
+ $nonViewObjects = $this->bulkProcessNonViewSections(
$xmlData,
$modelIdentifier,
$propertyDefinitionMap,
$allLookups
);
- $allObjects = array_merge($allObjects, $nonViewObjects);
+ $allObjects = array_merge($allObjects, $nonViewObjects);
- // SPEED OPTIMIZATION: Build elements lookup directly from raw data (faster than from processed objects)
- $elementsLookup = $this->buildElementsLookupFromRawData($allLookups['elements'], $nonViewObjects, $propertyDefinitionMap);
+ // SPEED OPTIMIZATION: Build elements lookup directly from raw data (faster than from processed objects).
+ $elementsLookup = $this->buildElementsLookupFromRawData(rawElementsData: $allLookups['elements'], processedObjects: $nonViewObjects, propertyDefinitionMap: $propertyDefinitionMap);
$bulkTime = microtime(true) - $bulkProcessingStart;
- // SPEED OPTIMIZATION 4: Process views with maximum speed optimizations
+ // SPEED OPTIMIZATION 4: Process views with maximum speed optimizations.
$viewProcessingStart = microtime(true);
- $viewObjects = $this->processViewsMaximumSpeed(
+ $viewObjects = $this->processViewsMaximumSpeed(
$xmlData,
$modelIdentifier,
$propertyDefinitionMap,
$elementsLookup
);
- $allObjects = array_merge($allObjects, $viewObjects);
- $viewTime = microtime(true) - $viewProcessingStart;
+ $allObjects = array_merge($allObjects, $viewObjects);
+ $viewTime = microtime(true) - $viewProcessingStart;
$totalTime = microtime(true) - $startTime;
- // Transformation completed
-
- // MEMORY CLEANUP: Free all intermediate lookups and caches before database operations
+ // Transformation completed.
+ // MEMORY CLEANUP: Free all intermediate lookups and caches before database operations.
$memoryBeforeCleanup = memory_get_usage(true);
unset($allLookups, $elementsLookup, $propertyDefinitionMap);
- $this->camelCaseCache = []; // Clear property name cache
- $this->identifierPatternCache = []; // Clear identifier pattern cache
- $this->propertyDefinitionMapCache = null; // Clear property definition cache
-
- // Force garbage collection to free memory immediately
- if (function_exists('gc_collect_cycles')) {
+ $this->camelCaseCache = [];
+ // Clear property name cache.
+ $this->identifierPatternCache = [];
+ // Clear identifier pattern cache.
+ $this->propertyDefinitionMapCache = null;
+ // Clear property definition cache.
+ // Force garbage collection to free memory immediately.
+ if (function_exists('gc_collect_cycles') === true) {
$cycles = gc_collect_cycles();
$memoryAfterCleanup = memory_get_usage(true);
- $memoryFreed = $memoryBeforeCleanup - $memoryAfterCleanup;
-
- $this->logger->info('Memory cleanup before database operations', [
- 'memory_freed_mb' => round($memoryFreed / 1024 / 1024, 1),
- 'gc_cycles_collected' => $cycles,
- 'memory_before_mb' => round($memoryBeforeCleanup / 1024 / 1024, 1),
- 'memory_after_mb' => round($memoryAfterCleanup / 1024 / 1024, 1)
- ]);
+ $memoryFreed = $memoryBeforeCleanup - $memoryAfterCleanup;
+
+ $this->logger->info(
+ 'Memory cleanup before database operations',
+ [
+ 'memory_freed_mb' => round($memoryFreed / 1024 / 1024, 1),
+ 'gc_cycles_collected' => $cycles,
+ 'memory_before_mb' => round($memoryBeforeCleanup / 1024 / 1024, 1),
+ 'memory_after_mb' => round($memoryAfterCleanup / 1024 / 1024, 1),
+ ]
+ );
}
return $allObjects;
- }
+ }//end transformArchiMateXmlToObjectsBatch()
/**
* Transform views with performance optimizations
@@ -3789,10 +4338,11 @@ private function transformArchiMateXmlToObjectsBatch(array $xmlData, string $mod
* - Optimized element lookup caching
* - Streamlined recursive processing
*
- * @param array $viewsData Views section data
- * @param string $modelIdentifier Model identifier
- * @param array $propertyDefinitionMap Property definition map
- * @param array $elementsLookup Elements lookup for splicing
+ * @param array $viewsData Views section data
+ * @param string $modelIdentifier Model identifier
+ * @param array $propertyDefinitionMap Property definition map
+ * @param array $elementsLookup Elements lookup for splicing
+ *
* @return array Array of processed view objects
*/
private function transformViewsOptimized(
@@ -3803,100 +4353,105 @@ private function transformViewsOptimized(
): array {
$objects = [];
- // Find items in section (optimized version for views)
- $items = $this->findItemsSimplified($viewsData, 'view');
+ // Find items in section (optimized version for views).
+ $items = $this->findItemsSimplified(sectionData: $viewsData, sectionType: 'view');
- // OPTIMIZATION: Pre-filter elements to only those actually referenced in views
- $referencedElements = $this->extractReferencedElements($items);
+ // OPTIMIZATION: Pre-filter elements to only those actually referenced in views.
+ $referencedElements = $this->extractReferencedElements(viewItems: $items);
$filteredElementsLookup = array_intersect_key($elementsLookup, array_flip($referencedElements));
- $this->logger->debug('Optimized elements lookup for views', [
- 'total_elements' => count($elementsLookup),
- 'referenced_elements' => count($filteredElementsLookup),
- 'optimization_ratio' => round((1 - count($filteredElementsLookup) / max(count($elementsLookup), 1)) * 100, 1) . '%'
- ]);
+ $this->logger->debug(
+ 'Optimized elements lookup for views',
+ [
+ 'total_elements' => count($elementsLookup),
+ 'referenced_elements' => count($filteredElementsLookup),
+ 'optimization_ratio' => round((1 - count($filteredElementsLookup) / max(count($elementsLookup), 1)) * 100, 1).'%',
+ ]
+ );
foreach ($items as $item) {
- if (!is_array($item)) {
+ if (is_array($item) === false) {
continue;
}
- $identifier = $this->extractIdentifier($item, 'view');
- if (!$identifier) {
+ $identifier = $this->extractIdentifier(item: $item, sectionName: 'view');
+ if ($identifier === null) {
continue;
}
- // OPTIMIZATION: Use filtered elements lookup for better performance
- $essentialXmlData = $this->extractEssentialXmlData($item, $filteredElementsLookup, 'view');
+ // OPTIMIZATION: Use filtered elements lookup for better performance.
+ $essentialXmlData = $this->extractEssentialXmlData(item: $item, elementsLookup: $filteredElementsLookup, schemaType: 'view');
$object = [
- '@self' => [
- 'register' => $this->cachedConfig['registerId'] ?? throw new \RuntimeException("Register ID not found in cached configuration. Please ensure AMEF configuration is properly initialized."),
- 'schema' => $this->getSchemaIdForSection('view'),
- 'id' => $identifier,
- 'owner' => $this->cachedConfig['userId'],
+ '@self' => [
+ 'register' => $this->cachedConfig['registerId'] ?? throw new \RuntimeException("Register ID not found in cached configuration. Please ensure AMEF configuration is properly initialized."),
+ 'schema' => $this->getSchemaIdForSection(section: 'view'),
+ 'id' => $identifier,
+ 'owner' => $this->cachedConfig['userId'],
'organisation' => $this->getCurrentOrganisation(),
],
- 'identifier' => $identifier,
- 'section' => 'view',
+ 'identifier' => $identifier,
+ 'section' => 'view',
'model_identifier' => $modelIdentifier,
- 'xml' => $essentialXmlData
+ 'xml' => $essentialXmlData,
];
- // Extract name and summary (same as other sections)
- if (isset($item['name'])) {
- if (is_array($item['name']) && isset($item['name']['_value'])) {
+ // Extract name and summary (same as other sections).
+ if (isset($item['name']) === true) {
+ if (is_array($item['name']) === true && isset($item['name']['_value']) === true) {
$object['name'] = $item['name']['_value'];
- } elseif (is_string($item['name'])) {
+ } else if (is_string($item['name']) === true) {
$object['name'] = $item['name'];
}
}
- if (isset($item['documentation'])) {
- if (is_array($item['documentation']) && isset($item['documentation']['_value'])) {
+ if (isset($item['documentation']) === true) {
+ if (is_array($item['documentation']) === true && isset($item['documentation']['_value']) === true) {
$object['summary'] = $item['documentation']['_value'];
- } elseif (is_string($item['documentation'])) {
+ } else if (is_string($item['documentation']) === true) {
$object['summary'] = $item['documentation'];
}
}
- // Extract type from xsi:type attribute
- if (isset($item['_xsi__type'])) {
+ // Extract type from xsi:type attribute.
+ if (isset($item['_xsi__type']) === true) {
$object['type'] = $item['_xsi__type'];
- } elseif (isset($item['_attributes']['xsi:type'])) {
+ } else if (isset($item['_attributes']['xsi:type']) === true) {
$object['type'] = $item['_attributes']['xsi:type'];
}
- // Flatten properties efficiently (same as other sections)
- if (isset($item['properties']['property']) && !empty($propertyDefinitionMap)) {
- $this->flattenPropertiesBatch($object, $item['properties']['property'], $propertyDefinitionMap);
+ // Flatten properties efficiently (same as other sections).
+ if (isset($item['properties']['property']) === true && empty($propertyDefinitionMap) === false) {
+ $this->flattenPropertiesBatch(object: $object, properties: $item['properties']['property'], propertyDefinitionMap: $propertyDefinitionMap);
- // Keep @self.id as the full ArchiMate identifier (set above)
+ // Keep @self.id as the full ArchiMate identifier (set above).
// so stored IDs match GEMMA Online URLs (id-e0f57689-...).
$object['@self']['slug'] = $identifier;
} else {
$object['@self']['slug'] = $identifier;
}
- // Copy viewNodes and viewRelationships from XML to root level for easy access
- if (isset($object['xml']['viewNodes'])) {
+ // Copy viewNodes and viewRelationships from XML to root level for easy access.
+ if (isset($object['xml']['viewNodes']) === true) {
$object['viewNodes'] = $object['xml']['viewNodes'];
}
- if (isset($object['xml']['viewRelationships'])) {
+
+ if (isset($object['xml']['viewRelationships']) === true) {
$object['viewRelationships'] = $object['xml']['viewRelationships'];
}
$objects[] = $object;
- }
+ }//end foreach
return $objects;
- }
+ }//end transformViewsOptimized()
/**
* Extract all element references from view items for optimization
*
- * @param array $viewItems Array of view items
+ * @param array $viewItems Array of view items
+ *
* @return array Array of referenced element identifiers
*/
private function extractReferencedElements(array $viewItems): array
@@ -3904,38 +4459,39 @@ private function extractReferencedElements(array $viewItems): array
$references = [];
foreach ($viewItems as $item) {
- $this->collectElementReferencesRecursively($item, $references);
+ $this->collectElementReferencesRecursively(data: $item, references: $references);
}
return array_unique($references);
- }
+ }//end extractReferencedElements()
/**
* Recursively collect element references from view data
*
- * @param array $data View data to process
- * @param array &$references Array to collect references into (by reference)
+ * @param array $data View data to process
+ * @param array &$references Array to collect references into (by reference)
+ *
* @return void
*/
private function collectElementReferencesRecursively(array $data, array &$references): void
{
- // Check for elementRef in current level
- if (isset($data['_attributes']['elementRef'])) {
+ // Check for elementRef in current level.
+ if (isset($data['_attributes']['elementRef']) === true) {
$references[] = $data['_attributes']['elementRef'];
}
- // Recursively check child nodes
- if (isset($data['node'])) {
+ // Recursively check child nodes.
+ if (isset($data['node']) === true) {
$nodeData = $data['node'];
- if (!isset($nodeData[0])) {
+ if (isset($nodeData[0]) === false) {
$nodeData = [$nodeData];
}
foreach ($nodeData as $node) {
- $this->collectElementReferencesRecursively($node, $references);
+ $this->collectElementReferencesRecursively(data: $node, references: $references);
}
}
- }
+ }//end collectElementReferencesRecursively()
/**
* Build elements lookup array for view processing with element splicing
@@ -3943,7 +4499,8 @@ private function collectElementReferencesRecursively(array $data, array &$refere
* This method creates a fast lookup array of elements by their identifier
* to enable efficient element splicing during view node processing.
*
- * @param array $elementObjects Array of processed element objects
+ * @param array $elementObjects Array of processed element objects
+ *
* @return array Lookup array with element identifier as key and element data as value
*/
private function buildElementsLookup(array $elementObjects): array
@@ -3952,18 +4509,21 @@ private function buildElementsLookup(array $elementObjects): array
foreach ($elementObjects as $element) {
$identifier = $element['identifier'] ?? null;
- if ($identifier) {
+ if (empty($identifier) === false) {
$lookup[$identifier] = $element;
}
}
- $this->logger->debug('Built elements lookup for view processing', [
- 'total_elements' => count($lookup),
- 'sample_identifiers' => array_slice(array_keys($lookup), 0, 5)
- ]);
+ $this->logger->debug(
+ 'Built elements lookup for view processing',
+ [
+ 'total_elements' => count($lookup),
+ 'sample_identifiers' => array_slice(array_keys($lookup), 0, 5),
+ ]
+ );
return $lookup;
- }
+ }//end buildElementsLookup()
/**
* SPEED OPTIMIZATION: Build elements lookup directly from raw XML data
@@ -3971,9 +4531,10 @@ private function buildElementsLookup(array $elementObjects): array
* This is faster than building from processed objects because we skip intermediate processing
* and build the lookup table directly from the source data with minimal transformations.
*
- * @param array $rawElementsData Raw elements data from XML
- * @param array $processedObjects Already processed objects (for fallback)
- * @param array $propertyDefinitionMap Property definition map
+ * @param array $rawElementsData Raw elements data from XML
+ * @param array $processedObjects Already processed objects (for fallback)
+ * @param array $propertyDefinitionMap Property definition map
+ *
* @return array Elements lookup for view processing
*/
private function buildElementsLookupFromRawData(
@@ -3983,164 +4544,181 @@ private function buildElementsLookupFromRawData(
): array {
$lookup = [];
- // SPEED: Build directly from raw data with minimal processing
+ // SPEED: Build directly from raw data with minimal processing.
foreach ($rawElementsData as $identifier => $rawItem) {
$element = [
'identifier' => $identifier,
- 'section' => 'element'
+ 'section' => 'element',
];
- // Fast name extraction
- if (isset($rawItem['name'])) {
- $element['name'] = is_array($rawItem['name']) && isset($rawItem['name']['_value'])
- ? $rawItem['name']['_value']
- : (is_string($rawItem['name']) ? $rawItem['name'] : '');
+ // Fast name extraction.
+ if (isset($rawItem['name']) === true) {
+ if (is_array($rawItem['name']) === true && isset($rawItem['name']['_value']) === true) {
+ $element['name'] = $rawItem['name']['_value'];
+ } else {
+ $element['name'] = (is_string($rawItem['name']) === true ? $rawItem['name'] : '');
+ }
}
- // Fast summary extraction
- if (isset($rawItem['documentation'])) {
- $element['summary'] = is_array($rawItem['documentation']) && isset($rawItem['documentation']['_value'])
- ? $rawItem['documentation']['_value']
- : (is_string($rawItem['documentation']) ? $rawItem['documentation'] : '');
+ // Fast summary extraction.
+ if (isset($rawItem['documentation']) === true) {
+ if (is_array($rawItem['documentation']) === true && isset($rawItem['documentation']['_value']) === true) {
+ $element['summary'] = $rawItem['documentation']['_value'];
+ } else {
+ $element['summary'] = (is_string($rawItem['documentation']) === true ? $rawItem['documentation'] : '');
+ }
}
- // Extract type from xsi:type attribute
- if (isset($rawItem['_xsi__type'])) {
+ // Extract type from xsi:type attribute.
+ if (isset($rawItem['_xsi__type']) === true) {
$element['type'] = $rawItem['_xsi__type'];
- } elseif (isset($rawItem['_attributes']['xsi:type'])) {
+ } else if (isset($rawItem['_attributes']['xsi:type']) === true) {
$element['type'] = $rawItem['_attributes']['xsi:type'];
}
- // Fast properties flattening (only essential properties for splicing)
- if (isset($rawItem['properties']['property']) && !empty($propertyDefinitionMap)) {
- $props = isset($rawItem['properties']['property'][0])
- ? $rawItem['properties']['property']
- : [$rawItem['properties']['property']];
+ // Fast properties flattening (only essential properties for splicing).
+ if (isset($rawItem['properties']['property']) === true && empty($propertyDefinitionMap) === false) {
+ if (isset($rawItem['properties']['property'][0]) === true) {
+ $props = $rawItem['properties']['property'];
+ } else {
+ $props = [$rawItem['properties']['property']];
+ }
foreach ($props as $prop) {
- if (!isset($prop['_attributes']['propertyDefinitionRef'])) continue;
+ if (isset($prop['_attributes']['propertyDefinitionRef']) === false) {
+ continue;
+ }
$defRef = $prop['_attributes']['propertyDefinitionRef'];
- $value = $prop['value']['_value'] ?? $prop['value'] ?? null;
+ $value = $prop['value']['_value'] ?? $prop['value'] ?? null;
- if ($value !== null && isset($propertyDefinitionMap[$defRef])) {
- $propertyName = $propertyDefinitionMap[$defRef];
- $camelCaseName = $this->convertToCamelCase($propertyName);
+ if ($value !== null && isset($propertyDefinitionMap[$defRef]) === true) {
+ $propertyName = $propertyDefinitionMap[$defRef];
+ $camelCaseName = $this->convertToCamelCase(propertyName: $propertyName);
$element[$camelCaseName] = $value;
}
}
- }
+ }//end if
$lookup[$identifier] = $element;
- }
+ }//end foreach
- $this->logger->debug('Built SPEED elements lookup from raw data', [
- 'total_elements' => count($lookup),
- 'sample_identifiers' => array_slice(array_keys($lookup), 0, 5)
- ]);
+ $this->logger->debug(
+ 'Built SPEED elements lookup from raw data',
+ [
+ 'total_elements' => count($lookup),
+ 'sample_identifiers' => array_slice(array_keys($lookup), 0, 5),
+ ]
+ );
return $lookup;
- }
+ }//end buildElementsLookupFromRawData()
/**
* Create model object directly with cached configuration
*
- * @param array $metadata Model metadata
- * @param string $modelIdentifier Model identifier
+ * @param array $metadata Model metadata
+ * @param string $modelIdentifier Model identifier
+ *
* @return array Model object with @self structure
*/
private function createModelObjectDirect(array $metadata, string $modelIdentifier): array
{
$organisation = $this->getCurrentOrganisation();
- // Extract a plain string name (schema column expects string, not array)
+ // Extract a plain string name (schema column expects string, not array).
$nameString = null;
- if (isset($metadata['name'])) {
- if (is_array($metadata['name']) && isset($metadata['name']['_value'])) {
- $nameString = (string)$metadata['name']['_value'];
- } elseif (is_string($metadata['name'])) {
+ if (isset($metadata['name']) === true) {
+ if (is_array($metadata['name']) === true && isset($metadata['name']['_value']) === true) {
+ $nameString = (string) $metadata['name']['_value'];
+ } else if (is_string($metadata['name']) === true) {
$nameString = $metadata['name'];
}
}
- // Build xml field preserving full array structure for round-trip fidelity
+ // Build xml field preserving full array structure for round-trip fidelity.
$xmlData = [];
- if (isset($metadata['name'])) {
+ if (isset($metadata['name']) === true) {
$xmlData['name'] = $metadata['name'];
}
- if (isset($metadata['documentation'])) {
+
+ if (isset($metadata['documentation']) === true) {
$xmlData['documentation'] = $metadata['documentation'];
}
- if (isset($metadata['properties'])) {
+
+ if (isset($metadata['properties']) === true) {
$xmlData['properties'] = $metadata['properties'];
}
- if (isset($metadata['propertyDefinitionMap'])) {
+
+ if (isset($metadata['propertyDefinitionMap']) === true) {
$xmlData['propertyDefinitionMap'] = $metadata['propertyDefinitionMap'];
}
$object = [
- '@self' => [
- 'register' => $this->cachedConfig['registerId'] ?? throw new \RuntimeException("Register ID not found in cached configuration. Please ensure AMEF configuration is properly initialized."),
- 'schema' => $this->cachedConfig['schemaIds']['model'] ?? throw new \RuntimeException("Schema ID for 'model' not found in cached configuration. Please ensure AMEF configuration is properly initialized."),
- 'id' => $modelIdentifier,
- 'owner' => $this->cachedConfig['userId'],
+ '@self' => [
+ 'register' => $this->cachedConfig['registerId'] ?? throw new \RuntimeException("Register ID not found in cached configuration. Please ensure AMEF configuration is properly initialized."),
+ 'schema' => $this->cachedConfig['schemaIds']['model'] ?? throw new \RuntimeException("Schema ID for 'model' not found in cached configuration. Please ensure AMEF configuration is properly initialized."),
+ 'id' => $modelIdentifier,
+ 'owner' => $this->cachedConfig['userId'],
'organisation' => $organisation,
- 'published' => date('Y-m-d\TH:i:s\Z')
+ 'published' => date('Y-m-d\TH:i:s\Z'),
],
- 'identifier' => $modelIdentifier,
- 'section' => 'model',
+ 'identifier' => $modelIdentifier,
+ 'section' => 'model',
'model_identifier' => $modelIdentifier,
- 'xml' => $xmlData
+ 'xml' => $xmlData,
] + $metadata;
- // Override name with string version so schema column stores it properly
+ // Override name with string version so schema column stores it properly.
if ($nameString !== null) {
$object['name'] = $nameString;
}
return $object;
- }
+ }//end createModelObjectDirect()
/**
* Find section data efficiently without complex nested searches
*
- * @param array $xmlData Parsed XML data
- * @param string $sectionName Section name to find
+ * @param array $xmlData Parsed XML data
+ * @param string $sectionName Section name to find
+ *
* @return array Section data or empty array
*/
private function findSectionData(array $xmlData, string $sectionName): array
{
- // Direct lookup first
- if (isset($xmlData[$sectionName])) {
+ // Direct lookup first.
+ if (isset($xmlData[$sectionName]) === true) {
return $xmlData[$sectionName];
}
- // Alternative names lookup
+ // Alternative names lookup.
$alternatives = [
- 'views' => ['diagrams'],
- 'organizations' => ['organisation'],
- 'property_definitions' => ['propertyDefinitions', 'propertydefinitions']
+ 'views' => ['diagrams'],
+ 'organizations' => ['organisation'],
+ 'property_definitions' => ['propertyDefinitions', 'propertydefinitions'],
];
- if (isset($alternatives[$sectionName])) {
+ if (isset($alternatives[$sectionName]) === true) {
foreach ($alternatives[$sectionName] as $altName) {
- if (isset($xmlData[$altName])) {
+ if (isset($xmlData[$altName]) === true) {
return $xmlData[$altName];
}
}
}
return [];
- }
+ }//end findSectionData()
/**
* Transform section objects in batch with minimal overhead and element splicing for views
*
- * @param array $sectionData Section data from XML
- * @param string $schemaType Schema type (singular)
- * @param string $modelIdentifier Model identifier
- * @param array $propertyDefinitionMap Property definition map
- * @param array $elementsLookup Optional elements lookup for view processing
+ * @param array $sectionData Section data from XML
+ * @param string $schemaType Schema type (singular)
+ * @param string $modelIdentifier Model identifier
+ * @param array $propertyDefinitionMap Property definition map
+ * @param array $elementsLookup Optional elements lookup for view processing
+ *
* @return array Array of transformed objects
*/
private function transformSectionObjectsBatch(
@@ -4148,266 +4726,341 @@ private function transformSectionObjectsBatch(
string $schemaType,
string $modelIdentifier,
array $propertyDefinitionMap,
- array $elementsLookup = []
+ array $elementsLookup=[]
): array {
$objects = [];
- // Find items in section (simplified version)
- $items = $this->findItemsSimplified($sectionData, $schemaType);
+ // Find items in section (simplified version).
+ $items = $this->findItemsSimplified(sectionData: $sectionData, sectionType: $schemaType);
- $skippedNotArray = 0;
+ $skippedNotArray = 0;
$skippedNoIdentifier = 0;
foreach ($items as $item) {
- if (!is_array($item)) {
+ if (is_array($item) === false) {
$skippedNotArray++;
continue;
}
- $identifier = $this->extractIdentifier($item, $schemaType);
- if (!$identifier) {
+ $identifier = $this->extractIdentifier(item: $item, sectionName: $schemaType);
+ if ($identifier === null) {
$skippedNoIdentifier++;
continue;
}
- // Create object directly (minimal processing) with element splicing for views
- $essentialXmlData = $this->extractEssentialXmlData($item, $elementsLookup, $schemaType);
+ // Create object directly (minimal processing) with element splicing for views.
+ $essentialXmlData = $this->extractEssentialXmlData(item: $item, elementsLookup: $elementsLookup, schemaType: $schemaType);
$object = [
- '@self' => [
- 'register' => $this->cachedConfig['registerId'] ?? throw new \RuntimeException("Register ID not found in cached configuration. Please ensure AMEF configuration is properly initialized."),
- 'schema' => $this->cachedConfig['schemaIds'][$schemaType] ?? throw new \RuntimeException("Schema ID for '{$schemaType}' not found in cached configuration. Please ensure AMEF configuration is properly initialized."),
- 'id' => $identifier,
- 'owner' => $this->cachedConfig['userId'],
+ '@self' => [
+ 'register' => $this->cachedConfig['registerId'] ?? throw new \RuntimeException("Register ID not found in cached configuration. Please ensure AMEF configuration is properly initialized."),
+ 'schema' => $this->cachedConfig['schemaIds'][$schemaType] ?? throw new \RuntimeException("Schema ID for '{$schemaType}' not found in cached configuration. Please ensure AMEF configuration is properly initialized."),
+ 'id' => $identifier,
+ 'owner' => $this->cachedConfig['userId'],
'organisation' => $this->getCurrentOrganisation(),
- 'published' => date('Y-m-d\TH:i:s\Z')
+ 'published' => date('Y-m-d\TH:i:s\Z'),
],
- 'identifier' => $identifier,
- 'section' => $schemaType,
+ 'identifier' => $identifier,
+ 'section' => $schemaType,
'model_identifier' => $modelIdentifier,
- 'xml' => $essentialXmlData
+ 'xml' => $essentialXmlData,
];
- // Debug: Log XML data extraction
- $this->logger->debug('XML data extracted for object', [
- 'object_id' => $identifier,
- 'section' => $schemaType,
- 'original_item_keys' => array_keys($item),
- 'essential_xml_keys' => array_keys($essentialXmlData),
- 'essential_xml_size' => strlen(json_encode($essentialXmlData)),
- 'has_properties' => isset($item['properties']),
- 'properties_structure' => isset($item['properties']) ? array_keys($item['properties']) : null
- ]);
-
- // Extract name from XML if it exists
- if (isset($item['name'])) {
- if (is_array($item['name']) && isset($item['name']['_value'])) {
+ // Debug: Log XML data extraction.
+ if (isset($item['properties']) === true) {
+ $propertiesStructureValue = array_keys($item['properties']);
+ } else {
+ $propertiesStructureValue = null;
+ }
+
+ $this->logger->debug(
+ 'XML data extracted for object',
+ [
+ 'object_id' => $identifier,
+ 'section' => $schemaType,
+ 'original_item_keys' => array_keys($item),
+ 'essential_xml_keys' => array_keys($essentialXmlData),
+ 'essential_xml_size' => strlen(json_encode($essentialXmlData)),
+ 'has_properties' => isset($item['properties']) === true,
+ 'properties_structure' => $propertiesStructureValue,
+ ]
+ );
+
+ // Extract name from XML if it exists.
+ if (isset($item['name']) === true) {
+ if (is_array($item['name']) === true && isset($item['name']['_value']) === true) {
$object['name'] = $item['name']['_value'];
- } elseif (is_string($item['name'])) {
+ } else if (is_string($item['name']) === true) {
$object['name'] = $item['name'];
}
}
- // Extract documentation from XML if it exists and set to summary
- if (isset($item['documentation'])) {
- if (is_array($item['documentation']) && isset($item['documentation']['_value'])) {
+ // Extract documentation from XML if it exists and set to summary.
+ if (isset($item['documentation']) === true) {
+ if (is_array($item['documentation']) === true && isset($item['documentation']['_value']) === true) {
$object['summary'] = $item['documentation']['_value'];
- } elseif (is_string($item['documentation'])) {
+ } else if (is_string($item['documentation']) === true) {
$object['summary'] = $item['documentation'];
}
}
- // Extract type from xsi:type attribute (e.g., "Capability", "ApplicationComponent")
- if (isset($item['_xsi__type'])) {
+ // Extract type from xsi:type attribute (e.g., "Capability", "ApplicationComponent").
+ if (isset($item['_xsi__type']) === true) {
$object['type'] = $item['_xsi__type'];
- } elseif (isset($item['_attributes']['xsi:type'])) {
+ } else if (isset($item['_attributes']['xsi:type']) === true) {
$object['type'] = $item['_attributes']['xsi:type'];
}
- // Flatten properties efficiently (if present)
- if (isset($item['properties']['property']) && !empty($propertyDefinitionMap)) {
- $this->flattenPropertiesBatch($object, $item['properties']['property'], $propertyDefinitionMap);
+ // Flatten properties efficiently (if present).
+ if (isset($item['properties']['property']) === true && empty($propertyDefinitionMap) === false) {
+ $this->flattenPropertiesBatch(object: $object, properties: $item['properties']['property'], propertyDefinitionMap: $propertyDefinitionMap);
- // FIXED: After properties are flattened, update ID and slug if objectId is available
- if (isset($object['objectId'])) {
- // Use objectId as main ID and AMEF identifier as slug
- $object['@self']['id'] = $object['objectId'];
- $object['@self']['slug'] = $identifier; // AMEF identifier becomes slug
+ // FIXED: After properties are flattened, update ID and slug if objectId is available.
+ if (isset($object['objectId']) === true) {
+ // Use objectId as main ID and AMEF identifier as slug.
+ $object['@self']['id'] = $object['objectId'];
+ $object['@self']['slug'] = $identifier;
+ // AMEF identifier becomes slug.
} else {
- // Fallback: extract clean UUID from AMEF identifier for slug
- if ($identifier && str_starts_with($identifier, 'id-')) {
- $object['@self']['slug'] = substr($identifier, 3); // Remove "id-" prefix
+ // Fallback: extract clean UUID from AMEF identifier for slug.
+ if ($identifier !== false && str_starts_with($identifier, 'id-') === true) {
+ $object['@self']['slug'] = substr($identifier, 3);
+ // Remove "id-" prefix.
} else {
$object['@self']['slug'] = $identifier;
}
}
} else {
- // No properties to flatten, use AMEF identifier logic
- if ($identifier && str_starts_with($identifier, 'id-')) {
- $object['@self']['slug'] = substr($identifier, 3); // Remove "id-" prefix
+ // No properties to flatten, use AMEF identifier logic.
+ if ($identifier !== false && str_starts_with($identifier, 'id-') === true) {
+ $object['@self']['slug'] = substr($identifier, 3);
+ // Remove "id-" prefix.
} else {
$object['@self']['slug'] = $identifier;
}
- }
+ }//end if
- // NEW: For view objects, copy viewNodes and viewRelationships from XML to root level
- if ($schemaType === 'view' && isset($object['xml'])) {
- if (isset($object['xml']['viewNodes'])) {
+ // NEW: For view objects, copy viewNodes and viewRelationships from XML to root level.
+ if ($schemaType === 'view' && isset($object['xml']) === true) {
+ if (isset($object['xml']['viewNodes']) === true) {
$object['viewNodes'] = $object['xml']['viewNodes'];
}
- if (isset($object['xml']['viewRelationships'])) {
+
+ if (isset($object['xml']['viewRelationships']) === true) {
$object['viewRelationships'] = $object['xml']['viewRelationships'];
}
}
- // DEBUG: Log final object structure before adding to array
- $this->logger->debug('Final object structure before save', [
- 'object_id' => $identifier,
- 'section' => $schemaType,
- 'object_keys' => array_keys($object),
- 'has_xml_property' => isset($object['xml']),
- 'xml_keys' => isset($object['xml']) ? array_keys($object['xml']) : null,
- 'has_property_mapping' => isset($object['_propertyMapping']),
- 'property_mapping_count' => isset($object['_propertyMapping']) ? count($object['_propertyMapping']) : 0,
- 'viewNodes_count' => isset($object['viewNodes']) ? count($object['viewNodes']) : 0,
- 'viewRelationships_count' => isset($object['viewRelationships']) ? count($object['viewRelationships']) : 0,
- 'sample_properties' => array_slice(array_diff(array_keys($object), ['@self', 'identifier', 'section', 'model_identifier', 'xml', '_propertyMapping', 'name', 'summary', 'viewNodes', 'viewRelationships']), 0, 5)
- ]);
+ // DEBUG: Log final object structure before adding to array.
+ if (isset($object['xml']) === true) {
+ $xml_keysValue = array_keys($object['xml']);
+ } else {
+ $xml_keysValue = null;
+ }
- $objects[] = $object;
- }
+ if (isset($object['_propertyMapping']) === true) {
+ $property_mapping_countValue = count($object['_propertyMapping']);
+ } else {
+ $property_mapping_countValue = 0;
+ }
+ if (isset($object['viewNodes']) === true) {
+ $viewNodes_countValue = count($object['viewNodes']);
+ } else {
+ $viewNodes_countValue = 0;
+ }
+
+ if (isset($object['viewRelationships']) === true) {
+ $viewRelationships_countValue = count($object['viewRelationships']);
+ } else {
+ $viewRelationships_countValue = 0;
+ }
+
+ $this->logger->debug(
+ 'Final object structure before save',
+ [
+ 'object_id' => $identifier,
+ 'section' => $schemaType,
+ 'object_keys' => array_keys($object),
+ 'has_xml_property' => isset($object['xml']) === true,
+ 'xml_keys' => $xml_keysValue,
+ 'has_property_mapping' => isset($object['_propertyMapping']) === true,
+ 'property_mapping_count' => $property_mapping_countValue,
+ 'viewNodes_count' => $viewNodes_countValue,
+ 'viewRelationships_count' => $viewRelationships_countValue,
+ 'sample_properties' => array_slice(array_diff(array_keys($object), ['@self', 'identifier', 'section', 'model_identifier', 'xml', '_propertyMapping', 'name', 'summary', 'viewNodes', 'viewRelationships']), 0, 5),
+ ]
+ );
+ $objects[] = $object;
+ }//end foreach
return $objects;
- }
+ }//end transformSectionObjectsBatch()
/**
* Simplified item finding for better performance
*
- * @param array $sectionData Section data
- * @param string $sectionType Section type
+ * @param array $sectionData Section data
+ * @param string $sectionType Section type
+ *
* @return array Items array
*/
private function findItemsSimplified(array $sectionData, string $sectionType): array
{
- // Handle views with diagrams structure
- if ($sectionType === 'view' && isset($sectionData['diagrams']['view'])) {
+ // Handle views with diagrams structure.
+ if ($sectionType === 'view' && isset($sectionData['diagrams']['view']) === true) {
$viewData = $sectionData['diagrams']['view'];
- return isset($viewData[0]) ? $viewData : [$viewData];
+ if (isset($viewData[0]) === true) {
+ return $viewData;
+ } else {
+ return [$viewData];
+ }
}
- // Try common patterns
+ // Try common patterns.
$patterns = [
- $sectionType, // singular: element, relationship, etc.
- $sectionType . 's', // plural: elements, relationships, etc.
- 'item', // organizations use 'item'
- 'propertyDefinition' // property definitions
+ $sectionType,
+ // singular: element, relationship, etc.
+ $sectionType.'s',
+ // plural: elements, relationships, etc.
+ 'item',
+ // organizations use 'item'.
+ 'propertyDefinition',
+ // property definitions.
];
foreach ($patterns as $pattern) {
- if (isset($sectionData[$pattern])) {
+ if (isset($sectionData[$pattern]) === true) {
$data = $sectionData[$pattern];
- return is_array($data) && isset($data[0]) ? $data : [$data];
+ if (is_array($data) === true && isset($data[0]) === true) {
+ return $data;
+ } else {
+ return [$data];
+ }
}
}
- // Fallback: treat section data as single item
+ // Fallback: treat section data as single item.
return [$sectionData];
- }
+ }//end findItemsSimplified()
/**
* Flatten properties in batch for better performance
*
- * @param array &$object Object to add properties to (by reference)
- * @param array $properties Properties array from XML
- * @param array $propertyDefinitionMap Property definition map
+ * @param array &$object Object to add properties to (by reference)
+ * @param array $properties Properties array from XML
+ * @param array $propertyDefinitionMap Property definition map
+ *
* @return void
*/
private function flattenPropertiesBatch(array &$object, array $properties, array $propertyDefinitionMap): void
{
- $props = isset($properties[0]) ? $properties : [$properties];
+ if (isset($properties[0]) === true) {
+ $props = $properties;
+ } else {
+ $props = [$properties];
+ }
+
$processedProperties = [];
- // Debug: Log property flattening process
- $this->logger->debug('Flattening properties for object', [
- 'object_id' => $object['identifier'] ?? 'unknown',
- 'properties_count' => count($props),
- 'property_definition_map_size' => count($propertyDefinitionMap),
- 'sample_property_definitions' => array_slice($propertyDefinitionMap, 0, 5, true)
- ]);
+ // Debug: Log property flattening process.
+ $this->logger->debug(
+ 'Flattening properties for object',
+ [
+ 'object_id' => $object['identifier'] ?? 'unknown',
+ 'properties_count' => count($props),
+ 'property_definition_map_size' => count($propertyDefinitionMap),
+ 'sample_property_definitions' => array_slice($propertyDefinitionMap, 0, 5, true),
+ ]
+ );
foreach ($props as $propIndex => $prop) {
- if (!isset($prop['_attributes']['propertyDefinitionRef'])) {
- $this->logger->warning('Property missing propertyDefinitionRef', [
- 'object_id' => $object['identifier'] ?? 'unknown',
- 'property_index' => $propIndex,
- 'property_structure' => array_keys($prop ?? [])
- ]);
+ if (isset($prop['_attributes']['propertyDefinitionRef']) === false) {
+ $this->logger->warning(
+ 'Property missing propertyDefinitionRef',
+ [
+ 'object_id' => $object['identifier'] ?? 'unknown',
+ 'property_index' => $propIndex,
+ 'property_structure' => array_keys($prop ?? []),
+ ]
+ );
continue;
}
$defRef = $prop['_attributes']['propertyDefinitionRef'];
- $value = $prop['value']['_value'] ?? $prop['value'] ?? null;
-
- // Debug: Log property reference lookup
- if (!isset($propertyDefinitionMap[$defRef])) {
- $this->logger->warning('Property definition not found in map', [
- 'object_id' => $object['identifier'] ?? 'unknown',
- 'property_def_ref' => $defRef,
- 'available_refs' => array_keys($propertyDefinitionMap)
- ]);
+ $value = $prop['value']['_value'] ?? $prop['value'] ?? null;
+
+ // Debug: Log property reference lookup.
+ if (isset($propertyDefinitionMap[$defRef]) === false) {
+ $this->logger->warning(
+ 'Property definition not found in map',
+ [
+ 'object_id' => $object['identifier'] ?? 'unknown',
+ 'property_def_ref' => $defRef,
+ 'available_refs' => array_keys($propertyDefinitionMap),
+ ]
+ );
continue;
}
- if ($value !== null && isset($propertyDefinitionMap[$defRef])) {
- $propertyName = $propertyDefinitionMap[$defRef];
- $camelCaseName = $this->convertToCamelCase($propertyName);
+ if ($value !== null && isset($propertyDefinitionMap[$defRef]) === true) {
+ $propertyName = $propertyDefinitionMap[$defRef];
+ $camelCaseName = $this->convertToCamelCase(propertyName: $propertyName);
$object[$camelCaseName] = $value;
- // Store property mapping for reference
- if (!isset($object['_propertyMapping'])) {
+ // Store property mapping for reference.
+ if (isset($object['_propertyMapping']) === false) {
$object['_propertyMapping'] = [];
}
+
$object['_propertyMapping'][$camelCaseName] = $propertyName;
$processedProperties[] = [
- 'original' => $propertyName,
+ 'original' => $propertyName,
'camelCase' => $camelCaseName,
- 'value' => $value,
- 'def_ref' => $defRef
+ 'value' => $value,
+ 'def_ref' => $defRef,
];
- // Object ID property is now handled after property flattening is complete
-
- // Debug: Log GEMMA type properties specifically
+ // Object ID property is now handled after property flattening is complete.
+ // Debug: Log GEMMA type properties specifically.
if (stripos($propertyName, 'gemma') !== false || $defRef === 'propid-3') {
- $this->logger->info('GEMMA type property processed', [
- 'object_id' => $object['identifier'] ?? 'unknown',
- 'property_name' => $propertyName,
- 'camel_case_name' => $camelCaseName,
- 'value' => $value,
- 'def_ref' => $defRef
- ]);
+ $this->logger->info(
+ 'GEMMA type property processed',
+ [
+ 'object_id' => $object['identifier'] ?? 'unknown',
+ 'property_name' => $propertyName,
+ 'camel_case_name' => $camelCaseName,
+ 'value' => $value,
+ 'def_ref' => $defRef,
+ ]
+ );
}
} else {
- $this->logger->warning('Property value is null or mapping missing', [
- 'object_id' => $object['identifier'] ?? 'unknown',
- 'property_def_ref' => $defRef,
- 'value' => $value,
- 'mapping_exists' => isset($propertyDefinitionMap[$defRef])
- ]);
- }
- }
-
- // Debug: Log final property flattening results
- $this->logger->debug('Property flattening completed', [
- 'object_id' => $object['identifier'] ?? 'unknown',
- 'processed_count' => count($processedProperties),
- 'processed_properties' => $processedProperties,
- 'object_keys_after_flattening' => array_keys($object)
- ]);
- }
+ $this->logger->warning(
+ 'Property value is null or mapping missing',
+ [
+ 'object_id' => $object['identifier'] ?? 'unknown',
+ 'property_def_ref' => $defRef,
+ 'value' => $value,
+ 'mapping_exists' => isset($propertyDefinitionMap[$defRef]) === true,
+ ]
+ );
+ }//end if
+ }//end foreach
+
+ // Debug: Log final property flattening results.
+ $this->logger->debug(
+ 'Property flattening completed',
+ [
+ 'object_id' => $object['identifier'] ?? 'unknown',
+ 'processed_count' => count($processedProperties),
+ 'processed_properties' => $processedProperties,
+ 'object_keys_after_flattening' => array_keys($object),
+ ]
+ );
+ }//end flattenPropertiesBatch()
/**
* SPEED OPTIMIZATION: Build all lookups simultaneously for maximum performance
@@ -4415,39 +5068,42 @@ private function flattenPropertiesBatch(array &$object, array $properties, array
* Pre-builds all possible lookups in parallel to eliminate lookup building overhead
* during processing. Uses more memory but significantly faster processing.
*
- * @param array $xmlData Complete XML data
+ * @param array $xmlData Complete XML data
+ *
* @return array Array with all lookups: ['elements' => [...], 'relationships' => [...], etc.]
*/
private function buildAllLookupsSimultaneously(array $xmlData): array
{
$lookups = [
- 'elements' => [],
- 'relationships' => [],
- 'organizations' => [],
- 'views' => [],
- 'property_definitions' => []
+ 'elements' => [],
+ 'relationships' => [],
+ 'organizations' => [],
+ 'views' => [],
+ 'property_definitions' => [],
];
- // Pre-extract all section data simultaneously
+ // Pre-extract all section data simultaneously.
$sections = [
- 'elements' => 'element',
- 'relationships' => 'relationship',
- 'organizations' => 'organization',
- 'views' => 'view',
- 'property_definitions' => 'property_definition'
+ 'elements' => 'element',
+ 'relationships' => 'relationship',
+ 'organizations' => 'organization',
+ 'views' => 'view',
+ 'property_definitions' => 'property_definition',
];
foreach ($sections as $sectionName => $schemaType) {
- $sectionData = $this->findSectionData($xmlData, $sectionName);
- if (!empty($sectionData)) {
- $items = $this->findItemsSimplified($sectionData, $schemaType);
+ $sectionData = $this->findSectionData(xmlData: $xmlData, sectionName: $sectionName);
+ if (empty($sectionData) === false) {
+ $items = $this->findItemsSimplified(sectionData: $sectionData, sectionType: $schemaType);
foreach ($items as $item) {
- if (!is_array($item)) continue;
+ if (is_array($item) === false) {
+ continue;
+ }
- $identifier = $this->extractIdentifier($item, $schemaType);
- if ($identifier) {
- // Store raw item data for fast processing later
+ $identifier = $this->extractIdentifier(item: $item, sectionName: $schemaType);
+ if (empty($identifier) === false) {
+ // Store raw item data for fast processing later.
$lookups[$sectionName][$identifier] = $item;
}
}
@@ -4455,15 +5111,16 @@ private function buildAllLookupsSimultaneously(array $xmlData): array
}
return $lookups;
- }
+ }//end buildAllLookupsSimultaneously()
/**
* SPEED OPTIMIZATION: Bulk process all non-view sections with vectorized operations
*
- * @param array $xmlData XML data
- * @param string $modelIdentifier Model identifier
- * @param array $propertyDefinitionMap Property definition map
- * @param array $allLookups All pre-built lookups
+ * @param array $xmlData XML data
+ * @param string $modelIdentifier Model identifier
+ * @param array $propertyDefinitionMap Property definition map
+ * @param array $allLookups All pre-built lookups
+ *
* @return array Processed objects
*/
private function bulkProcessNonViewSections(
@@ -4475,44 +5132,50 @@ private function bulkProcessNonViewSections(
$objects = [];
$sections = [
- 'elements' => 'element',
- 'relationships' => 'relationship',
- 'organizations' => 'organization',
- 'property_definitions' => 'property_definition'
+ 'elements' => 'element',
+ 'relationships' => 'relationship',
+ 'organizations' => 'organization',
+ 'property_definitions' => 'property_definition',
];
foreach ($sections as $sectionName => $schemaType) {
- // Organizations are hierarchical folder trees — store as one tree object
+ // Organizations are hierarchical folder trees — store as one tree object.
if ($sectionName === 'organizations') {
- $orgData = $this->findSectionData($xmlData, 'organizations');
- if (!empty($orgData)) {
- $syntheticId = 'org-' . preg_replace('/^id-/', '', $modelIdentifier);
- $objects[] = [
- '@self' => [
- 'register' => $this->cachedConfig['registerId'] ?? throw new \RuntimeException("Register ID not found in cached configuration."),
- 'schema' => $this->cachedConfig['schemaIds']['organization'] ?? throw new \RuntimeException("Schema ID for 'organization' not found."),
- 'id' => $syntheticId,
- 'owner' => $this->cachedConfig['userId'],
+ $orgData = $this->findSectionData(xmlData: $xmlData, sectionName: 'organizations');
+ if (empty($orgData) === false) {
+ $syntheticId = 'org-'.preg_replace('/^id-/', '', $modelIdentifier);
+ $objects[] = [
+ '@self' => [
+ 'register' => $this->cachedConfig['registerId'] ?? throw new \RuntimeException("Register ID not found in cached configuration."),
+ 'schema' => $this->cachedConfig['schemaIds']['organization'] ?? throw new \RuntimeException("Schema ID for 'organization' not found."),
+ 'id' => $syntheticId,
+ 'owner' => $this->cachedConfig['userId'],
'organisation' => $this->getCurrentOrganisation(),
- 'published' => date('Y-m-d\TH:i:s\Z')
+ 'published' => date('Y-m-d\TH:i:s\Z'),
],
- 'identifier' => $syntheticId,
- 'section' => 'organization',
+ 'identifier' => $syntheticId,
+ 'section' => 'organization',
'model_identifier' => $modelIdentifier,
- 'name' => 'Organizations',
- 'xml' => $orgData
+ 'name' => 'Organizations',
+ 'xml' => $orgData,
];
}
+
continue;
- }
+ }//end if
- if (empty($allLookups[$sectionName])) continue;
+ if (empty($allLookups[$sectionName]) === true) {
+ continue;
+ }
- $this->logger->debug("SPEED: Bulk processing {$sectionName}", [
- 'item_count' => count($allLookups[$sectionName])
- ]);
+ $this->logger->debug(
+ "SPEED: Bulk processing {$sectionName}",
+ [
+ 'item_count' => count($allLookups[$sectionName]),
+ ]
+ );
- // SPEED OPTIMIZATION: Process all items in this section as a batch
+ // SPEED OPTIMIZATION: Process all items in this section as a batch.
$sectionObjects = $this->bulkTransformSection(
$allLookups[$sectionName],
$schemaType,
@@ -4521,18 +5184,19 @@ private function bulkProcessNonViewSections(
);
$objects = array_merge($objects, $sectionObjects);
- }
+ }//end foreach
return $objects;
- }
+ }//end bulkProcessNonViewSections()
/**
* SPEED OPTIMIZATION: Bulk transform a section with vectorized operations
*
- * @param array $sectionItems Pre-loaded section items by identifier
- * @param string $schemaType Schema type
- * @param string $modelIdentifier Model identifier
- * @param array $propertyDefinitionMap Property definition map
+ * @param array $sectionItems Pre-loaded section items by identifier
+ * @param string $schemaType Schema type
+ * @param string $modelIdentifier Model identifier
+ * @param array $propertyDefinitionMap Property definition map
+ *
* @return array Transformed objects
*/
private function bulkTransformSection(
@@ -4544,91 +5208,100 @@ private function bulkTransformSection(
$objects = [];
foreach ($sectionItems as $identifier => $item) {
- // SPEED OPTIMIZATION: Direct object creation without intermediate steps
- $essentialXmlData = $this->extractEssentialXmlData($item, [], $schemaType);
+ // SPEED OPTIMIZATION: Direct object creation without intermediate steps.
+ $essentialXmlData = $this->extractEssentialXmlData(item: $item, elementsLookup: [], schemaType: $schemaType);
$object = [
- '@self' => [
- 'register' => $this->cachedConfig['registerId'] ?? throw new \RuntimeException("Register ID not found in cached configuration. Please ensure AMEF configuration is properly initialized."),
- 'schema' => $this->cachedConfig['schemaIds'][$schemaType] ?? throw new \RuntimeException("Schema ID for '{$schemaType}' not found in cached configuration. Please ensure AMEF configuration is properly initialized."),
- 'id' => $identifier,
- 'owner' => $this->cachedConfig['userId'],
+ '@self' => [
+ 'register' => $this->cachedConfig['registerId'] ?? throw new \RuntimeException("Register ID not found in cached configuration. Please ensure AMEF configuration is properly initialized."),
+ 'schema' => $this->cachedConfig['schemaIds'][$schemaType] ?? throw new \RuntimeException("Schema ID for '{$schemaType}' not found in cached configuration. Please ensure AMEF configuration is properly initialized."),
+ 'id' => $identifier,
+ 'owner' => $this->cachedConfig['userId'],
'organisation' => $this->getCurrentOrganisation(),
- 'published' => date('Y-m-d\TH:i:s\Z')
+ 'published' => date('Y-m-d\TH:i:s\Z'),
],
- 'identifier' => $identifier,
- 'section' => $schemaType,
+ 'identifier' => $identifier,
+ 'section' => $schemaType,
'model_identifier' => $modelIdentifier,
- 'xml' => $essentialXmlData
+ 'xml' => $essentialXmlData,
];
- // Fast extract name and summary
- if (isset($item['name'])) {
- $object['name'] = is_array($item['name']) && isset($item['name']['_value'])
- ? $item['name']['_value']
- : (is_string($item['name']) ? $item['name'] : '');
+ // Fast extract name and summary.
+ if (isset($item['name']) === true) {
+ if (is_array($item['name']) === true && isset($item['name']['_value']) === true) {
+ $object['name'] = $item['name']['_value'];
+ } else {
+ $object['name'] = (is_string($item['name']) === true ? $item['name'] : '');
+ }
}
- if (isset($item['documentation'])) {
- $object['summary'] = is_array($item['documentation']) && isset($item['documentation']['_value'])
- ? $item['documentation']['_value']
- : (is_string($item['documentation']) ? $item['documentation'] : '');
+ if (isset($item['documentation']) === true) {
+ if (is_array($item['documentation']) === true && isset($item['documentation']['_value']) === true) {
+ $object['summary'] = $item['documentation']['_value'];
+ } else {
+ $object['summary'] = (is_string($item['documentation']) === true ? $item['documentation'] : '');
+ }
}
- // Extract type from xsi:type attribute (e.g., "Capability", "ApplicationComponent")
- if (isset($item['_xsi__type'])) {
+ // Extract type from xsi:type attribute (e.g., "Capability", "ApplicationComponent").
+ if (isset($item['_xsi__type']) === true) {
$object['type'] = $item['_xsi__type'];
- } elseif (isset($item['_attributes']['xsi:type'])) {
+ } else if (isset($item['_attributes']['xsi:type']) === true) {
$object['type'] = $item['_attributes']['xsi:type'];
}
- // For relationships, extract source and target from attributes
+ // For relationships, extract source and target from attributes.
if ($schemaType === 'relationship') {
- if (isset($item['_source'])) {
+ if (isset($item['_source']) === true) {
$object['source'] = $item['_source'];
- } elseif (isset($item['_attributes']['source'])) {
+ } else if (isset($item['_attributes']['source']) === true) {
$object['source'] = $item['_attributes']['source'];
}
- if (isset($item['_target'])) {
+ if (isset($item['_target']) === true) {
$object['target'] = $item['_target'];
- } elseif (isset($item['_attributes']['target'])) {
+ } else if (isset($item['_attributes']['target']) === true) {
$object['target'] = $item['_attributes']['target'];
}
}
- // Fast flatten properties
- if (isset($item['properties']['property']) && !empty($propertyDefinitionMap)) {
- $this->flattenPropertiesBatch($object, $item['properties']['property'], $propertyDefinitionMap);
+ // Fast flatten properties.
+ if (isset($item['properties']['property']) === true && empty($propertyDefinitionMap) === false) {
+ $this->flattenPropertiesBatch(object: $object, properties: $item['properties']['property'], propertyDefinitionMap: $propertyDefinitionMap);
- // Fast ID/slug update
- if (isset($object['objectId'])) {
- $object['@self']['id'] = $object['objectId'];
+ // Fast ID/slug update.
+ if (isset($object['objectId']) === true) {
+ $object['@self']['id'] = $object['objectId'];
$object['@self']['slug'] = $identifier;
} else {
- $object['@self']['slug'] = str_starts_with($identifier, 'id-')
- ? substr($identifier, 3)
- : $identifier;
+ if (str_starts_with($identifier, 'id-') === true) {
+ $object['@self']['slug'] = substr($identifier, 3);
+ } else {
+ $object['@self']['slug'] = $identifier;
+ }
}
} else {
- $object['@self']['slug'] = str_starts_with($identifier, 'id-')
- ? substr($identifier, 3)
- : $identifier;
- }
+ if (str_starts_with($identifier, 'id-') === true) {
+ $object['@self']['slug'] = substr($identifier, 3);
+ } else {
+ $object['@self']['slug'] = $identifier;
+ }
+ }//end if
$objects[] = $object;
- }
+ }//end foreach
return $objects;
- }
+ }//end bulkTransformSection()
/**
* SPEED OPTIMIZATION: Process views with maximum speed optimizations
*
- * @param array $xmlData XML data
- * @param string $modelIdentifier Model identifier
- * @param array $propertyDefinitionMap Property definition map
- * @param array $elementsLookup Elements lookup for splicing
+ * @param array $xmlData XML data
+ * @param string $modelIdentifier Model identifier
+ * @param array $propertyDefinitionMap Property definition map
+ * @param array $elementsLookup Elements lookup for splicing
+ *
* @return array Processed view objects
*/
private function processViewsMaximumSpeed(
@@ -4637,39 +5310,46 @@ private function processViewsMaximumSpeed(
array $propertyDefinitionMap,
array $elementsLookup
): array {
- $viewsData = $this->findSectionData($xmlData, 'views');
- if (empty($viewsData)) {
+ $viewsData = $this->findSectionData(xmlData: $xmlData, sectionName: 'views');
+ if (empty($viewsData) === true) {
return [];
}
- $this->logger->info('SPEED MODE: Processing views with maximum optimizations', [
- 'elements_available' => count($elementsLookup)
- ]);
+ $this->logger->info(
+ 'SPEED MODE: Processing views with maximum optimizations',
+ [
+ 'elements_available' => count($elementsLookup),
+ ]
+ );
- // SPEED OPTIMIZATION: Pre-extract all referenced elements
- $items = $this->findItemsSimplified($viewsData, 'view');
- $referencedElements = $this->extractReferencedElements($items);
+ // SPEED OPTIMIZATION: Pre-extract all referenced elements.
+ $items = $this->findItemsSimplified(sectionData: $viewsData, sectionType: 'view');
+ $referencedElements = $this->extractReferencedElements(viewItems: $items);
- // SPEED OPTIMIZATION: Build super-fast lookup with array_intersect_key
+ // SPEED OPTIMIZATION: Build super-fast lookup with array_intersect_key.
$filteredElementsLookup = array_intersect_key($elementsLookup, array_flip($referencedElements));
- $this->logger->debug('SPEED: Optimized element references', [
- 'total_elements' => count($elementsLookup),
- 'referenced_elements' => count($filteredElementsLookup),
- 'memory_savings_percent' => round((1 - count($filteredElementsLookup) / max(count($elementsLookup), 1)) * 100, 1)
- ]);
+ $this->logger->debug(
+ 'SPEED: Optimized element references',
+ [
+ 'total_elements' => count($elementsLookup),
+ 'referenced_elements' => count($filteredElementsLookup),
+ 'memory_savings_percent' => round((1 - count($filteredElementsLookup) / max(count($elementsLookup), 1)) * 100, 1),
+ ]
+ );
- // SPEED OPTIMIZATION: Process with bulk operations
- return $this->bulkTransformViews($items, $modelIdentifier, $propertyDefinitionMap, $filteredElementsLookup);
- }
+ // SPEED OPTIMIZATION: Process with bulk operations.
+ return $this->bulkTransformViews(viewItems: $items, modelIdentifier: $modelIdentifier, propertyDefinitionMap: $propertyDefinitionMap, elementsLookup: $filteredElementsLookup);
+ }//end processViewsMaximumSpeed()
/**
* SPEED OPTIMIZATION: Bulk transform views with vectorized element splicing
*
- * @param array $viewItems View items to process
- * @param string $modelIdentifier Model identifier
- * @param array $propertyDefinitionMap Property definition map
- * @param array $elementsLookup Filtered elements lookup
+ * @param array $viewItems View items to process
+ * @param string $modelIdentifier Model identifier
+ * @param array $propertyDefinitionMap Property definition map
+ * @param array $elementsLookup Filtered elements lookup
+ *
* @return array Processed view objects
*/
private function bulkTransformViews(
@@ -4681,73 +5361,82 @@ private function bulkTransformViews(
$objects = [];
foreach ($viewItems as $item) {
- if (!is_array($item)) continue;
+ if (is_array($item) === false) {
+ continue;
+ }
- $identifier = $this->extractIdentifier($item, 'view');
- if (!$identifier) continue;
+ $identifier = $this->extractIdentifier(item: $item, sectionName: 'view');
+ if ($identifier === null) {
+ continue;
+ }
- // SPEED OPTIMIZATION: Direct processing with minimal overhead
- $essentialXmlData = $this->extractEssentialXmlData($item, $elementsLookup, 'view');
+ // SPEED OPTIMIZATION: Direct processing with minimal overhead.
+ $essentialXmlData = $this->extractEssentialXmlData(item: $item, elementsLookup: $elementsLookup, schemaType: 'view');
$object = [
- '@self' => [
- 'register' => $this->cachedConfig['registerId'] ?? throw new \RuntimeException("Register ID not found in cached configuration. Please ensure AMEF configuration is properly initialized."),
- 'schema' => $this->getSchemaIdForSection('view'),
- 'id' => $identifier,
- 'owner' => $this->cachedConfig['userId'],
+ '@self' => [
+ 'register' => $this->cachedConfig['registerId'] ?? throw new \RuntimeException("Register ID not found in cached configuration. Please ensure AMEF configuration is properly initialized."),
+ 'schema' => $this->getSchemaIdForSection(section: 'view'),
+ 'id' => $identifier,
+ 'owner' => $this->cachedConfig['userId'],
'organisation' => $this->getCurrentOrganisation(),
- 'published' => date('Y-m-d\TH:i:s\Z')
+ 'published' => date('Y-m-d\TH:i:s\Z'),
],
- 'identifier' => $identifier,
- 'section' => 'view',
+ 'identifier' => $identifier,
+ 'section' => 'view',
'model_identifier' => $modelIdentifier,
- 'xml' => $essentialXmlData
+ 'xml' => $essentialXmlData,
];
- // Fast name/summary extraction
- if (isset($item['name'])) {
- $object['name'] = is_array($item['name']) && isset($item['name']['_value'])
- ? $item['name']['_value']
- : (is_string($item['name']) ? $item['name'] : '');
+ // Fast name/summary extraction.
+ if (isset($item['name']) === true) {
+ if (is_array($item['name']) === true && isset($item['name']['_value']) === true) {
+ $object['name'] = $item['name']['_value'];
+ } else {
+ $object['name'] = (is_string($item['name']) === true ? $item['name'] : '');
+ }
}
- if (isset($item['documentation'])) {
- $object['summary'] = is_array($item['documentation']) && isset($item['documentation']['_value'])
- ? $item['documentation']['_value']
- : (is_string($item['documentation']) ? $item['documentation'] : '');
+ if (isset($item['documentation']) === true) {
+ if (is_array($item['documentation']) === true && isset($item['documentation']['_value']) === true) {
+ $object['summary'] = $item['documentation']['_value'];
+ } else {
+ $object['summary'] = (is_string($item['documentation']) === true ? $item['documentation'] : '');
+ }
}
- // Extract type from xsi:type attribute
- if (isset($item['_xsi__type'])) {
+ // Extract type from xsi:type attribute.
+ if (isset($item['_xsi__type']) === true) {
$object['type'] = $item['_xsi__type'];
- } elseif (isset($item['_attributes']['xsi:type'])) {
+ } else if (isset($item['_attributes']['xsi:type']) === true) {
$object['type'] = $item['_attributes']['xsi:type'];
}
- // Fast properties flattening
- if (isset($item['properties']['property']) && !empty($propertyDefinitionMap)) {
- $this->flattenPropertiesBatch($object, $item['properties']['property'], $propertyDefinitionMap);
+ // Fast properties flattening.
+ if (isset($item['properties']['property']) === true && empty($propertyDefinitionMap) === false) {
+ $this->flattenPropertiesBatch(object: $object, properties: $item['properties']['property'], propertyDefinitionMap: $propertyDefinitionMap);
- // Keep @self.id as the full ArchiMate identifier (set above)
+ // Keep @self.id as the full ArchiMate identifier (set above).
// so stored IDs match GEMMA Online URLs (id-e0f57689-...).
$object['@self']['slug'] = $identifier;
} else {
$object['@self']['slug'] = $identifier;
}
- // SPEED OPTIMIZATION: Direct copy without checks (we know it exists)
- if (isset($object['xml']['viewNodes'])) {
+ // SPEED OPTIMIZATION: Direct copy without checks (we know it exists).
+ if (isset($object['xml']['viewNodes']) === true) {
$object['viewNodes'] = $object['xml']['viewNodes'];
}
- if (isset($object['xml']['viewRelationships'])) {
+
+ if (isset($object['xml']['viewRelationships']) === true) {
$object['viewRelationships'] = $object['xml']['viewRelationships'];
}
$objects[] = $object;
- }
+ }//end foreach
return $objects;
- }
+ }//end bulkTransformViews()
/**
* Create intelligent batches based on object size to prevent MySQL packet size issues
@@ -4759,103 +5448,115 @@ private function bulkTransformViews(
* This functionality should be available for all bulk operations, not just ArchiMate imports.
* OpenRegister's saveObjects() method should handle this automatically based on object sizes.
*
- * @param array $objects Array of objects to batch
+ * @param array $objects Array of objects to batch
+ *
* @return array Array of batches, each containing objects that fit within size limits
*/
private function createIntelligentBatches(array $objects): array
{
$maxBatchSizeBytes = self::PERFORMANCE_OPTIMIZATIONS['max_batch_size_bytes'];
- $minBatchSize = self::PERFORMANCE_OPTIMIZATIONS['min_batch_size'];
- $sampleSize = self::PERFORMANCE_OPTIMIZATIONS['size_estimation_sample'];
+ $minBatchSize = self::PERFORMANCE_OPTIMIZATIONS['min_batch_size'];
+ $sampleSize = self::PERFORMANCE_OPTIMIZATIONS['size_estimation_sample'];
- if (empty($objects)) {
+ if (empty($objects) === true) {
return [];
}
- // Estimate average object size by sampling
- $avgObjectSize = $this->estimateAverageObjectSize($objects, $sampleSize);
+ // Estimate average object size by sampling.
+ $avgObjectSize = $this->estimateAverageObjectSize(objects: $objects, sampleSize: $sampleSize);
- // Calculate optimal batch size based on object size
+ // Calculate optimal batch size based on object size.
$optimalBatchSize = max($minBatchSize, intval($maxBatchSizeBytes / $avgObjectSize));
- $this->logger->info('Intelligent batch sizing analysis', [
- 'total_objects' => count($objects),
- 'estimated_avg_object_size_bytes' => $avgObjectSize,
- 'max_batch_size_bytes' => $maxBatchSizeBytes,
- 'calculated_optimal_batch_size' => $optimalBatchSize,
- 'min_batch_size_enforced' => $minBatchSize
- ]);
-
- // Create batches with size awareness
- $batches = [];
- $currentBatch = [];
+ $this->logger->info(
+ 'Intelligent batch sizing analysis',
+ [
+ 'total_objects' => count($objects),
+ 'estimated_avg_object_size_bytes' => $avgObjectSize,
+ 'max_batch_size_bytes' => $maxBatchSizeBytes,
+ 'calculated_optimal_batch_size' => $optimalBatchSize,
+ 'min_batch_size_enforced' => $minBatchSize,
+ ]
+ );
+
+ // Create batches with size awareness.
+ $batches = [];
+ $currentBatch = [];
$currentBatchSize = 0;
foreach ($objects as $object) {
- $objectSize = $this->estimateObjectSize($object);
+ $objectSize = $this->estimateObjectSize(object: $object);
- // Check if adding this object would exceed the batch size limit
- if (!empty($currentBatch) && ($currentBatchSize + $objectSize) > $maxBatchSizeBytes) {
- // Current batch is full, save it and start a new one
- $batches[] = $currentBatch;
- $currentBatch = [$object];
+ // Check if adding this object would exceed the batch size limit.
+ if (empty($currentBatch) === false && ($currentBatchSize + $objectSize) > $maxBatchSizeBytes) {
+ // Current batch is full, save it and start a new one.
+ $batches[] = $currentBatch;
+ $currentBatch = [$object];
$currentBatchSize = $objectSize;
} else {
- // Add object to current batch
- $currentBatch[] = $object;
+ // Add object to current batch.
+ $currentBatch[] = $object;
$currentBatchSize += $objectSize;
}
- // Safety check: if a single object is larger than max batch size,
- // create a batch with just that object
+ // Safety check: if a single object is larger than max batch size,.
+ // create a batch with just that object.
if (count($currentBatch) === 1 && $objectSize > $maxBatchSizeBytes) {
- $this->logger->warning('Very large object detected, creating single-object batch', [
- 'object_id' => $object['@self']['id'] ?? 'unknown',
- 'object_size_bytes' => $objectSize,
- 'max_batch_size_bytes' => $maxBatchSizeBytes
- ]);
- $batches[] = $currentBatch;
- $currentBatch = [];
+ $this->logger->warning(
+ 'Very large object detected, creating single-object batch',
+ [
+ 'object_id' => $object['@self']['id'] ?? 'unknown',
+ 'object_size_bytes' => $objectSize,
+ 'max_batch_size_bytes' => $maxBatchSizeBytes,
+ ]
+ );
+ $batches[] = $currentBatch;
+ $currentBatch = [];
$currentBatchSize = 0;
}
- }
+ }//end foreach
- // Add the last batch if it has objects
- if (!empty($currentBatch)) {
+ // Add the last batch if it has objects.
+ if (empty($currentBatch) === false) {
$batches[] = $currentBatch;
}
- $this->logger->info('Intelligent batching completed', [
- 'total_objects' => count($objects),
- 'total_batches_created' => count($batches),
- 'batch_sizes' => array_map('count', $batches),
- 'estimated_batch_sizes_bytes' => array_map(fn($batch) => array_sum(array_map([$this, 'estimateObjectSize'], $batch)), $batches)
- ]);
+ $this->logger->info(
+ 'Intelligent batching completed',
+ [
+ 'total_objects' => count($objects),
+ 'total_batches_created' => count($batches),
+ 'batch_sizes' => array_map('count', $batches),
+ 'estimated_batch_sizes_bytes' => array_map(fn($batch) => array_sum(array_map([$this, 'estimateObjectSize'], $batch)), $batches),
+ ]
+ );
return $batches;
- }
+ }//end createIntelligentBatches()
/**
* Estimate the average size of objects by sampling
*
- * @param array $objects Array of objects to sample
- * @param int $sampleSize Number of objects to sample for size estimation
+ * @param array $objects Array of objects to sample
+ * @param int $sampleSize Number of objects to sample for size estimation
+ *
* @return int Estimated average object size in bytes
*/
private function estimateAverageObjectSize(array $objects, int $sampleSize): int
{
$totalObjects = count($objects);
if ($totalObjects === 0) {
- return 1000; // Default fallback size
+ return 1000;
+ // Default fallback size.
}
- // Sample evenly distributed objects
+ // Sample evenly distributed objects.
$sampleIndices = [];
if ($totalObjects <= $sampleSize) {
- // Use all objects if we have fewer than sample size
+ // Use all objects if we have fewer than sample size.
$sampleIndices = range(0, $totalObjects - 1);
} else {
- // Sample evenly across the array
+ // Sample evenly across the array.
$step = max(1, intval($totalObjects / $sampleSize));
for ($i = 0; $i < $totalObjects; $i += $step) {
$sampleIndices[] = $i;
@@ -4865,73 +5566,82 @@ private function estimateAverageObjectSize(array $objects, int $sampleSize): int
}
}
- // Calculate sizes of sampled objects
+ // Calculate sizes of sampled objects.
$totalSampleSize = 0;
foreach ($sampleIndices as $index) {
- $totalSampleSize += $this->estimateObjectSize($objects[$index]);
+ $totalSampleSize += $this->estimateObjectSize(object: $objects[$index]);
}
$averageSize = intval($totalSampleSize / count($sampleIndices));
- $this->logger->debug('Object size estimation completed', [
- 'total_objects' => $totalObjects,
- 'sampled_objects' => count($sampleIndices),
- 'total_sample_size_bytes' => $totalSampleSize,
- 'estimated_average_size_bytes' => $averageSize
- ]);
+ $this->logger->debug(
+ 'Object size estimation completed',
+ [
+ 'total_objects' => $totalObjects,
+ 'sampled_objects' => count($sampleIndices),
+ 'total_sample_size_bytes' => $totalSampleSize,
+ 'estimated_average_size_bytes' => $averageSize,
+ ]
+ );
- return max(1000, $averageSize); // Minimum 1KB per object
- }
+ return max(1000, $averageSize);
+ // Minimum 1KB per object.
+ }//end estimateAverageObjectSize()
/**
* Estimate the serialized size of an object for batching purposes
*
- * @param array $object The object to estimate size for
+ * @param array $object The object to estimate size for
+ *
* @return int Estimated size in bytes
*/
private function estimateObjectSize(array $object): int
{
- // Quick estimation based on JSON serialization
- // This includes overhead for SQL parameters and structure
+ // Quick estimation based on JSON serialization.
+ // This includes overhead for SQL parameters and structure.
$jsonSize = strlen(json_encode($object));
- // Add overhead for SQL INSERT statement structure
- // Each object becomes multiple parameters in a bulk INSERT
- $sqlOverhead = 500; // Estimated overhead per object in SQL
-
+ // Add overhead for SQL INSERT statement structure.
+ // Each object becomes multiple parameters in a bulk INSERT.
+ $sqlOverhead = 500;
+ // Estimated overhead per object in SQL.
return $jsonSize + $sqlOverhead;
- }
+ }//end estimateObjectSize()
/**
* Calculate detailed object statistics for import operations
*
- * @param array $normalizedData Normalized ArchiMate data
- * @param array $savedObjects Objects that were saved to database
+ * @param array $normalizedData Normalized ArchiMate data
+ * @param array $savedObjects Objects that were saved to database
+ *
* @return array Comprehensive statistics
*/
private function calculateObjectStatistics(array $normalizedData, array $savedObjects): array
{
- // Initialize statistics structure
+ // Initialize statistics structure.
$statistics = [
- 'elements' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []],
- 'organizations' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []],
- 'relationships' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []],
- 'views' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []],
- 'property_definitions' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []]
+ 'elements' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []],
+ 'organizations' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []],
+ 'relationships' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []],
+ 'views' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []],
+ 'property_definitions' => ['created' => 0, 'updated' => 0, 'unchanged' => 0, 'errors' => []],
];
- // If we have access to the actual save results from ObjectService, use those
+ // If we have access to the actual save results from ObjectService, use those.
if ($this->lastSaveResult !== null) {
$saveResult = $this->lastSaveResult;
// DEBUG: Log what we got from ObjectService.
- $this->logger->info('[ArchiMate] Using lastSaveResult for statistics', [
- 'saved_count' => count($saveResult['saved'] ?? []),
- 'updated_count' => count($saveResult['updated'] ?? []),
- 'unchanged_count' => count($saveResult['unchanged'] ?? []),
- 'invalid_count' => count($saveResult['invalid'] ?? []),
- 'keys_present' => array_keys($saveResult)
- ]);
+ $this->logger->info(
+ '[ArchiMate] Using lastSaveResult for statistics',
+ [
+ 'saved_count' => count($saveResult['saved'] ?? []),
+ 'updated_count' => count($saveResult['updated'] ?? []),
+ 'unchanged_count' => count($saveResult['unchanged'] ?? []),
+ 'invalid_count' => count($saveResult['invalid'] ?? []),
+ 'keys_present' => array_keys($saveResult),
+ ]
+ );
// Count objects by section type from the actual processed objects.
$allProcessedObjects = array_merge(
@@ -4943,15 +5653,15 @@ private function calculateObjectStatistics(array $normalizedData, array $savedOb
);
foreach ($allProcessedObjects as $object) {
- // Convert ObjectEntity to array if needed
- if (is_object($object) && method_exists($object, 'jsonSerialize')) {
+ // Convert ObjectEntity to array if needed.
+ if (is_object($object) === true && method_exists($object, 'jsonSerialize') === true) {
$object = $object->jsonSerialize();
}
$sectionType = $object['section'] ?? null;
- // Map section types (singular or plural) to statistics keys
- $sectionKey = match($sectionType) {
+ // Map section types (singular or plural) to statistics keys.
+ $sectionKey = match ($sectionType) {
'elements', 'element', 'model' => 'elements',
'relationships', 'relationship' => 'relationships',
'organizations', 'organization' => 'organizations',
@@ -4960,229 +5670,265 @@ private function calculateObjectStatistics(array $normalizedData, array $savedOb
default => null
};
- // Fallback: use @self.schema to determine section
- if ($sectionKey === null && $this->cachedConfig !== null && isset($this->cachedConfig['schemaIds'])) {
+ // Fallback: use @self.schema to determine section.
+ if ($sectionKey === null && $this->cachedConfig !== null && isset($this->cachedConfig['schemaIds']) === true) {
$objSchemaId = $object['@self']['schema'] ?? null;
if ($objSchemaId !== null) {
$singularToPlural = [
- 'element' => 'elements', 'relationship' => 'relationships',
- 'organization' => 'organizations', 'view' => 'views',
- 'property_definition' => 'property_definitions', 'model' => 'elements',
+ 'element' => 'elements',
+ 'relationship' => 'relationships',
+ 'organization' => 'organizations',
+ 'view' => 'views',
+ 'property_definition' => 'property_definitions',
+ 'model' => 'elements',
];
foreach ($this->cachedConfig['schemaIds'] as $type => $schemaId) {
- if ((int)$schemaId === (int)$objSchemaId) {
+ if ((int) $schemaId === (int) $objSchemaId) {
$sectionKey = $singularToPlural[$type] ?? 'elements';
break;
}
}
}
+
$sectionKey = $sectionKey ?? 'elements';
- } elseif ($sectionKey === null) {
+ } else if ($sectionKey === null) {
$sectionKey = 'elements';
- }
+ }//end if
- if (!isset($statistics[$sectionKey])) {
- continue; // Skip unknown section types
+ if (isset($statistics[$sectionKey]) === false) {
+ continue;
+ // Skip unknown section types.
}
// Determine if this object was created, updated, or had errors.
$objectId = $object['@self']['id'] ?? $object['identifier'] ?? null;
// Check if this object is in the saved (created) list.
- $wasCreated = !empty(array_filter($saveResult['saved'] ?? [],
- fn($saved) => ($saved->getUuid() === $objectId)));
+ $wasCreated = empty(
+ array_filter(
+ $saveResult['saved'] ?? [],
+ fn($saved) => ($saved->getUuid() === $objectId)
+ )
+ ) === false;
// Check if this object is in the updated list.
- $wasUpdated = !empty(array_filter($saveResult['updated'] ?? [],
- fn($updated) => ($updated->getUuid() === $objectId)));
+ $wasUpdated = empty(
+ array_filter(
+ $saveResult['updated'] ?? [],
+ fn($updated) => ($updated->getUuid() === $objectId)
+ )
+ ) === false;
// Check if this object was unchanged (no changes).
$unchangedObjects = $saveResult['unchanged'] ?? [];
- $wasSkipped = !empty(array_filter($unchangedObjects,
- fn($unchanged) => ($unchanged->getUuid() === $objectId)));
+ $wasSkipped = empty(
+ array_filter(
+ $unchangedObjects,
+ fn($unchanged) => ($unchanged->getUuid() === $objectId)
+ )
+ ) === false;
// Check if this object had validation errors.
- $hasErrors = !empty(array_filter($saveResult['invalid'] ?? [],
- fn($invalid) => (($invalid['object']['@self']['id'] ?? null) === $objectId)));
-
- if ($wasCreated) {
+ $hasErrors = empty(
+ array_filter(
+ $saveResult['invalid'] ?? [],
+ fn($invalid) => (($invalid['object']['@self']['id'] ?? null) === $objectId)
+ )
+ ) === false;
+
+ if (empty($wasCreated) === false) {
$statistics[$sectionKey]['created']++;
- } elseif ($wasUpdated) {
+ } else if (empty($wasUpdated) === false) {
$statistics[$sectionKey]['updated']++;
- } elseif ($wasSkipped) {
+ } else if (empty($wasSkipped) === false) {
$statistics[$sectionKey]['unchanged']++;
- } elseif ($hasErrors) {
+ } else if (empty($hasErrors) === false) {
// Add to errors array for this section.
- $errorInfo = array_filter($saveResult['invalid'] ?? [],
- fn($invalid) => (($invalid['object']['@self']['id'] ?? null) === $objectId));
+ $errorInfo = array_filter(
+ $saveResult['invalid'] ?? [],
+ fn($invalid) => (($invalid['object']['@self']['id'] ?? null) === $objectId)
+ );
- if (!empty($errorInfo)) {
+ if (empty($errorInfo) === false) {
$statistics[$sectionKey]['errors'][] = array_values($errorInfo)[0]['error'] ?? 'Unknown validation error';
}
} else {
// This shouldn't happen, but leave as fallback.
$statistics[$sectionKey]['unchanged']++;
}
- }
+ }//end foreach
} else {
- // Fallback to old method if no save result is available
+ // Fallback to old method if no save result is available.
$sections = ['elements', 'relationships', 'organizations', 'views', 'property_definitions'];
foreach ($sections as $section) {
- if (isset($normalizedData[$section])) {
+ if (isset($normalizedData[$section]) === true) {
$count = count($normalizedData[$section]);
- // Assume all objects were created (legacy behavior)
+ // Assume all objects were created (legacy behavior).
$statistics[$section]['created'] = $count;
}
}
- }
+ }//end if
// Calculate summary totals from actual statistics.
$summary = [
- 'total_objects_created' => 0,
- 'total_objects_updated' => 0,
- 'total_objects_deleted' => 0,
+ 'total_objects_created' => 0,
+ 'total_objects_updated' => 0,
+ 'total_objects_deleted' => 0,
'total_objects_unchanged' => 0,
- 'total_errors' => 0
+ 'total_errors' => 0,
];
foreach ($statistics as $section => $sectionStats) {
- if ($section !== 'summary') { // Skip summary section itself.
- $summary['total_objects_created'] += $sectionStats['created'];
- $summary['total_objects_updated'] += $sectionStats['updated'];
+ if ($section !== 'summary') {
+ // Skip summary section itself.
+ $summary['total_objects_created'] += $sectionStats['created'];
+ $summary['total_objects_updated'] += $sectionStats['updated'];
$summary['total_objects_unchanged'] += $sectionStats['unchanged'];
- $summary['total_errors'] += count($sectionStats['errors']);
+ $summary['total_errors'] += count($sectionStats['errors']);
}
}
$statistics['summary'] = $summary;
// DEBUG: Log the summary statistics to help diagnose the issue.
- $this->logger->info('[ArchiMate] Statistics summary calculated', [
- 'summary' => $summary,
- 'section_counts' => array_map(fn($s) => [
- 'created' => $s['created'] ?? 0,
- 'updated' => $s['updated'] ?? 0,
- 'unchanged' => $s['unchanged'] ?? 0,
- 'errors' => count($s['errors'] ?? [])
- ], array_filter($statistics, fn($k) => $k !== 'summary', ARRAY_FILTER_USE_KEY))
- ]);
+ $this->logger->info(
+ '[ArchiMate] Statistics summary calculated',
+ [
+ 'summary' => $summary,
+ 'section_counts' => array_map(
+ fn($s) => [
+ 'created' => $s['created'] ?? 0,
+ 'updated' => $s['updated'] ?? 0,
+ 'unchanged' => $s['unchanged'] ?? 0,
+ 'errors' => count($s['errors'] ?? []),
+ ],
+ array_filter($statistics, fn($k) => $k !== 'summary', ARRAY_FILTER_USE_KEY)
+ ),
+ ]
+ );
return $statistics;
- }
+ }//end calculateObjectStatistics()
/**
* Extract detailed error information from import statistics for frontend display
*
- * @param array $statistics Import statistics containing section-wise error data
+ * @param array $statistics Import statistics containing section-wise error data
+ *
* @return array Formatted error information for frontend consumption
*/
private function extractDetailedErrors(array $statistics): array
{
$detailedErrors = [
'total_count' => 0,
- 'by_section' => [],
- 'summary' => []
+ 'by_section' => [],
+ 'summary' => [],
];
$sections = ['elements', 'relationships', 'organizations', 'views', 'property_definitions'];
foreach ($sections as $section) {
- if (isset($statistics[$section]['errors']) && !empty($statistics[$section]['errors'])) {
- $sectionErrors = $statistics[$section]['errors'];
+ if (isset($statistics[$section]['errors']) === true && empty($statistics[$section]['errors']) === false) {
+ $sectionErrors = $statistics[$section]['errors'];
$sectionErrorCount = count($sectionErrors);
$detailedErrors['total_count'] += $sectionErrorCount;
- // Group errors by type/message for better presentation
+ // Group errors by type/message for better presentation.
$errorGroups = [];
foreach ($sectionErrors as $error) {
- $errorMessage = is_string($error) ? $error : ($error['message'] ?? 'Unknown error');
- $errorType = $this->categorizeError($errorMessage);
+ if (is_string($error) === true) {
+ $errorMessage = $error;
+ } else {
+ $errorMessage = ($error['message'] ?? 'Unknown error');
+ }
- if (!isset($errorGroups[$errorType])) {
+ $errorType = $this->categorizeError(errorMessage: $errorMessage);
+
+ if (isset($errorGroups[$errorType]) === false) {
$errorGroups[$errorType] = [
- 'type' => $errorType,
- 'message' => $errorMessage,
- 'count' => 0,
- 'examples' => []
+ 'type' => $errorType,
+ 'message' => $errorMessage,
+ 'count' => 0,
+ 'examples' => [],
];
}
$errorGroups[$errorType]['count']++;
- // Add example object ID if available (limit to 5 examples)
+ // Add example object ID if available (limit to 5 examples).
if (count($errorGroups[$errorType]['examples']) < 5) {
- if (is_array($error) && isset($error['object_id'])) {
+ if (is_array($error) === true && isset($error['object_id']) === true) {
$errorGroups[$errorType]['examples'][] = $error['object_id'];
}
}
- }
+ }//end foreach
$detailedErrors['by_section'][$section] = [
'section_name' => ucfirst(str_replace('_', ' ', $section)),
'total_errors' => $sectionErrorCount,
- 'error_groups' => array_values($errorGroups)
+ 'error_groups' => array_values($errorGroups),
];
- }
- }
+ }//end if
+ }//end foreach
- // Create summary of most common errors across all sections
+ // Create summary of most common errors across all sections.
$allErrors = [];
foreach ($detailedErrors['by_section'] as $sectionData) {
foreach ($sectionData['error_groups'] as $errorGroup) {
$errorType = $errorGroup['type'];
- if (!isset($allErrors[$errorType])) {
+ if (isset($allErrors[$errorType]) === false) {
$allErrors[$errorType] = [
- 'type' => $errorType,
- 'message' => $errorGroup['message'],
- 'total_count' => 0,
- 'affected_sections' => []
+ 'type' => $errorType,
+ 'message' => $errorGroup['message'],
+ 'total_count' => 0,
+ 'affected_sections' => [],
];
}
- $allErrors[$errorType]['total_count'] += $errorGroup['count'];
+
+ $allErrors[$errorType]['total_count'] += $errorGroup['count'];
$allErrors[$errorType]['affected_sections'][] = $sectionData['section_name'];
}
}
- // Sort by frequency and take top 10
+ // Sort by frequency and take top 10.
uasort($allErrors, fn($a, $b) => $b['total_count'] - $a['total_count']);
$detailedErrors['summary'] = array_slice(array_values($allErrors), 0, 10);
return $detailedErrors;
- }
+ }//end extractDetailedErrors()
/**
* Categorize error types for better grouping and presentation
*
- * @param string $errorMessage The error message to categorize
+ * @param string $errorMessage The error message to categorize
+ *
* @return string Error category/type
*/
private function categorizeError(string $errorMessage): string
{
$errorMessage = strtolower($errorMessage);
- // Define error patterns and their categories
+ // Define error patterns and their categories.
$errorPatterns = [
- 'validation' => ['validation', 'invalid', 'required', 'missing', 'empty'],
- 'schema' => ['schema', 'structure', 'format', 'type'],
- 'reference' => ['reference', 'identifier', 'not found', 'missing reference'],
- 'property' => ['property', 'attribute', 'field'],
- 'constraint' => ['constraint', 'unique', 'duplicate', 'already exists'],
+ 'validation' => ['validation', 'invalid', 'required', 'missing', 'empty'],
+ 'schema' => ['schema', 'structure', 'format', 'type'],
+ 'reference' => ['reference', 'identifier', 'not found', 'missing reference'],
+ 'property' => ['property', 'attribute', 'field'],
+ 'constraint' => ['constraint', 'unique', 'duplicate', 'already exists'],
'relationship' => ['relationship', 'source', 'target', 'connection'],
- 'data_type' => ['string', 'integer', 'boolean', 'array', 'object'],
- 'encoding' => ['encoding', 'character', 'utf', 'ascii'],
+ 'data_type' => ['string', 'integer', 'boolean', 'array', 'object'],
+ 'encoding' => ['encoding', 'character', 'utf', 'ascii'],
];
foreach ($errorPatterns as $category => $patterns) {
foreach ($patterns as $pattern) {
- if (str_contains($errorMessage, $pattern)) {
+ if (str_contains($errorMessage, $pattern) === true) {
return $category;
}
}
}
return 'general';
- }
-}
-
-
+ }//end categorizeError()
+}//end class
diff --git a/lib/Service/ArchiMateService.php b/lib/Service/ArchiMateService.php
index 7177ad0c..5c021724 100644
--- a/lib/Service/ArchiMateService.php
+++ b/lib/Service/ArchiMateService.php
@@ -2,16 +2,15 @@
/**
* ArchiMate Service for SoftwareCatalog
- *
+ *
* Handles import and export of ArchiMate XML files with round-trip fidelity.
* Stores complete XML data as JSON blobs in the database and reconstructs
* exact XML output during export.
- *
+ *
* @category Service
* @package OCA\SoftwareCatalog\Service
- * @author SoftwareCatalog Team
- * @license AGPL-3.0
- * @version 1.0.0
+ * @author SoftwareCatalog Team
+ * @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.en.html
* @link https://github.com/nextcloud/softwarecatalog
*/
@@ -31,17 +30,16 @@
/**
* ArchiMate Service for handling XML import/export with round-trip fidelity
- *
+ *
* This service provides a clean approach to ArchiMate XML processing:
* 1. Import: Parse XML to array, store complete data as JSON blob
* 2. Storage: Use ObjectService::saveObjects with proper @self structure
* 3. Export: Reconstruct exact XML from stored JSON blobs
- *
+ *
* @category Service
* @package OCA\SoftwareCatalog\Service
- * @author SoftwareCatalog Team
- * @license AGPL-3.0
- * @version 1.0.0
+ * @author SoftwareCatalog Team
+ * @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.en.html
* @link https://github.com/nextcloud/softwarecatalog
*/
class ArchiMateService
@@ -49,63 +47,69 @@ class ArchiMateService
/**
* Configuration keys for ArchiMate processing
*/
-
+
/**
* Store last save operation timing breakdown for performance metrics
- *
+ *
* @var array
*/
private array $lastSaveTimingBreakdown = [];
/**
* Cache for camelCase property name conversions to avoid redundant processing
- *
+ *
* @var array
*/
private array $camelCaseCache = [];
/**
* Cache for identifier extraction patterns by section type
- *
+ *
* @var array
*/
private array $identifierPatternCache = [];
/**
* Cache for property definition maps to avoid rebuilding during import
- *
+ *
* @var array|null
*/
private ?array $propertyDefinitionMapCache = null;
/**
* Flag to track if we've already logged finding a GEMMA type property
- *
- * @var bool
+ *
+ * @var boolean
*/
private bool $gemmaTypePropertyFound = false;
- private const CONFIG_KEYS = [
- 'archimate_register_id' => 'archimate_register_id',
- 'archimate_schema_id' => 'archimate_schema_id',
- 'archimate_model_schema_id' => 'archimate_model_schema_id'
+ private const CONFIG_KEYS = [
+ 'archimate_register_id' => 'archimate_register_id',
+ 'archimate_schema_id' => 'archimate_schema_id',
+ 'archimate_model_schema_id' => 'archimate_model_schema_id',
];
/**
* Performance optimization settings
*/
private const PERFORMANCE_OPTIMIZATIONS = [
- 'disable_validation' => true,
- 'disable_events' => true,
- 'disable_rbac' => false, // Keep RBAC for security
- 'use_multi' => true,
- 'xml_parse_flags' => LIBXML_NOCDATA | LIBXML_NONET,
- 'memory_cleanup' => true,
- 'parallel_processing' => true,
- 'batch_size' => 1000, // Default batch size (will be adjusted intelligently)
- 'parallel_batches' => 8, // Process 8 batches concurrently
- 'max_batch_size_bytes' => 8388608, // 8 MB - safe under MySQL's 16 MB limit
- 'min_batch_size' => 50, // Minimum batch size for very large objects
- 'size_estimation_sample' => 10 // Sample size for estimating object sizes
+ 'disable_validation' => true,
+ 'disable_events' => true,
+ 'disable_rbac' => false,
+ // Keep RBAC for security.
+ 'use_multi' => true,
+ 'xml_parse_flags' => LIBXML_NOCDATA | LIBXML_NONET,
+ 'memory_cleanup' => true,
+ 'parallel_processing' => true,
+ 'batch_size' => 1000,
+ // Default batch size (will be adjusted intelligently).
+ 'parallel_batches' => 8,
+ // Process 8 batches concurrently.
+ 'max_batch_size_bytes' => 8388608,
+ // 8 MB - safe under MySQL's 16 MB limit.
+ 'min_batch_size' => 50,
+ // Minimum batch size for very large objects.
+ 'size_estimation_sample' => 10,
+ // Sample size for estimating object sizes.
];
/**
@@ -114,28 +118,32 @@ class ArchiMateService
*/
/**
- * Storage for the last save operation results
- * Contains the structured return from ObjectService::saveObjects
+ * Storage for the last save operation results.
+ * Contains the structured return from ObjectService::saveObjects.
+ *
+ * @var array|null
*/
private ?array $lastSaveResult = null;
/**
- * Cached configuration values for performance optimization
+ * Cached configuration values for performance optimization.
+ *
+ * @var array|null
*/
private ?array $cachedConfig = null;
/**
* Constructor for ArchiMateService
- *
- * @param IAppConfig $config Nextcloud app configuration service
- * @param IRootFolder $rootFolder Root folder service
- * @param IUserSession $userSession User session service
- * @param IAppManager $appManager App manager service
- * @param ContainerInterface $container PSR-11 container interface
- * @param LoggerInterface $logger Logger service
- * @param SettingsService $settingsService Settings service for schema and organization configuration
- * @param ArchiMateImportService $importService Import service for XML parsing
- * @param ArchiMateExportService $exportService Export service for XML generation
+ *
+ * @param IAppConfig $config Nextcloud app configuration service
+ * @param IRootFolder $rootFolder Root folder service
+ * @param IUserSession $userSession User session service
+ * @param IAppManager $appManager App manager service
+ * @param ContainerInterface $container PSR-11 container interface
+ * @param LoggerInterface $logger Logger service
+ * @param SettingsService $settingsService Settings service for schema and organization configuration
+ * @param ArchiMateImportService $importService Import service for XML parsing
+ * @param ArchiMateExportService $exportService Export service for XML generation
*/
public function __construct(
private readonly IAppConfig $config,
@@ -148,99 +156,111 @@ public function __construct(
private readonly ArchiMateImportService $importService,
private readonly ArchiMateExportService $exportService
) {
- }
+ }//end __construct()
/**
* OPTIMIZED: Import ArchiMate XML file using OpenRegister-style performance optimization
- *
+ *
* This method follows the same pattern as OpenRegister ImportService:
* 1. Parse ALL XML data first (single pass)
* 2. Transform to objects array (batch processing)
* 3. Single saveObjects() call with all objects
- *
+ *
* Expected performance: <1 minute for 8000 objects (vs current 13 minutes)
- *
+ *
* @param array $options Import options including file_path, fileName, etc.
+ *
* @return array Import results with detailed status
*/
- public function importArchiMateFileFromPathOptimized(array $options = []): array
+ public function importArchiMateFileFromPathOptimized(array $options=[]): array
{
- // Delegate to the import service
+ // Delegate to the import service.
return $this->importService->importArchiMateFileFromPathOptimized($options);
- }
+ }//end importArchiMateFileFromPathOptimized()
/**
* Import ArchiMate XML file from path with model detection and round-trip fidelity
- *
+ *
* This method handles the complete import workflow:
* 1. Parse XML to array (capturing all possible XML values)
* 2. Detect if model already exists or is new
* 3. Normalize data structure for storage as JSON blob
* 4. Convert to OpenRegister objects with proper @self structure
* 5. Save objects using ObjectService::saveObjects
- *
+ *
* @param array $options Import options including file_path, fileName, etc.
+ *
* @return array Import results with detailed status
*/
- public function importArchiMateFileFromPath(array $options = []): array
+ public function importArchiMateFileFromPath(array $options=[]): array
{
- // Delegate to the import service
+ // Delegate to the import service.
return $this->importService->importArchiMateFileFromPath($options);
- }
+ }//end importArchiMateFileFromPath()
/**
* Export ArchiMate data to XML
- *
+ *
* @param string|null $organization Organization filter (currently not implemented)
+ *
* @return array Export results
*/
- public function exportToArchiMate(?string $organization = null): array
+ public function exportToArchiMate(?string $organization=null): array
{
- $this->logger->info('Starting ArchiMate XML export', [
- 'organization' => $organization
- ]);
+ $this->logger->info(
+ 'Starting ArchiMate XML export',
+ [
+ 'organization' => $organization,
+ ]
+ );
try {
- // Get ObjectService and register ID
+ // Get ObjectService and register ID.
$objectService = $this->getObjectService();
- if (!$objectService) {
+ if ($objectService === null) {
throw new \RuntimeException('ObjectService not available');
}
$registerId = $this->getAmefRegisterId();
- if (!$registerId) {
+ if ($registerId === null) {
throw new \RuntimeException('AMEF register ID is not configured. Please configure the AMEF register via the admin interface.');
}
- // Create schema ID mapping for the export service
+ // Create schema ID mapping for the export service.
$schemaIdMap = $this->createSchemaIdMap();
- // Use export service to handle complete export process in one go
+ // Use export service to handle complete export process in one go.
$xml = $this->exportService->exportArchiMateXml($objectService, $registerId, $schemaIdMap, $organization);
-
- $this->logger->info('ArchiMate export completed successfully', [
- 'organization_filter' => $organization,
- 'xml_size' => strlen($xml)
- ]);
+
+ $this->logger->info(
+ 'ArchiMate export completed successfully',
+ [
+ 'organization_filter' => $organization,
+ 'xml_size' => strlen($xml),
+ ]
+ );
return [
- 'success' => true,
- 'xml' => $xml,
- 'exported_count' => 'calculated_in_export_service' // Will be logged by export service
+ 'success' => true,
+ 'xml' => $xml,
+ 'exported_count' => 'calculated_in_export_service',
+ // Will be logged by export service.
];
-
} catch (\Exception $e) {
- $this->logger->error('ArchiMate export failed', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'ArchiMate export failed',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return [
'success' => false,
- 'error' => $e->getMessage()
+ 'error' => $e->getMessage(),
];
- }
- }
+ }//end try
+ }//end exportToArchiMate()
/**
* Export organization-specific ArchiMate data to XML
@@ -248,38 +268,52 @@ public function exportToArchiMate(?string $organization = null): array
* Produces an enriched AMEFF file with the base GEMMA model plus the
* organization's applications plotted on referentiecomponenten in views.
*
- * @param string $organizationUuid UUID of the organization to export for
+ * @param string $organizationUuid UUID of the organization to export for.
+ * @param array $options Optional export options.
+ *
* @return array Export results with 'success', 'xml', 'file_name'
*/
- public function exportOrgArchiMate(string $organizationUuid, array $options = []): array
+ public function exportOrgArchiMate(string $organizationUuid, array $options=[]): array
{
- $this->logger->info('Starting organization ArchiMate XML export', [
- 'organization_uuid' => $organizationUuid,
- 'options' => $options
- ]);
+ $this->logger->info(
+ 'Starting organization ArchiMate XML export',
+ [
+ 'organization_uuid' => $organizationUuid,
+ 'options' => $options,
+ ]
+ );
try {
$objectService = $this->getObjectService();
- if (!$objectService) {
+ if ($objectService === null) {
throw new \RuntimeException('ObjectService not available');
}
- // Look up the organization from Voorzieningen register
+ // Look up the organization from Voorzieningen register.
$voorzConfig = $this->settingsService->getVoorzieningenConfig();
- $orgRegisterId = !empty($voorzConfig['register']) ? (int)$voorzConfig['register'] : null;
- $orgSchemaId = !empty($voorzConfig['organisatie_schema']) ? (int)$voorzConfig['organisatie_schema'] : null;
+ if (empty($voorzConfig['register']) === false) {
+ $orgRegisterId = (int) $voorzConfig['register'];
+ } else {
+ $orgRegisterId = null;
+ }
- if (!$orgRegisterId || !$orgSchemaId) {
- // Fallback to generic lookup
+ if (empty($voorzConfig['organisatie_schema']) === false) {
+ $orgSchemaId = (int) $voorzConfig['organisatie_schema'];
+ } else {
+ $orgSchemaId = null;
+ }
+
+ if ($orgRegisterId === null || $orgSchemaId === false) {
+ // Fallback to generic lookup.
$orgRegisterId = $this->settingsService->getVoorzieningenRegisterId();
- $orgSchemaId = $this->settingsService->getSchemaIdForObjectType('organisatie');
+ $orgSchemaId = $this->settingsService->getSchemaIdForObjectType('organisatie');
}
- if (!$orgRegisterId || !$orgSchemaId) {
+ if ($orgRegisterId === null || $orgSchemaId === false) {
throw new \RuntimeException('Organization register/schema not configured');
}
- // Look up the organization directly by UUID
+ // Look up the organization directly by UUID.
try {
$orgEntity = $objectService->find(
id: $organizationUuid,
@@ -295,101 +329,132 @@ public function exportOrgArchiMate(string $organizationUuid, array $options = []
if ($orgEntity === null) {
return [
'success' => false,
- 'error' => 'Organization not found: ' . $organizationUuid
+ 'error' => 'Organization not found: '.$organizationUuid,
];
}
$organization = $orgEntity->jsonSerialize();
- $orgName = $organization['naam'] ?? $organization['name'] ?? $organization['@self']['name'] ?? 'Unknown';
+ $orgName = $organization['naam'] ?? $organization['name'] ?? $organization['@self']['name'] ?? 'Unknown';
- // Get AMEF config and base objects
+ // Get AMEF config and base objects.
$registerId = $this->getAmefRegisterId();
- if (!$registerId) {
+ if ($registerId === null) {
throw new \RuntimeException('AMEF register ID is not configured');
}
+
$schemaIdMap = $this->createSchemaIdMap();
- // Query organization's gebruik and modules from Voorzieningen register
- $gebruikSchemaId = !empty($voorzConfig['gebruik_schema']) ? (int)$voorzConfig['gebruik_schema'] : null;
+ // Query organization's gebruik and modules from Voorzieningen register.
+ if (empty($voorzConfig['gebruik_schema']) === false) {
+ $gebruikSchemaId = (int) $voorzConfig['gebruik_schema'];
+ } else {
+ $gebruikSchemaId = null;
+ }
+
$gebruikData = [];
- if ($gebruikSchemaId) {
+ if (empty($gebruikSchemaId) === false) {
$gebruikQuery = [
- '@self' => [
- 'register' => $orgRegisterId,
- 'schema' => $gebruikSchemaId,
- 'organisation' => $organizationUuid
+ '@self' => [
+ 'register' => $orgRegisterId,
+ 'schema' => $gebruikSchemaId,
+ 'organisation' => $organizationUuid,
],
- '_limit' => 10000
+ '_limit' => 10000,
];
- $gebruikData = $objectService->searchObjects(query: $gebruikQuery, _rbac: false, _multitenancy: false);
+ $gebruikData = $objectService->searchObjects(query: $gebruikQuery, _rbac: false, _multitenancy: false);
+ }
+
+ if (empty($voorzConfig['module_schema']) === false) {
+ $moduleSchemaId = (int) $voorzConfig['module_schema'];
+ } else {
+ $moduleSchemaId = null;
}
- $moduleSchemaId = !empty($voorzConfig['module_schema']) ? (int)$voorzConfig['module_schema'] : null;
$modulesData = [];
- if ($moduleSchemaId) {
+ if (empty($moduleSchemaId) === false) {
$modulesQuery = [
- '@self' => [
- 'register' => $orgRegisterId,
- 'schema' => $moduleSchemaId,
- 'organisation' => $organizationUuid
+ '@self' => [
+ 'register' => $orgRegisterId,
+ 'schema' => $moduleSchemaId,
+ 'organisation' => $organizationUuid,
],
- '_limit' => 10000
+ '_limit' => 10000,
];
- $modulesData = $objectService->searchObjects(query: $modulesQuery, _rbac: false, _multitenancy: false);
+ $modulesData = $objectService->searchObjects(query: $modulesQuery, _rbac: false, _multitenancy: false);
}
- // Query deelname gebruik if enabled (gebruik objects where this org is in deelnemers)
+ // Query deelname gebruik if enabled (gebruik objects where this org is in deelnemers).
$deelnamesData = [];
if ($options['deelnames'] ?? false) {
- if ($gebruikSchemaId) {
+ if (empty($gebruikSchemaId) === false) {
$deelnameQuery = [
- '@self' => [
+ '@self' => [
'register' => $orgRegisterId,
- 'schema' => $gebruikSchemaId
+ 'schema' => $gebruikSchemaId,
],
'deelnemers' => $organizationUuid,
- '_limit' => 10000
+ '_limit' => 10000,
];
$deelnamesData = $objectService->searchObjects(
- query: $deelnameQuery, _rbac: false, _multitenancy: false
+ query: $deelnameQuery,
+ _rbac: false,
+ _multitenancy: false
);
- $this->logger->info('Retrieved deelname gebruik for org export', [
- 'deelnames_count' => count($deelnamesData),
- 'organization_uuid' => $organizationUuid
- ]);
-
- // Deelname modules belong to other orgs, so query all modules (without org filter)
- // to resolve names for the export
- if (!empty($deelnamesData) && $moduleSchemaId) {
+ $this->logger->info(
+ 'Retrieved deelname gebruik for org export',
+ [
+ 'deelnames_count' => count($deelnamesData),
+ 'organization_uuid' => $organizationUuid,
+ ]
+ );
+
+ // Deelname modules belong to other orgs, so query all modules (without org filter).
+ // to resolve names for the export.
+ if (empty($deelnamesData) === false && $moduleSchemaId === true) {
$allModulesQuery = [
- '@self' => [
+ '@self' => [
'register' => $orgRegisterId,
- 'schema' => $moduleSchemaId
+ 'schema' => $moduleSchemaId,
],
- '_limit' => 10000
+ '_limit' => 10000,
];
- $allModules = $objectService->searchObjects(
- query: $allModulesQuery, _rbac: false, _multitenancy: false
+ $allModules = $objectService->searchObjects(
+ query: $allModulesQuery,
+ _rbac: false,
+ _multitenancy: false
);
- // Merge into modulesData, deduplicating by ID
+ // Merge into modulesData, deduplicating by ID.
$existingIds = [];
foreach ($modulesData as $m) {
- $mid = is_array($m) ? ($m['id'] ?? $m['@self']['id'] ?? null) : null;
- if ($mid) $existingIds[$mid] = true;
+ if (is_array($m) === true) {
+ $mid = ($m['id'] ?? $m['@self']['id'] ?? null);
+ } else {
+ $mid = null;
+ }
+
+ if (empty($mid) === false) {
+ $existingIds[$mid] = true;
+ }
}
+
foreach ($allModules as $mod) {
- $modArr = (is_object($mod) && method_exists($mod, 'jsonSerialize')) ? $mod->jsonSerialize() : $mod;
+ if ((is_object($mod) === true && method_exists($mod, 'jsonSerialize') === true)) {
+ $modArr = $mod->jsonSerialize();
+ } else {
+ $modArr = $mod;
+ }
+
$modId = $modArr['id'] ?? $modArr['@self']['id'] ?? null;
- if ($modId && !isset($existingIds[$modId])) {
- $modulesData[] = $mod;
+ if ($modId !== false && isset($existingIds[$modId]) === false) {
+ $modulesData[] = $mod;
$existingIds[$modId] = true;
}
}
- }
- }
- }
+ }//end if
+ }//end if
+ }//end if
- // Delegate to export service
+ // Delegate to export service.
$xml = $this->exportService->exportOrganizationArchiMateXml(
$objectService,
$registerId,
@@ -402,27 +467,29 @@ public function exportOrgArchiMate(string $organizationUuid, array $options = []
$options
);
- // Generate file name: DD-MM-YYYY_Softwarecatalogus_AMEFF_export_OrgName.xml
- $fileName = date('d-m-Y') . '_Softwarecatalogus_AMEFF_export_' . str_replace(' ', '_', $orgName) . '.xml';
+ // Generate file name: DD-MM-YYYY_Softwarecatalogus_AMEFF_export_OrgName.xml.
+ $fileName = date('d-m-Y').'_Softwarecatalogus_AMEFF_export_'.str_replace(' ', '_', $orgName).'.xml';
return [
- 'success' => true,
- 'xml' => $xml,
- 'file_name' => $fileName
+ 'success' => true,
+ 'xml' => $xml,
+ 'file_name' => $fileName,
];
-
} catch (\Exception $e) {
- $this->logger->error('Organization ArchiMate export failed', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'Organization ArchiMate export failed',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return [
'success' => false,
- 'error' => $e->getMessage()
+ 'error' => $e->getMessage(),
];
- }
- }
+ }//end try
+ }//end exportOrgArchiMate()
/**
* Create schema ID mapping for export service
@@ -436,267 +503,269 @@ private function createSchemaIdMap(): array
foreach ($schemaTypes as $schemaType) {
$schemaId = $this->settingsService->getSchemaIdForObjectType($schemaType);
- if ($schemaId) {
+ if (empty($schemaId) === false) {
$schemaIdMap[$schemaId] = $schemaType;
}
}
return $schemaIdMap;
- }
-
-
-
-
-
-
-
-
-
-
-
-
+ }//end createSchemaIdMap()
/**
* Get section structure configuration for XML parsing
- *
+ *
* @param string $sectionName The name of the section (e.g., 'elements', 'relationships', 'views', etc.)
+ *
* @return array Configuration with direct_tags and nested_paths for finding items
*/
private function getSectionStructureConfig(string $sectionName): array
{
- // Define the structure configuration for each section type
+ // Define the structure configuration for each section type.
$configs = [
- 'elements' => [
- 'direct_tags' => ['element', 'elements'],
+ 'elements' => [
+ 'direct_tags' => ['element', 'elements'],
'nested_paths' => [
['model', 'elements', 'element'],
['model', 'elements'],
['elements', 'element'],
- ['elements']
- ]
+ ['elements'],
+ ],
],
- 'relationships' => [
- 'direct_tags' => ['relationship', 'relationships'],
+ 'relationships' => [
+ 'direct_tags' => ['relationship', 'relationships'],
'nested_paths' => [
['model', 'relationships', 'relationship'],
['model', 'relationships'],
['relationships', 'relationship'],
- ['relationships']
- ]
+ ['relationships'],
+ ],
],
- 'views' => [
- 'direct_tags' => ['view', 'views', 'diagram', 'diagrams'],
+ 'views' => [
+ 'direct_tags' => ['view', 'views', 'diagram', 'diagrams'],
'nested_paths' => [
['model', 'views', 'diagrams', 'view'],
['model', 'views', 'diagrams'],
['model', 'views'],
['views', 'diagrams', 'view'],
['views', 'diagrams'],
- ['views']
- ]
+ ['views'],
+ ],
],
- 'organizations' => [
- 'direct_tags' => ['item', 'items'],
+ 'organizations' => [
+ 'direct_tags' => ['item', 'items'],
'nested_paths' => [
['model', 'organizations', 'item'],
['model', 'organizations'],
['organizations', 'item'],
- ['organizations']
- ]
+ ['organizations'],
+ ],
],
'property_definitions' => [
- 'direct_tags' => ['propertyDefinition', 'propertyDefinitions'],
+ 'direct_tags' => ['propertyDefinition', 'propertyDefinitions'],
'nested_paths' => [
['model', 'propertyDefinitions', 'propertyDefinition'],
['model', 'propertyDefinitions'],
['propertyDefinitions', 'propertyDefinition'],
- ['propertyDefinitions']
- ]
- ]
+ ['propertyDefinitions'],
+ ],
+ ],
];
return $configs[$sectionName] ?? [
- 'direct_tags' => [$sectionName],
- 'nested_paths' => [[$sectionName]]
+ 'direct_tags' => [$sectionName],
+ 'nested_paths' => [[$sectionName]],
];
- }
+ }//end getSectionStructureConfig()
/**
* Check if an array is associative (has string keys)
- *
+ *
* @param array $array The array to check
+ *
* @return bool True if associative, false if indexed
*/
private function isAssociativeArray(array $array): bool
{
return count(array_filter(array_keys($array), 'is_string')) > 0;
- }
+ }//end isAssociativeArray()
/**
* Find items within a specific section using AMEF configuration
- *
- * @param array $sectionData The section data to search
+ *
+ * @param array $sectionData The section data to search
* @param string $sectionName The name of the section
+ *
* @return array Array of items found
*/
private function findItemsInSection(array $sectionData, string $sectionName): array
{
- // OPTIMIZATION: Removed debug logging from section processing
-
+ // OPTIMIZATION: Removed debug logging from section processing.
$items = [];
-
- // Safety check: ensure sectionData is an array
- if (!is_array($sectionData)) {
+
+ // Safety check: ensure sectionData is an array.
+ if (is_array($sectionData) === false) {
return [];
}
-
- // Get section structure configuration from AMEF config
- $config = $this->getSectionStructureConfig($sectionName);
-
- // Special handling for views with diagrams structure
+
+ // Get section structure configuration from AMEF config.
+ $config = $this->getSectionStructureConfig(sectionName: $sectionName);
+
+ // Special handling for views with diagrams structure.
if ($sectionName === 'views') {
-
- // Handle nested structure:
- if (isset($sectionData['diagrams'])) {
- if (isset($sectionData['diagrams']['view'])) {
+ // Handle nested structure: .
+ if (isset($sectionData['diagrams']) === true) {
+ if (isset($sectionData['diagrams']['view']) === true) {
$viewArray = $sectionData['diagrams']['view'];
-
- // Handle single view vs array of views
- if (!isset($viewArray[0]) && isset($viewArray['_attributes'])) {
- // Single view
+
+ // Handle single view vs array of views.
+ if (isset($viewArray[0]) === false && isset($viewArray['_attributes']) === true) {
+ // Single view.
$items = [$viewArray];
} else {
- // Array of views
+ // Array of views.
$items = $viewArray;
}
}
} else {
- // Direct views structure (fallback)
- if (isset($sectionData['view'])) {
+ // Direct views structure (fallback).
+ if (isset($sectionData['view']) === true) {
$items = $sectionData['view'];
}
}
} else {
- // Try to find items using the configured paths for other sections
+ // Try to find items using the configured paths for other sections.
foreach ($config['nested_paths'] as $path) {
$currentData = $sectionData;
- $pathValid = true;
-
+ $pathValid = true;
+
foreach ($path as $key) {
- if (isset($currentData[$key])) {
+ if (isset($currentData[$key]) === true) {
$currentData = $currentData[$key];
} else {
$pathValid = false;
break;
}
}
-
- if ($pathValid && is_array($currentData)) {
- // Check if this is a direct array of items or needs further processing
- if (isset($currentData[0]) || $this->isAssociativeArray($currentData)) {
+
+ if ($pathValid !== false && is_array($currentData) === true) {
+ // Check if this is a direct array of items or needs further processing.
+ if (isset($currentData[0]) === true || $this->isAssociativeArray(array: $currentData) === true) {
$items = $currentData;
break;
}
}
- }
- }
-
- // If no items found through nested paths, try direct tags
- if (empty($items)) {
+ }//end foreach
+ }//end if
+
+ // If no items found through nested paths, try direct tags.
+ if (empty($items) === true) {
foreach ($config['direct_tags'] as $tag) {
- if (isset($sectionData[$tag])) {
+ if (isset($sectionData[$tag]) === true) {
$items = $sectionData[$tag];
break;
}
}
}
-
- // If still no items found, treat the section itself as items
- if (empty($items)) {
+
+ // If still no items found, treat the section itself as items.
+ if (empty($items) === true) {
$items = [$sectionData];
}
-
- // Ensure items is always an array
- if (!is_array($items)) {
+
+ // Ensure items is always an array.
+ if (is_array($items) === false) {
$items = [$items];
}
-
- // If items is an associative array with numeric keys, convert to indexed array
- if ($this->isAssociativeArray($items)) {
+
+ // If items is an associative array with numeric keys, convert to indexed array.
+ if ($this->isAssociativeArray(array: $items) === true) {
$items = array_values($items);
}
-
+
return $items;
- }
+ }//end findItemsInSection()
/**
* Extract identifier from item data
- *
- * @param array $item Item data
+ *
+ * @param array $item Item data
* @param string $sectionName The section name for special handling
+ *
* @return string|null Identifier or null if not found
*/
- private function extractIdentifier(array $item, string $sectionName = ''): ?string
+ private function extractIdentifier(array $item, string $sectionName=''): ?string
{
- // OPTIMIZATION: Use cached patterns for section-specific identifier extraction
- if (isset($this->identifierPatternCache[$sectionName])) {
+ // OPTIMIZATION: Use cached patterns for section-specific identifier extraction.
+ if (isset($this->identifierPatternCache[$sectionName]) === true) {
$patterns = $this->identifierPatternCache[$sectionName];
-
- // Try cached patterns in order of success frequency
+
+ // Try cached patterns in order of success frequency.
foreach ($patterns as $pattern) {
- $result = $this->extractIdentifierByPattern($item, $pattern);
+ $result = $this->extractIdentifierByPattern(item: $item, pattern: $pattern);
if ($result !== null) {
return $result;
}
}
}
-
- // OPTIMIZATION: Build pattern cache on first encounter of section type
- $patterns = $this->buildIdentifierPatternsForSection($sectionName);
+
+ // OPTIMIZATION: Build pattern cache on first encounter of section type.
+ $patterns = $this->buildIdentifierPatternsForSection(sectionName: $sectionName);
$this->identifierPatternCache[$sectionName] = $patterns;
-
- // Try all patterns and return first successful match
+
+ // Try all patterns and return first successful match.
foreach ($patterns as $pattern) {
- $result = $this->extractIdentifierByPattern($item, $pattern);
+ $result = $this->extractIdentifierByPattern(item: $item, pattern: $pattern);
if ($result !== null) {
return $result;
}
}
return null;
- }
+ }//end extractIdentifier()
/**
* OPTIMIZATION: Extract identifier using a specific pattern
- *
- * @param array $item The item to extract from
+ *
+ * @param array $item The item to extract from
* @param array $pattern The extraction pattern ['path' => string[], 'type' => string]
+ *
* @return string|null The extracted identifier or null
*/
private function extractIdentifierByPattern(array $item, array $pattern): ?string
{
$path = $pattern['path'];
$type = $pattern['type'];
-
- // Navigate to the target location
+
+ // Navigate to the target location.
$current = $item;
foreach ($path as $key) {
- if (!isset($current[$key])) {
+ if (isset($current[$key]) === false) {
return null;
}
+
$current = $current[$key];
}
-
- // Extract based on type
+
+ // Extract based on type.
switch ($type) {
case 'direct':
- return is_string($current) ? $current : null;
+ if (is_string($current) === true) {
+ return $current;
+ } else {
+ return null;
+ }
+
case 'value':
- return is_array($current) && isset($current['_value']) ? (string) $current['_value'] : null;
+ if (is_array($current) === true && isset($current['_value']) === true) {
+ return (string) $current['_value'];
+ } else {
+ return null;
+ }
+
case 'array_search':
- if (is_array($current)) {
+ if (is_array($current) === true) {
foreach ($current as $childItem) {
- if (isset($childItem['_attributes']['identifierRef'])) {
+ if (isset($childItem['_attributes']['identifierRef']) === true) {
return (string) $childItem['_attributes']['identifierRef'];
}
}
@@ -704,27 +773,28 @@ private function extractIdentifierByPattern(array $item, array $pattern): ?strin
return null;
default:
return null;
- }
- }
+ }//end switch
+ }//end extractIdentifierByPattern()
/**
* OPTIMIZATION: Build identifier extraction patterns for a section type
- *
+ *
* @param string $sectionName The section name
+ *
* @return array Array of extraction patterns ordered by likelihood of success
*/
private function buildIdentifierPatternsForSection(string $sectionName): array
{
$patterns = [];
-
- // Special handling for organizations
+
+ // Special handling for organizations.
if ($sectionName === 'organizations') {
$patterns[] = ['path' => ['_attributes', 'identifierRef'], 'type' => 'direct'];
$patterns[] = ['path' => ['item'], 'type' => 'array_search'];
$patterns[] = ['path' => ['label'], 'type' => 'value'];
$patterns[] = ['path' => ['label'], 'type' => 'direct'];
} else {
- // Standard patterns for other sections (ordered by frequency in ArchiMate)
+ // Standard patterns for other sections (ordered by frequency in ArchiMate).
$patterns[] = ['path' => ['_attributes', 'identifier'], 'type' => 'direct'];
$patterns[] = ['path' => ['_attributes', 'id'], 'type' => 'direct'];
$patterns[] = ['path' => ['identifier'], 'type' => 'value'];
@@ -735,542 +805,616 @@ private function buildIdentifierPatternsForSection(string $sectionName): array
$patterns[] = ['path' => ['name'], 'type' => 'value'];
$patterns[] = ['path' => ['name'], 'type' => 'direct'];
}
-
+
return $patterns;
- }
+ }//end buildIdentifierPatternsForSection()
/**
* Convert normalized data to OpenRegister objects with @self structure
- *
+ *
* This method creates OpenRegister objects from the normalized ArchiMate data:
* 1. Creates a model object with proper @self structure
* 2. Creates section objects for each item (elements, relationships, etc.)
* 3. Ensures each object has the required @self structure for ObjectService::saveObjects
* 4. Links all objects to the parent model via model_identifier
- *
- * @param array $normalizedData Normalized ArchiMate data with model_identifier
+ *
+ * @param array $normalizedData Normalized ArchiMate data with model_identifier
* @param string $modelIdentifier The model identifier for linking objects
+ *
* @return array Array of OpenRegister objects with proper @self structure
*/
private function convertToOpenRegisterObjects(array $normalizedData, string $modelIdentifier): array
{
- $this->logger->info('Converting to OpenRegister objects with @self structure', [
- 'model_identifier' => $modelIdentifier
- ]);
+ $this->logger->info(
+ 'Converting to OpenRegister objects with @self structure',
+ [
+ 'model_identifier' => $modelIdentifier,
+ ]
+ );
$objects = [];
-
- // STEP 1: Convert model metadata to model object
- if (!empty($normalizedData['model_metadata'])) {
+
+ // STEP 1: Convert model metadata to model object.
+ if (empty($normalizedData['model_metadata']) === false) {
$this->logger->debug('Creating model object from metadata');
- $objects[] = $this->createModelObject($normalizedData['model_metadata'], $modelIdentifier);
+ $objects[] = $this->createModelObject(metadata: $normalizedData['model_metadata'], modelIdentifier: $modelIdentifier);
}
- // STEP 2: Convert each section to individual objects
+ // STEP 2: Convert each section to individual objects.
$sections = ['elements', 'relationships', 'organizations', 'views', 'property_definitions'];
-
- // OPTIMIZATION: Removed excessive debug logging from tight loops
+
+ // OPTIMIZATION: Removed excessive debug logging from tight loops.
$sectionCounts = [];
foreach ($sections as $section) {
- if (!empty($normalizedData[$section]) && is_array($normalizedData[$section])) {
+ if (empty($normalizedData[$section]) === false && is_array($normalizedData[$section]) === true) {
$sectionCounts[$section] = count($normalizedData[$section]);
foreach ($normalizedData[$section] as $identifier => $data) {
- $objects[] = $this->createSectionObject($section, $identifier, $data, $modelIdentifier);
+ $objects[] = $this->createSectionObject(
+ section: $section,
+ identifier: $identifier,
+ data: $data,
+ modelIdentifier: $modelIdentifier
+ );
}
} else {
$sectionCounts[$section] = 0;
}
}
-
- // Single consolidated log entry
+
+ // Single consolidated log entry.
$this->logger->debug('Sections processed', $sectionCounts);
- $this->logger->info('Conversion to OpenRegister objects completed', [
- 'model_identifier' => $modelIdentifier,
- 'total_objects' => count($objects),
- 'sections_processed' => $sections
- ]);
+ $this->logger->info(
+ 'Conversion to OpenRegister objects completed',
+ [
+ 'model_identifier' => $modelIdentifier,
+ 'total_objects' => count($objects),
+ 'sections_processed' => $sections,
+ ]
+ );
return $objects;
- }
+ }//end convertToOpenRegisterObjects()
/**
* Create model object with @self structure
- *
- * @param array $metadata Model metadata
+ *
+ * @param array $metadata Model metadata
* @param string $modelIdentifier Model identifier
+ *
* @return array Model object with @self structure
*/
private function createModelObject(array $metadata, string $modelIdentifier): array
{
- // OPTIMIZATION: Use cached configuration values
+ // OPTIMIZATION: Use cached configuration values.
$registerId = $this->cachedConfig['registerId'];
- $schemaId = $this->cachedConfig['schemaIds']['model'];
-
- // Create object with @self structure and metadata at root level (no JSON serialization)
+ $schemaId = $this->cachedConfig['schemaIds']['model'];
+
+ // Create object with @self structure and metadata at root level (no JSON serialization).
$object = [
- '@self' => [
- 'register' => $registerId,
- 'schema' => $schemaId,
- 'id' => $metadata['identifier'] ?? uniqid('model_'),
- 'published' => date('Y-m-d\TH:i:s\Z')
+ '@self' => [
+ 'register' => $registerId,
+ 'schema' => $schemaId,
+ 'id' => $metadata['identifier'] ?? uniqid('model_'),
+ 'published' => date('Y-m-d\TH:i:s\Z'),
],
- 'identifier' => $metadata['identifier'] ?? '',
- 'section' => 'model',
- 'model_identifier' => $modelIdentifier
+ 'identifier' => $metadata['identifier'] ?? '',
+ 'section' => 'model',
+ 'model_identifier' => $modelIdentifier,
];
-
- // Merge metadata directly at root level
+
+ // Merge metadata directly at root level.
return array_merge($object, $metadata);
- }
+ }//end createModelObject()
/**
* Create section object with @self structure and flattened XML data
- *
- * @param string $section Section name
- * @param string $identifier Item identifier
- * @param array $data Item data (already contains XML data at root level)
+ *
+ * @param string $section Section name
+ * @param string $identifier Item identifier
+ * @param array $data Item data (already contains XML data at root level)
* @param string $modelIdentifier Model identifier for linking
+ *
* @return array Section object with @self structure
*/
private function createSectionObject(string $section, string $identifier, array $data, string $modelIdentifier): array
{
- // OPTIMIZATION: Use cached configuration values
+ // OPTIMIZATION: Use cached configuration values.
$registerId = $this->cachedConfig['registerId'];
- $schemaId = $this->cachedConfig['schemaIds'][$section] ?? $this->getSchemaIdForSection($section);
-
- // Create object with @self structure and XML data at root level (no double serialization)
+ $schemaId = $this->cachedConfig['schemaIds'][$section] ?? $this->getSchemaIdForSection(section: $section);
+
+ // Create object with @self structure and XML data at root level (no double serialization).
$object = [
'@self' => [
- 'register' => $registerId,
- 'schema' => $schemaId,
- 'id' => $identifier,
- 'published' => date('Y-m-d\TH:i:s\Z')
- ]
+ 'register' => $registerId,
+ 'schema' => $schemaId,
+ 'id' => $identifier,
+ 'published' => date('Y-m-d\TH:i:s\Z'),
+ ],
];
-
- // Set slug: first try from _slug field, then from Object ID property, then extract from identifier
+
+ // Set slug: first try from _slug field, then from Object ID property, then extract from identifier.
$slug = null;
-
- // Check if there's a temporary slug to move to @self structure
- if (isset($data['_slug'])) {
+
+ // Check if there's a temporary slug to move to @self structure.
+ if (isset($data['_slug']) === true) {
$slug = $data['_slug'];
- unset($data['_slug']); // Remove the temporary field
- }
- // Check if we have "Object ID" property directly
- elseif (isset($data['Object ID'])) {
+ unset($data['_slug']);
+ } else if (isset($data['Object ID']) === true) {
+ // Check if we have "Object ID" property directly.
$slug = $data['Object ID'];
+ } else if ($identifier !== false && str_starts_with($identifier, 'id-') === true) {
+ // Fallback: extract from identifier (remove "id-" prefix if present).
+ $slug = substr($identifier, 3);
}
- // Fallback: extract from identifier (remove "id-" prefix if present)
- elseif ($identifier && str_starts_with($identifier, 'id-')) {
- $slug = substr($identifier, 3); // Remove "id-" prefix
- }
-
- // Set the slug if we found one
- if ($slug) {
+
+ // Set the slug if we found one.
+ if (empty($slug) === false) {
$object['@self']['slug'] = $slug;
}
-
- // Merge XML data directly at root level (data already contains identifier, section, model_identifier)
+
+ // Merge XML data directly at root level (data already contains identifier, section, model_identifier).
return array_merge($object, $data);
- }
+ }//end createSectionObject()
/**
* Save objects to database using ObjectService::saveObjects
- *
+ *
* @param array $objects Objects to save
+ *
* @return array Saved objects
*/
private function saveObjectsToDatabase(array $objects): array
{
$saveStartTime = microtime(true);
-
+
$serviceInitStartTime = microtime(true);
- $objectService = $this->getObjectService();
- if (!$objectService) {
+ $objectService = $this->getObjectService();
+ if ($objectService === null) {
throw new \RuntimeException('ObjectService not available');
}
+
$serviceInitTime = microtime(true) - $serviceInitStartTime;
- // ENHANCEMENT: Process GEMMA Referentiecomponent-Standaard relationships before saving
+ // ENHANCEMENT: Process GEMMA Referentiecomponent-Standaard relationships before saving.
$gemmaProcessingStartTime = microtime(true);
- $objects = $this->processGemmaReferenceComponentStandards($objects);
+ $objects = $this->processGemmaReferenceComponentStandards(objects: $objects);
$gemmaProcessingTime = microtime(true) - $gemmaProcessingStartTime;
- $this->logger->info('Saving objects to database using parallel batch processing', [
- 'count' => count($objects),
- 'batch_size' => self::PERFORMANCE_OPTIMIZATIONS['batch_size'],
- 'parallel_batches' => self::PERFORMANCE_OPTIMIZATIONS['parallel_batches'],
- 'service_init_time' => round($serviceInitTime, 3),
- 'gemma_processing_time' => round($gemmaProcessingTime, 3)
- ]);
+ $this->logger->info(
+ 'Saving objects to database using parallel batch processing',
+ [
+ 'count' => count($objects),
+ 'batch_size' => self::PERFORMANCE_OPTIMIZATIONS['batch_size'],
+ 'parallel_batches' => self::PERFORMANCE_OPTIMIZATIONS['parallel_batches'],
+ 'service_init_time' => round($serviceInitTime, 3),
+ 'gemma_processing_time' => round($gemmaProcessingTime, 3),
+ ]
+ );
- // OPTIMIZATION: Use cached register ID
+ // OPTIMIZATION: Use cached register ID.
$registerId = $this->cachedConfig['registerId'];
- // PERFORMANCE OPTIMIZATION: Use parallel batch processing for large datasets
+ // PERFORMANCE OPTIMIZATION: Use parallel batch processing for large datasets.
$batchProcessingStartTime = microtime(true);
- if (self::PERFORMANCE_OPTIMIZATIONS['parallel_processing'] && count($objects) > self::PERFORMANCE_OPTIMIZATIONS['batch_size']) {
- $result = $this->saveObjectsInParallelBatches($objects, $objectService, $registerId);
+ if (self::PERFORMANCE_OPTIMIZATIONS['parallel_processing'] === true && count($objects) > self::PERFORMANCE_OPTIMIZATIONS['batch_size']) {
+ $result = $this->saveObjectsInParallelBatches(objects: $objects, objectService: $objectService, registerId: $registerId);
} else {
- // Fallback to single batch for small datasets
- $result = $this->saveObjectsInSingleBatch($objects, $objectService, $registerId);
+ // Fallback to single batch for small datasets.
+ $result = $this->saveObjectsInSingleBatch(objects: $objects, objectService: $objectService, registerId: $registerId);
}
+
$batchProcessingTime = microtime(true) - $batchProcessingStartTime;
-
+
$totalSaveTime = microtime(true) - $saveStartTime;
-
- $this->logger->info('Database save operation completed', [
- 'total_save_time' => round($totalSaveTime, 3),
- 'service_init_time' => round($serviceInitTime, 3),
- 'gemma_processing_time' => round($gemmaProcessingTime, 3),
- 'batch_processing_time' => round($batchProcessingTime, 3),
- 'objects_saved' => count($result),
- 'save_rate_objects_per_second' => round(count($objects) / max($totalSaveTime, 0.001), 1)
- ]);
-
- // Store timing breakdown for performance metrics
+
+ $this->logger->info(
+ 'Database save operation completed',
+ [
+ 'total_save_time' => round($totalSaveTime, 3),
+ 'service_init_time' => round($serviceInitTime, 3),
+ 'gemma_processing_time' => round($gemmaProcessingTime, 3),
+ 'batch_processing_time' => round($batchProcessingTime, 3),
+ 'objects_saved' => count($result),
+ 'save_rate_objects_per_second' => round(count($objects) / max($totalSaveTime, 0.001), 1),
+ ]
+ );
+
+ // Store timing breakdown for performance metrics.
$this->lastSaveTimingBreakdown = [
- 'total_save_seconds' => round($totalSaveTime, 3),
- 'service_init_seconds' => round($serviceInitTime, 3),
- 'gemma_processing_seconds' => round($gemmaProcessingTime, 3),
- 'batch_processing_seconds' => round($batchProcessingTime, 3),
- 'objects_saved' => count($result),
- 'save_rate_objects_per_second' => round(count($objects) / max($totalSaveTime, 0.001), 1)
+ 'total_save_seconds' => round($totalSaveTime, 3),
+ 'service_init_seconds' => round($serviceInitTime, 3),
+ 'gemma_processing_seconds' => round($gemmaProcessingTime, 3),
+ 'batch_processing_seconds' => round($batchProcessingTime, 3),
+ 'objects_saved' => count($result),
+ 'save_rate_objects_per_second' => round(count($objects) / max($totalSaveTime, 0.001), 1),
];
return $result;
- }
+ }//end saveObjectsToDatabase()
/**
* Save objects in parallel batches for maximum performance
- *
- * @param array $objects Array of objects to save
+ *
+ * @param array $objects Array of objects to save
* @param ObjectService $objectService ObjectService instance
- * @param int $registerId Register ID
+ * @param int $registerId Register ID
+ *
* @return array Array of saved objects
*/
private function saveObjectsInParallelBatches(array $objects, ObjectService $objectService, int $registerId): array
{
- $batchSize = self::PERFORMANCE_OPTIMIZATIONS['batch_size'];
+ $batchSize = self::PERFORMANCE_OPTIMIZATIONS['batch_size'];
$parallelBatches = self::PERFORMANCE_OPTIMIZATIONS['parallel_batches'];
-
- // INTELLIGENT BATCH SIZING: Create size-aware batches instead of fixed-size chunks
- $chunks = $this->createIntelligentBatches($objects);
+
+ // INTELLIGENT BATCH SIZING: Create size-aware batches instead of fixed-size chunks.
+ $chunks = $this->createIntelligentBatches(objects: $objects);
$totalChunks = count($chunks);
-
- $this->logger->info('Starting intelligent batch processing', [
- 'total_objects_to_save' => count($objects),
- 'intelligent_batches_created' => $totalChunks,
- 'batch_sizes' => array_map('count', $chunks),
- 'batching_method' => 'size_aware_intelligent',
- 'mysql_packet_limit_safe' => true
- ]);
-
- $allResults = [];
+
+ $this->logger->info(
+ 'Starting intelligent batch processing',
+ [
+ 'total_objects_to_save' => count($objects),
+ 'intelligent_batches_created' => $totalChunks,
+ 'batch_sizes' => array_map('count', $chunks),
+ 'batching_method' => 'size_aware_intelligent',
+ 'mysql_packet_limit_safe' => true,
+ ]
+ );
+
+ $allResults = [];
$processedChunks = 0;
-
- // Accumulate statistics from all chunks
+
+ // Accumulate statistics from all chunks.
$aggregatedStats = [
- 'saved' => [],
+ 'saved' => [],
'updated' => [],
'skipped' => [],
- 'invalid' => []
+ 'invalid' => [],
];
-
- // Process chunks sequentially but with larger batch sizes for better performance
+
+ // Process chunks sequentially but with larger batch sizes for better performance.
foreach ($chunks as $chunkIndex => $chunk) {
- // OPTIMIZATION: Removed debug logging from chunk processing loop
-
+ // OPTIMIZATION: Removed debug logging from chunk processing loop.
try {
+ if (self::PERFORMANCE_OPTIMIZATIONS['disable_rbac'] === true) {
+ $rbacValue = false;
+ } else {
+ $rbacValue = true;
+ }
+
$saveResult = $objectService->saveObjects(
objects: $chunk,
register: $registerId,
schema: null,
- rbac: self::PERFORMANCE_OPTIMIZATIONS['disable_rbac'] ? false : true,
+ rbac: $rbacValue,
multi: self::PERFORMANCE_OPTIMIZATIONS['use_multi'],
validation: !self::PERFORMANCE_OPTIMIZATIONS['disable_validation'],
events: !self::PERFORMANCE_OPTIMIZATIONS['disable_events']
);
-
- // Accumulate statistics from this chunk
- $aggregatedStats['saved'] = array_merge($aggregatedStats['saved'], $saveResult['saved'] ?? []);
+
+ // Accumulate statistics from this chunk.
+ $aggregatedStats['saved'] = array_merge($aggregatedStats['saved'], $saveResult['saved'] ?? []);
$aggregatedStats['updated'] = array_merge($aggregatedStats['updated'], $saveResult['updated'] ?? []);
$aggregatedStats['skipped'] = array_merge($aggregatedStats['skipped'], $saveResult['skipped'] ?? []);
$aggregatedStats['invalid'] = array_merge($aggregatedStats['invalid'], $saveResult['invalid'] ?? []);
-
+
$savedObjects = array_merge(
$saveResult['saved'] ?? [],
$saveResult['updated'] ?? []
);
-
+
$allResults = array_merge($allResults, $savedObjects);
-
+
$processedChunks++;
- $this->logger->info('Processed chunk', [
- 'processed_chunks' => $processedChunks,
- 'total_chunks' => $totalChunks,
- 'progress_percent' => round(($processedChunks / $totalChunks) * 100, 1),
- 'chunk_saved' => count($saveResult['saved'] ?? []),
- 'chunk_updated' => count($saveResult['updated'] ?? []),
- 'chunk_skipped' => count($saveResult['skipped'] ?? []),
- 'chunk_invalid' => count($saveResult['invalid'] ?? [])
- ]);
-
+ $this->logger->info(
+ 'Processed chunk',
+ [
+ 'processed_chunks' => $processedChunks,
+ 'total_chunks' => $totalChunks,
+ 'progress_percent' => round(($processedChunks / $totalChunks) * 100, 1),
+ 'chunk_saved' => count($saveResult['saved'] ?? []),
+ 'chunk_updated' => count($saveResult['updated'] ?? []),
+ 'chunk_skipped' => count($saveResult['skipped'] ?? []),
+ 'chunk_invalid' => count($saveResult['invalid'] ?? []),
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('Error processing chunk', [
- 'chunk_index' => $chunkIndex,
- 'error' => $e->getMessage()
- ]);
- // Continue with other chunks
- }
-
- // Memory cleanup between chunks
- if (self::PERFORMANCE_OPTIMIZATIONS['memory_cleanup']) {
+ $this->logger->error(
+ 'Error processing chunk',
+ [
+ 'chunk_index' => $chunkIndex,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ // Continue with other chunks.
+ }//end try
+
+ // Memory cleanup between chunks.
+ if (self::PERFORMANCE_OPTIMIZATIONS['memory_cleanup'] !== false) {
$this->cleanupMemory();
}
- }
-
- // Store the aggregated result for statistics calculation
+ }//end foreach
+
+ // Store the aggregated result for statistics calculation.
$this->lastSaveResult = $aggregatedStats;
-
- $this->logger->info('Optimized batch processing completed', [
- 'total_objects_processed' => count($allResults),
- 'total_chunks_processed' => $totalChunks,
- 'aggregated_saved' => count($aggregatedStats['saved']),
- 'aggregated_updated' => count($aggregatedStats['updated']),
- 'aggregated_skipped' => count($aggregatedStats['skipped']),
- 'aggregated_invalid' => count($aggregatedStats['invalid'])
- ]);
-
+
+ $this->logger->info(
+ 'Optimized batch processing completed',
+ [
+ 'total_objects_processed' => count($allResults),
+ 'total_chunks_processed' => $totalChunks,
+ 'aggregated_saved' => count($aggregatedStats['saved']),
+ 'aggregated_updated' => count($aggregatedStats['updated']),
+ 'aggregated_skipped' => count($aggregatedStats['skipped']),
+ 'aggregated_invalid' => count($aggregatedStats['invalid']),
+ ]
+ );
+
return $allResults;
- }
+ }//end saveObjectsInParallelBatches()
/**
* Save objects in a single batch (fallback method)
- *
- * @param array $objects Array of objects to save
+ *
+ * @param array $objects Array of objects to save
* @param ObjectService $objectService ObjectService instance
- * @param int $registerId Register ID
+ * @param int $registerId Register ID
+ *
* @return array Array of saved objects
*/
private function saveObjectsInSingleBatch(array $objects, ObjectService $objectService, int $registerId): array
{
- $this->logger->info('Using single batch processing', [
- 'count' => count($objects)
- ]);
-
+ $this->logger->info(
+ 'Using single batch processing',
+ [
+ 'count' => count($objects),
+ ]
+ );
+
+ if (self::PERFORMANCE_OPTIMIZATIONS['disable_rbac'] === true) {
+ $rbacValue = false;
+ } else {
+ $rbacValue = true;
+ }
+
$saveResult = $objectService->saveObjects(
objects: $objects,
register: $registerId,
schema: null,
- rbac: self::PERFORMANCE_OPTIMIZATIONS['disable_rbac'] ? false : true,
+ rbac: $rbacValue,
multi: self::PERFORMANCE_OPTIMIZATIONS['use_multi'],
validation: !self::PERFORMANCE_OPTIMIZATIONS['disable_validation'],
events: !self::PERFORMANCE_OPTIMIZATIONS['disable_events']
);
- // Store the save result for later access to statistics
+ // Store the save result for later access to statistics.
$this->lastSaveResult = $saveResult;
- // Extract saved objects from the new structured return format
+ // Extract saved objects from the new structured return format.
$savedObjects = array_merge(
$saveResult['saved'] ?? [],
$saveResult['updated'] ?? []
);
- // Log detailed results including validation errors
- $this->logger->info('Objects saved successfully', [
- 'saved_count' => count($saveResult['saved'] ?? []),
- 'updated_count' => count($saveResult['updated'] ?? []),
- 'unchanged_count' => count($saveResult['skipped'] ?? []),
- 'invalid_count' => count($saveResult['invalid'] ?? []),
- 'error_count' => count($saveResult['errors'] ?? []),
- 'total_processed' => $saveResult['statistics']['totalProcessed'] ?? 0
- ]);
-
- // Log any validation errors for debugging
- if (!empty($saveResult['invalid'])) {
+ // Log detailed results including validation errors.
+ $this->logger->info(
+ 'Objects saved successfully',
+ [
+ 'saved_count' => count($saveResult['saved'] ?? []),
+ 'updated_count' => count($saveResult['updated'] ?? []),
+ 'unchanged_count' => count($saveResult['skipped'] ?? []),
+ 'invalid_count' => count($saveResult['invalid'] ?? []),
+ 'error_count' => count($saveResult['errors'] ?? []),
+ 'total_processed' => $saveResult['statistics']['totalProcessed'] ?? 0,
+ ]
+ );
+
+ // Log any validation errors for debugging.
+ if (empty($saveResult['invalid']) === false) {
foreach ($saveResult['invalid'] as $invalidItem) {
- $this->logger->warning('Object failed validation during import', [
- 'object_id' => $invalidItem['object']['@self']['id'] ?? 'unknown',
- 'error' => $invalidItem['error'] ?? 'Unknown validation error',
- 'type' => $invalidItem['type'] ?? 'ValidationException'
- ]);
+ $this->logger->warning(
+ 'Object failed validation during import',
+ [
+ 'object_id' => $invalidItem['object']['@self']['id'] ?? 'unknown',
+ 'error' => $invalidItem['error'] ?? 'Unknown validation error',
+ 'type' => $invalidItem['type'] ?? 'ValidationException',
+ ]
+ );
}
}
- // Log details about skipped objects if any
- if (!empty($saveResult['skipped'])) {
- $this->logger->info('Objects skipped during import (no changes detected)', [
- 'skipped_count' => count($saveResult['skipped']),
- 'sample_skipped_ids' => array_slice(
+ // Log details about skipped objects if any.
+ if (empty($saveResult['skipped']) === false) {
+ $this->logger->info(
+ 'Objects skipped during import (no changes detected)',
+ [
+ 'skipped_count' => count($saveResult['skipped']),
+ 'sample_skipped_ids' => array_slice(
array_map(fn($obj) => $obj->getUuid() ?? 'unknown', $saveResult['skipped']),
- 0,
+ 0,
5
- )
- ]);
+ ),
+ ]
+ );
}
- // Return the combined saved and updated objects (maintaining backward compatibility)
+ // Return the combined saved and updated objects (maintaining backward compatibility).
return $savedObjects;
- }
-
-
-
-
-
-
+ }//end saveObjectsInSingleBatch()
/**
* Get ObjectService from container
- *
+ *
* @return ObjectService|null ObjectService instance or null if not available
*/
private function getObjectService(): ?ObjectService
{
- if (!$this->appManager->isInstalled('openregister')) {
+ if ($this->appManager->isInstalled(appId: 'openregister') === false) {
return null;
}
try {
return $this->container->get(ObjectService::class);
} catch (\Exception $e) {
- $this->logger->warning('Failed to get ObjectService', [
- 'error' => $e->getMessage()
- ]);
+ $this->logger->warning(
+ 'Failed to get ObjectService',
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
return null;
}
- }
+ }//end getObjectService()
/**
* Initialize cached configuration values for performance optimization
- *
+ *
* @return void
*/
private function initializeCache(): void
{
if ($this->cachedConfig !== null) {
- return; // Already cached
+ return;
+ // Already cached.
}
- // Get the AMEF register ID from configuration - throw error if missing
+ // Get the AMEF register ID from configuration - throw error if missing.
$amefConfig = $this->settingsService->getAmefConfig();
- if (!isset($amefConfig['register']) || empty($amefConfig['register'])) {
+ if (isset($amefConfig['register']) === false || empty($amefConfig['register']) === true) {
throw new \InvalidArgumentException('AMEF register ID is not configured. Please configure the AMEF register via the admin interface.');
}
- $registerId = (int)$amefConfig['register'];
-
+
+ $registerId = (int) $amefConfig['register'];
+
$this->cachedConfig = [
- 'registerId' => $registerId, // Use AMEF register ID directly
- 'schemaIds' => [
- 'model' => $this->settingsService->getSchemaIdForObjectType('model'),
- 'element' => $this->settingsService->getSchemaIdForObjectType('element'),
- 'relationship' => $this->settingsService->getSchemaIdForObjectType('relationship'),
- 'view' => $this->settingsService->getSchemaIdForObjectType('view'),
- 'organization' => $this->settingsService->getSchemaIdForObjectType('organization'),
- 'property_definition' => $this->settingsService->getSchemaIdForObjectType('property_definition')
- // NOTE: 'property' removed - properties are never root-level AMEF objects, only nested within other elements
- ]
+ 'registerId' => $registerId,
+ // Use AMEF register ID directly.
+ 'schemaIds' => [
+ 'model' => $this->settingsService->getSchemaIdForObjectType('model'),
+ 'element' => $this->settingsService->getSchemaIdForObjectType('element'),
+ 'relationship' => $this->settingsService->getSchemaIdForObjectType('relationship'),
+ 'view' => $this->settingsService->getSchemaIdForObjectType('view'),
+ 'organization' => $this->settingsService->getSchemaIdForObjectType('organization'),
+ 'property_definition' => $this->settingsService->getSchemaIdForObjectType('property_definition'),
+ // NOTE: 'property' removed - properties are never root-level AMEF objects, only nested within other elements.
+ ],
];
- $this->logger->debug('ArchiMateService: Cache initialized', [
- 'registerId' => $this->cachedConfig['registerId'],
- 'schemaIds' => $this->cachedConfig['schemaIds']
- ]);
-
- // Validate that all required schema IDs are configured
+ $this->logger->debug(
+ 'ArchiMateService: Cache initialized',
+ [
+ 'registerId' => $this->cachedConfig['registerId'],
+ 'schemaIds' => $this->cachedConfig['schemaIds'],
+ ]
+ );
+
+ // Validate that all required schema IDs are configured.
$this->validateRequiredConfiguration();
- }
+ }//end initializeCache()
/**
- * Validates that all required configuration is present before import
+ * Validates that all required configuration is present before import.
+ *
+ * @return void
*
- * @throws \RuntimeException If required configuration is missing
+ * @throws \RuntimeException If required configuration is missing.
*/
private function validateRequiredConfiguration(): void
{
- $missingConfig = [];
+ $missingConfig = [];
$requiredSchemaTypes = ['model', 'element', 'relationship', 'view', 'organization', 'property'];
-
- // Check register ID
- if (empty($this->cachedConfig['registerId'])) {
+
+ // Check register ID.
+ if (empty($this->cachedConfig['registerId']) === true) {
$missingConfig[] = 'AMEF Register ID (amef_register)';
}
-
- // Check all required schema IDs
+
+ // Check all required schema IDs.
foreach ($requiredSchemaTypes as $schemaType) {
$schemaId = $this->cachedConfig['schemaIds'][$schemaType] ?? null;
if ($schemaId === null) {
- $configKey = "amef_{$schemaType}_schema";
+ $configKey = "amef_{$schemaType}_schema";
$missingConfig[] = "Schema ID for {$schemaType} ({$configKey})";
}
}
-
- // If any configuration is missing, throw detailed error
- if (!empty($missingConfig)) {
- $this->logger->error('ArchiMateService: Missing required configuration', [
- 'missing_config' => $missingConfig,
- 'current_config' => [
- 'registerId' => $this->cachedConfig['registerId'],
- 'schemaIds' => $this->cachedConfig['schemaIds']
- ]
- ]);
-
- $errorMessage = 'ArchiMate import cannot proceed due to missing configuration:' . "\n\n";
+
+ // If any configuration is missing, throw detailed error.
+ if (empty($missingConfig) === false) {
+ $this->logger->error(
+ 'ArchiMateService: Missing required configuration',
+ [
+ 'missing_config' => $missingConfig,
+ 'current_config' => [
+ 'registerId' => $this->cachedConfig['registerId'],
+ 'schemaIds' => $this->cachedConfig['schemaIds'],
+ ],
+ ]
+ );
+
+ $errorMessage = 'ArchiMate import cannot proceed due to missing configuration:'."\n\n";
$errorMessage .= "Missing configuration:\n";
foreach ($missingConfig as $item) {
$errorMessage .= "- {$item}\n";
}
+
$errorMessage .= "\nPlease configure the AMEF register and all required schema IDs in the SoftwareCatalog settings before importing.";
$errorMessage .= "\nYou can use the auto-configuration feature or set them manually via the admin interface.";
-
+
throw new \RuntimeException($errorMessage);
- }
-
- $this->logger->info('ArchiMateService: Configuration validation passed', [
- 'registerId' => $this->cachedConfig['registerId'],
- 'configuredSchemas' => count(array_filter($this->cachedConfig['schemaIds']))
- ]);
- }
+ }//end if
+
+ $this->logger->info(
+ 'ArchiMateService: Configuration validation passed',
+ [
+ 'registerId' => $this->cachedConfig['registerId'],
+ 'configuredSchemas' => count(array_filter($this->cachedConfig['schemaIds'])),
+ ]
+ );
+ }//end validateRequiredConfiguration()
/**
* Log current memory usage for performance monitoring
- *
+ *
* @param string $stage Description of the current processing stage
+ *
* @return void
*/
private function logMemoryUsage(string $stage): void
{
- // Check if debug logging is available (Nextcloud logger doesn't have isDebug method)
+ // Check if debug logging is available (Nextcloud logger doesn't have isDebug method).
$memoryUsage = memory_get_usage(true);
- $memoryPeak = memory_get_peak_usage(true);
+ $memoryPeak = memory_get_peak_usage(true);
$memoryLimit = ini_get('memory_limit');
-
- $this->logger->debug("Memory usage at: {$stage}", [
- 'current_mb' => round($memoryUsage / 1024 / 1024, 2),
- 'peak_mb' => round($memoryPeak / 1024 / 1024, 2),
- 'limit' => $memoryLimit
- ]);
- }
+
+ $this->logger->debug(
+ "Memory usage at: {$stage}",
+ [
+ 'current_mb' => round($memoryUsage / 1024 / 1024, 2),
+ 'peak_mb' => round($memoryPeak / 1024 / 1024, 2),
+ 'limit' => $memoryLimit,
+ ]
+ );
+ }//end logMemoryUsage()
/**
* Clean up memory by forcing garbage collection
- *
+ *
* @return void
*/
private function cleanupMemory(): void
{
- if (function_exists('gc_collect_cycles')) {
+ if (function_exists('gc_collect_cycles') === true) {
$cycles = gc_collect_cycles();
- // Use PSR-3 standard logging instead of isDebug() check
- $this->logger->debug('Garbage collection completed', [
- 'cycles_collected' => $cycles
- ]);
+ // Use PSR-3 standard logging instead of isDebug() check.
+ $this->logger->debug(
+ 'Garbage collection completed',
+ [
+ 'cycles_collected' => $cycles,
+ ]
+ );
}
- }
-
-
+ }//end cleanupMemory()
/**
* NOTE: Removed deprecated methods getArchiMateRegisterId() and getArchiMateModelSchemaId()
@@ -1280,35 +1424,39 @@ private function cleanupMemory(): void
/**
* Get schema ID for a section
- *
+ *
* @param string $section Section name
+ *
* @return int Schema ID
*/
private function getSchemaIdForSection(string $section): int
{
- // Map section names to object types for SettingsService
+ // Map section names to object types for SettingsService.
$objectTypeMapping = [
- 'elements' => 'element',
- 'relationships' => 'relationship',
- 'views' => 'view',
- 'organizations' => 'organization',
- 'property_definitions' => 'property_definition'
+ 'elements' => 'element',
+ 'relationships' => 'relationship',
+ 'views' => 'view',
+ 'organizations' => 'organization',
+ 'property_definitions' => 'property_definition',
];
-
+
$objectType = $objectTypeMapping[$section] ?? $section;
- $schemaId = $this->settingsService->getSchemaIdForObjectType($objectType);
-
- // Ensure schema ID is configured - no hardcoded fallbacks
+ $schemaId = $this->settingsService->getSchemaIdForObjectType($objectType);
+
+ // Ensure schema ID is configured - no hardcoded fallbacks.
if ($schemaId === null) {
- throw new \RuntimeException("Schema ID for section '{$section}' is not configured. Please configure all AMEF schema IDs via the admin interface. Expected object type: '{$objectType}'");
+ $configMsg = "Schema ID for section '{$section}' is not configured.";
+ $helpMsg = "Please configure all AMEF schema IDs via the admin interface.";
+ $typeMsg = "Expected object type: '{$objectType}'";
+ throw new \RuntimeException("{$configMsg} {$helpMsg} {$typeMsg}");
}
-
+
return $schemaId;
- }
+ }//end getSchemaIdForSection()
/**
* Test round-trip functionality
- *
+ *
* @return array Test results
*/
public function testRoundTrip(): array
@@ -1316,59 +1464,63 @@ public function testRoundTrip(): array
$this->logger->info('Testing ArchiMate round-trip functionality');
try {
- // Create test XML
+ // Create test XML.
$testXml = $this->createTestArchiMateXml();
-
- // Import
- $importResult = $this->importArchiMateFileFromPath([
- 'file_path' => $this->createTempFile($testXml)
- ]);
-
- if (!$importResult['success']) {
+
+ // Import.
+ $importResult = $this->importArchiMateFileFromPath(
+ options: [
+ 'file_path' => $this->createTempFile(content: $testXml),
+ ]
+ );
+
+ if ($importResult['success'] === false) {
return [
'success' => false,
- 'error' => 'Import failed: ' . $importResult['error']
+ 'error' => 'Import failed: '.$importResult['error'],
];
}
- // Export
+ // Export.
$exportResult = $this->exportToArchiMate();
-
- if (!$exportResult['success']) {
+
+ if ($exportResult['success'] === false) {
return [
'success' => false,
- 'error' => 'Export failed: ' . $exportResult['error']
+ 'error' => 'Export failed: '.$exportResult['error'],
];
}
- // Compare (simplified comparison)
+ // Compare (simplified comparison).
$importedCount = $importResult['imported_count'];
$exportedCount = $exportResult['exported_count'];
-
+
$success = $importedCount === $exportedCount;
return [
- 'success' => $success,
- 'imported_count' => $importedCount,
- 'exported_count' => $exportedCount,
- 'round_trip_successful' => $success
+ 'success' => $success,
+ 'imported_count' => $importedCount,
+ 'exported_count' => $exportedCount,
+ 'round_trip_successful' => $success,
];
-
} catch (\Exception $e) {
- $this->logger->error('Round-trip test failed', [
- 'error' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Round-trip test failed',
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
return [
'success' => false,
- 'error' => $e->getMessage()
+ 'error' => $e->getMessage(),
];
- }
- }
+ }//end try
+ }//end testRoundTrip()
/**
* Create test ArchiMate XML
- *
+ *
* @return string Test XML content
*/
private function createTestArchiMateXml(): string
@@ -1389,12 +1541,13 @@ private function createTestArchiMateXml(): string
';
- }
+ }//end createTestArchiMateXml()
/**
* Create temporary file with content
- *
+ *
* @param string $content File content
+ *
* @return string Temporary file path
*/
private function createTempFile(string $content): string
@@ -1402,48 +1555,51 @@ private function createTempFile(string $content): string
$tempFile = tempnam(sys_get_temp_dir(), 'archimate_test_');
file_put_contents($tempFile, $content);
return $tempFile;
- }
+ }//end createTempFile()
/**
* Get AMEF configuration from app config
- *
+ *
* @return array AMEF configuration
*/
public function getAmefConfig(): array
{
$this->logger->info('Getting AMEF configuration');
-
+
try {
- // Get configuration from app config using the correct method
- $config = $this->config->getValueString('softwarecatalog', 'amef_config', '{}');
+ // Get configuration from app config using the correct method.
+ $config = $this->config->getValueString('softwarecatalog', 'amef_config', '{}');
$decoded = json_decode($config, true);
-
- if (!is_array($decoded)) {
- // Fallback to individual config values for backward compatibility
+
+ if (is_array($decoded) === false) {
+ // Fallback to individual config values for backward compatibility.
$decoded = [
- 'register_id' => $this->config->getValueString('softwarecatalog', 'amef_register', ''),
- 'model_schema_id' => $this->config->getValueString('softwarecatalog', 'amef_model_schema', ''),
- 'elements_schema' => $this->config->getValueString('softwarecatalog', 'amef_elements_schema', ''),
- 'relationships_schema' => $this->config->getValueString('softwarecatalog', 'amef_relationships_schema', ''),
- 'views_schema' => $this->config->getValueString('softwarecatalog', 'amef_views_schema', ''),
- 'organizations_schema' => $this->config->getValueString('softwarecatalog', 'amef_organizations_schema', ''),
- 'folders_schema' => $this->config->getValueString('softwarecatalog', 'amef_folders_schema', ''),
- 'property_definitions_schema' => $this->config->getValueString('softwarecatalog', 'amef_property_definitions_schema', '')
+ 'register_id' => $this->config->getValueString('softwarecatalog', 'amef_register', ''),
+ 'model_schema_id' => $this->config->getValueString('softwarecatalog', 'amef_model_schema', ''),
+ 'elements_schema' => $this->config->getValueString('softwarecatalog', 'amef_elements_schema', ''),
+ 'relationships_schema' => $this->config->getValueString('softwarecatalog', 'amef_relationships_schema', ''),
+ 'views_schema' => $this->config->getValueString('softwarecatalog', 'amef_views_schema', ''),
+ 'organizations_schema' => $this->config->getValueString('softwarecatalog', 'amef_organizations_schema', ''),
+ 'folders_schema' => $this->config->getValueString('softwarecatalog', 'amef_folders_schema', ''),
+ 'property_definitions_schema' => $this->config->getValueString('softwarecatalog', 'amef_property_definitions_schema', ''),
];
}
-
+
return $decoded;
} catch (\Exception $e) {
- $this->logger->error('Failed to get AMEF configuration', [
- 'error' => $e->getMessage()
- ]);
-
+ $this->logger->error(
+ 'Failed to get AMEF configuration',
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
+
return [
'success' => false,
- 'error' => $e->getMessage()
+ 'error' => $e->getMessage(),
];
- }
- }
+ }//end try
+ }//end getAmefConfig()
/**
* Get Voorzieningen configuration directly from IAppConfig
@@ -1452,20 +1608,20 @@ public function getAmefConfig(): array
*/
private function getVoorzieningenConfig(): array
{
- $config = $this->config->getValueString('softwarecatalog', 'voorzieningen_config', '{}');
+ $config = $this->config->getValueString('softwarecatalog', 'voorzieningen_config', '{}');
$decoded = json_decode($config, true);
-
- if (!is_array($decoded)) {
- // Fallback to individual config values for backward compatibility
+
+ if (is_array($decoded) === false) {
+ // Fallback to individual config values for backward compatibility.
$decoded = [
- 'register' => $this->config->getValueString('softwarecatalog', 'voorzieningen_register', ''),
- 'organisatie_schema' => $this->config->getValueString('softwarecatalog', 'voorzieningen_organisatie_schema', ''),
+ 'register' => $this->config->getValueString('softwarecatalog', 'voorzieningen_register', ''),
+ 'organisatie_schema' => $this->config->getValueString('softwarecatalog', 'voorzieningen_organisatie_schema', ''),
'contactpersoon_schema' => $this->config->getValueString('softwarecatalog', 'voorzieningen_contactpersoon_schema', ''),
];
}
-
+
return $decoded;
- }
+ }//end getVoorzieningenConfig()
/**
* Get the current status of ArchiMate operations
@@ -1475,85 +1631,93 @@ private function getVoorzieningenConfig(): array
public function getArchiMateStatus(): array
{
$this->logger->info('Getting ArchiMate status');
-
+
try {
- // Get basic status information
+ // Get basic status information.
$objectService = $this->getObjectService();
- if (!$objectService) {
+ if ($objectService === null) {
return [
'success' => false,
- 'error' => 'ObjectService not available'
+ 'error' => 'ObjectService not available',
];
}
-
- // Get object counts using the proper getter methods
- $elementObjects = $this->getElementObjects();
+
+ // Get object counts using the proper getter methods.
+ $elementObjects = $this->getElementObjects();
$organizationObjects = $this->getOrganizationObjects();
- $viewObjects = $this->getViewObjects();
+ $viewObjects = $this->getViewObjects();
$relationshipObjects = $this->getRelationshipObjects();
- $modelObjects = $this->getModelObjects();
- $propertyObjects = $this->getPropertyObjects();
+ $modelObjects = $this->getModelObjects();
+ $propertyObjects = $this->getPropertyObjects();
$propertyDefinitionObjects = $this->getPropertyDefinitionObjects();
-
- // Calculate totals
- $totalCount = count($elementObjects) + count($organizationObjects) +
- count($viewObjects) + count($relationshipObjects) +
- count($modelObjects) + count($propertyObjects) +
- count($propertyDefinitionObjects);
-
+
+ // Calculate totals.
+ $elemCount = count($elementObjects) + count($organizationObjects);
+ $viewRelCount = count($viewObjects) + count($relationshipObjects);
+ $modelPropCount = count($modelObjects) + count($propertyObjects);
+ $totalCount = $elemCount + $viewRelCount + $modelPropCount + count($propertyDefinitionObjects);
+
return [
- 'success' => true,
- 'status' => 'ready',
- 'model_count' => count($modelObjects),
- 'total_objects' => $totalCount,
- 'element_count' => count($elementObjects),
- 'organization_count' => count($organizationObjects),
- 'view_count' => count($viewObjects),
- 'relationship_count' => count($relationshipObjects),
- 'property_count' => count($propertyObjects),
- 'property_definition_count' => count($propertyDefinitionObjects)
+ 'success' => true,
+ 'status' => 'ready',
+ 'model_count' => count($modelObjects),
+ 'total_objects' => $totalCount,
+ 'element_count' => count($elementObjects),
+ 'organization_count' => count($organizationObjects),
+ 'view_count' => count($viewObjects),
+ 'relationship_count' => count($relationshipObjects),
+ 'property_count' => count($propertyObjects),
+ 'property_definition_count' => count($propertyDefinitionObjects),
];
} catch (\Exception $e) {
- $this->logger->error('Failed to get ArchiMate status', [
- 'error' => $e->getMessage()
- ]);
-
+ $this->logger->error(
+ 'Failed to get ArchiMate status',
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
+
return [
'success' => false,
- 'error' => $e->getMessage()
+ 'error' => $e->getMessage(),
];
- }
- }
+ }//end try
+ }//end getArchiMateStatus()
/**
* Get AMEF register ID from configuration
- *
+ *
* @return int|null The register ID or null if not configured
*/
private function getAmefRegisterId(): ?int
{
- // Retrieve AMEF configuration (use SettingsService for consistency)
+ // Retrieve AMEF configuration (use SettingsService for consistency).
$amefConfig = $this->settingsService->getAmefConfig();
- // Try JSON config keys first: support both 'register_id' and 'register'
- $rawRegisterId = $amefConfig['register_id']
- ?? $amefConfig['register']
- ?? null;
+ // Try JSON config keys first: support both 'register_id' and 'register'.
+ $rawRegisterId = $amefConfig['register_id'] ?? $amefConfig['register'] ?? null;
- // Fallback to legacy individual app config keys if not present in JSON
+ // Fallback to legacy individual app config keys if not present in JSON.
if ($rawRegisterId === null || $rawRegisterId === '') {
- $rawRegisterId = $this->config->getValueString('softwarecatalog', 'amef_register', '')
- ?: $this->config->getValueString('softwarecatalog', 'amef_register_id', '');
+ if ($this->config->getValueString('softwarecatalog', 'amef_register', '') !== '') {
+ $rawRegisterId = $this->config->getValueString('softwarecatalog', 'amef_register', '');
+ } else {
+ $rawRegisterId = $this->config->getValueString('softwarecatalog', 'amef_register_id', '');
+ }
}
- // Validate and normalize to positive int
- if ($rawRegisterId !== null && $rawRegisterId !== '' && is_numeric((string) $rawRegisterId)) {
+ // Validate and normalize to positive int.
+ if ($rawRegisterId !== null && $rawRegisterId !== '' && is_numeric((string) $rawRegisterId) === true) {
$registerId = (int) $rawRegisterId;
- return $registerId > 0 ? $registerId : null;
+ if ($registerId > 0) {
+ return $registerId;
+ } else {
+ return null;
+ }
}
return null;
- }
+ }//end getAmefRegisterId()
/**
* Get AMEF schema ID for a specific ArchiMate type via SettingsService
@@ -1561,325 +1725,374 @@ private function getAmefRegisterId(): ?int
* This method retrieves the schema ID for a given ArchiMate type from SettingsService.
*
* @param string $archiMateType The ArchiMate type (e.g., 'element', 'organization', 'relationship')
+ *
* @return int|null The schema ID for the given type or null if not configured
*/
private function getAmefSchemaIdForType(string $archiMateType): ?int
{
- // Use SettingsService to get schema ID
+ // Use SettingsService to get schema ID.
$schemaId = $this->settingsService->getSchemaIdForObjectType($archiMateType);
return $schemaId;
- }
+ }//end getAmefSchemaIdForType()
/**
* Get element objects from the database
- *
+ *
* @param array $query Query parameters
+ *
* @return array Array of element objects
*/
- public function getElementObjects(array $query = []): array
+ public function getElementObjects(array $query=[]): array
{
- return $this->getObjectsWithPagination('element', $query);
- }
+ return $this->getObjectsWithPagination(schemaType: 'element', query: $query);
+ }//end getElementObjects()
/**
* Get organization objects from the database
- *
+ *
* @param array $query Query parameters
+ *
* @return array Array of organization objects
*/
- public function getOrganizationObjects(array $query = []): array
+ public function getOrganizationObjects(array $query=[]): array
{
- return $this->getObjectsWithPagination('organization', $query);
- }
+ return $this->getObjectsWithPagination(schemaType: 'organization', query: $query);
+ }//end getOrganizationObjects()
/**
* Get view objects from the database
- *
+ *
* @param array $query Query parameters
+ *
* @return array Array of view objects
*/
- public function getViewObjects(array $query = []): array
+ public function getViewObjects(array $query=[]): array
{
- return $this->getObjectsWithPagination('view', $query);
- }
+ return $this->getObjectsWithPagination(schemaType: 'view', query: $query);
+ }//end getViewObjects()
/**
* Get relationship objects from the database
- *
+ *
* @param array $query Query parameters
+ *
* @return array Array of relationship objects
*/
- public function getRelationshipObjects(array $query = []): array
+ public function getRelationshipObjects(array $query=[]): array
{
- return $this->getObjectsWithPagination('relationship', $query);
- }
+ return $this->getObjectsWithPagination(schemaType: 'relationship', query: $query);
+ }//end getRelationshipObjects()
/**
* Get model objects from the database
- *
+ *
* @param array $query Query parameters
+ *
* @return array Array of model objects
*/
- public function getModelObjects(array $query = []): array
+ public function getModelObjects(array $query=[]): array
{
- return $this->getObjectsWithPagination('model', $query);
- }
+ return $this->getObjectsWithPagination(schemaType: 'model', query: $query);
+ }//end getModelObjects()
/**
* Get property objects from the database
- *
+ *
* @param array $query Query parameters
+ *
* @return array Array of property objects
*/
- public function getPropertyObjects(array $query = []): array
+ public function getPropertyObjects(array $query=[]): array
{
- return $this->getObjectsWithPagination('property', $query);
- }
+ return $this->getObjectsWithPagination(schemaType: 'property', query: $query);
+ }//end getPropertyObjects()
/**
* Get property definition objects from the database
- *
+ *
* @param array $query Query parameters
+ *
* @return array Array of property definition objects
*/
- public function getPropertyDefinitionObjects(array $query = []): array
+ public function getPropertyDefinitionObjects(array $query=[]): array
{
- return $this->getObjectsWithPagination('property_definition', $query);
- }
+ return $this->getObjectsWithPagination(schemaType: 'property_definition', query: $query);
+ }//end getPropertyDefinitionObjects()
/**
* Get objects with pagination support for a specific schema type
*
* @param string $schemaType The schema type to retrieve objects for
- * @param array $query Optional query criteria and pagination parameters
+ * @param array $query Optional query criteria and pagination parameters
+ *
* @return array Array of objects matching the criteria
*/
- private function getObjectsWithPagination(string $schemaType, array $query = []): array
+ private function getObjectsWithPagination(string $schemaType, array $query=[]): array
{
try {
$objectService = $this->getObjectService();
- if (!$objectService) {
+ if ($objectService === null) {
$this->logger->error("ArchiMateService: ObjectService not available for {$schemaType} objects retrieval");
return [];
}
- // AMEF object types use a single register ID, not per-type register IDs
- // Check if this is an AMEF object type
+ // AMEF object types use a single register ID, not per-type register IDs.
+ // Check if this is an AMEF object type.
$amefObjectTypes = ['model', 'element', 'relationship', 'view', 'property_definition', 'organization', 'property'];
- $isAmefType = in_array($schemaType, $amefObjectTypes, true);
-
- // Use AMEF register ID for AMEF types, otherwise use per-type register ID
- if ($isAmefType) {
+ $isAmefType = in_array($schemaType, $amefObjectTypes, true) === true;
+
+ // Use AMEF register ID for AMEF types, otherwise use per-type register ID.
+ if (empty($isAmefType) === false) {
$registerId = $this->getAmefRegisterId();
} else {
$registerId = $this->settingsService->getRegisterIdForObjectType($schemaType);
}
-
+
$schemaId = $this->settingsService->getSchemaIdForObjectType($schemaType);
-
- if (!$registerId || !$schemaId) {
- $errorMessage = $isAmefType
- ? "ArchiMateService: AMEF register or {$schemaType} schema not configured"
- : "ArchiMateService: Register or {$schemaType} schema not configured";
- $this->logger->error($errorMessage, [
- 'registerId' => $registerId,
- 'schemaId' => $schemaId,
- 'isAmefType' => $isAmefType,
- 'schemaType' => $schemaType
- ]);
+
+ if ($registerId === null || $schemaId === false) {
+ if ($isAmefType === true) {
+ $errorMessage = "ArchiMateService: AMEF register or {$schemaType} schema not configured";
+ } else {
+ $errorMessage = "ArchiMateService: Register or {$schemaType} schema not configured";
+ }
+
+ $this->logger->error(
+ $errorMessage,
+ [
+ 'registerId' => $registerId,
+ 'schemaId' => $schemaId,
+ 'isAmefType' => $isAmefType,
+ 'schemaType' => $schemaType,
+ ]
+ );
return [];
}
- // Extract pagination parameters
- $limit = $query['limit'] ?? 1000; // Default limit for large datasets
- $offset = $query['offset'] ?? 0;
+ // Extract pagination parameters.
+ $limit = $query['limit'] ?? 1000;
+ // Default limit for large datasets.
+ $offset = $query['offset'] ?? 0;
$usePagination = $query['use_pagination'] ?? false;
-
- // Remove pagination parameters from query
+
+ // Remove pagination parameters from query.
unset($query['limit'], $query['offset'], $query['use_pagination']);
- // Build base query for register and schema
+ // Build base query for register and schema.
$baseQuery = [
'@self' => [
'register' => (int) $registerId,
- 'schema' => (int) $schemaId
- ]
+ 'schema' => (int) $schemaId,
+ ],
];
-
- // Merge with provided query
+
+ // Merge with provided query.
$finalQuery = array_merge_recursive($baseQuery, $query);
-
- // Add pagination if requested
- if ($usePagination && $limit > 0) {
+
+ // Add pagination if requested.
+ if ($usePagination !== false && $limit > 0) {
$finalQuery['@pagination'] = [
- 'limit' => (int) $limit,
- 'offset' => (int) $offset
+ 'limit' => (int) $limit,
+ 'offset' => (int) $offset,
];
}
-
- $this->logger->debug("ArchiMateService: Retrieving {$schemaType} objects", [
- 'register' => $registerId,
- 'schema' => $schemaId,
- 'query' => $finalQuery,
- 'pagination' => $usePagination ? ['limit' => $limit, 'offset' => $offset] : 'disabled'
- ]);
-
- // Use searchObjects method for filtering
+
+ if ($usePagination === true) {
+ $paginationValue = ['limit' => $limit, 'offset' => $offset];
+ } else {
+ $paginationValue = 'disabled';
+ }
+
+ $this->logger->debug(
+ "ArchiMateService: Retrieving {$schemaType} objects",
+ [
+ 'register' => $registerId,
+ 'schema' => $schemaId,
+ 'query' => $finalQuery,
+ 'pagination' => $paginationValue,
+ ]
+ );
+
+ // Use searchObjects method for filtering.
$objects = $objectService->searchObjects($finalQuery);
-
- $this->logger->debug("ArchiMateService: Retrieved {$schemaType} objects", [
- 'register' => $registerId,
- 'schema' => $schemaId,
- 'count' => count($objects),
- 'pagination' => $usePagination ? ['limit' => $limit, 'offset' => $offset] : 'disabled'
- ]);
+
+ if ($usePagination === true) {
+ $paginationValue = ['limit' => $limit, 'offset' => $offset];
+ } else {
+ $paginationValue = 'disabled';
+ }
+
+ $this->logger->debug(
+ "ArchiMateService: Retrieved {$schemaType} objects",
+ [
+ 'register' => $registerId,
+ 'schema' => $schemaId,
+ 'count' => count($objects),
+ 'pagination' => $paginationValue,
+ ]
+ );
return $objects;
} catch (\Exception $e) {
- $this->logger->error("ArchiMateService: Failed to retrieve {$schemaType} objects", [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
-
+ $this->logger->error(
+ "ArchiMateService: Failed to retrieve {$schemaType} objects",
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+
return [];
- }
- }
+ }//end try
+ }//end getObjectsWithPagination()
/**
* Check if import is in progress
- *
+ *
* @return bool True if import is in progress
*/
public function isImportInProgress(): bool
{
- // For now, return false as we haven't implemented status tracking yet
+ // For now, return false as we haven't implemented status tracking yet.
return false;
- }
+ }//end isImportInProgress()
/**
* Check if export is in progress
- *
+ *
* @return bool True if export is in progress
*/
public function isExportInProgress(): bool
{
- // For now, return false as we haven't implemented status tracking yet
+ // For now, return false as we haven't implemented status tracking yet.
return false;
- }
+ }//end isExportInProgress()
/**
* Check if any operation is in progress
- *
+ *
* @return bool True if any operation is in progress
*/
public function isOperationInProgress(): bool
{
return $this->isImportInProgress() || $this->isExportInProgress();
- }
+ }//end isOperationInProgress()
/**
* Create intelligent batches based on object size to prevent MySQL packet size issues
- *
+ *
* This method analyzes object sizes and creates batches that stay under the MySQL
* max_allowed_packet limit while maintaining reasonable performance.
- *
+ *
* @param array $objects Array of objects to batch
+ *
* @return array Array of batches, each containing objects that fit within size limits
*/
private function createIntelligentBatches(array $objects): array
{
$maxBatchSizeBytes = self::PERFORMANCE_OPTIMIZATIONS['max_batch_size_bytes'];
- $minBatchSize = self::PERFORMANCE_OPTIMIZATIONS['min_batch_size'];
- $sampleSize = self::PERFORMANCE_OPTIMIZATIONS['size_estimation_sample'];
-
- if (empty($objects)) {
+ $minBatchSize = self::PERFORMANCE_OPTIMIZATIONS['min_batch_size'];
+ $sampleSize = self::PERFORMANCE_OPTIMIZATIONS['size_estimation_sample'];
+
+ if (empty($objects) === true) {
return [];
}
-
- // Estimate average object size by sampling
- $avgObjectSize = $this->estimateAverageObjectSize($objects, $sampleSize);
-
- // Calculate optimal batch size based on object size
+
+ // Estimate average object size by sampling.
+ $avgObjectSize = $this->estimateAverageObjectSize(objects: $objects, sampleSize: $sampleSize);
+
+ // Calculate optimal batch size based on object size.
$optimalBatchSize = max($minBatchSize, intval($maxBatchSizeBytes / $avgObjectSize));
-
- $this->logger->info('Intelligent batch sizing analysis', [
- 'total_objects' => count($objects),
- 'estimated_avg_object_size_bytes' => $avgObjectSize,
- 'max_batch_size_bytes' => $maxBatchSizeBytes,
- 'calculated_optimal_batch_size' => $optimalBatchSize,
- 'min_batch_size_enforced' => $minBatchSize
- ]);
-
- // Create batches with size awareness
- $batches = [];
- $currentBatch = [];
+
+ $this->logger->info(
+ 'Intelligent batch sizing analysis',
+ [
+ 'total_objects' => count($objects),
+ 'estimated_avg_object_size_bytes' => $avgObjectSize,
+ 'max_batch_size_bytes' => $maxBatchSizeBytes,
+ 'calculated_optimal_batch_size' => $optimalBatchSize,
+ 'min_batch_size_enforced' => $minBatchSize,
+ ]
+ );
+
+ // Create batches with size awareness.
+ $batches = [];
+ $currentBatch = [];
$currentBatchSize = 0;
-
+
foreach ($objects as $object) {
- $objectSize = $this->estimateObjectSize($object);
-
- // Check if adding this object would exceed the batch size limit
- if (!empty($currentBatch) && ($currentBatchSize + $objectSize) > $maxBatchSizeBytes) {
- // Current batch is full, save it and start a new one
- $batches[] = $currentBatch;
- $currentBatch = [$object];
+ $objectSize = $this->estimateObjectSize(object: $object);
+
+ // Check if adding this object would exceed the batch size limit.
+ if (empty($currentBatch) === false && ($currentBatchSize + $objectSize) > $maxBatchSizeBytes) {
+ // Current batch is full, save it and start a new one.
+ $batches[] = $currentBatch;
+ $currentBatch = [$object];
$currentBatchSize = $objectSize;
} else {
- // Add object to current batch
- $currentBatch[] = $object;
+ // Add object to current batch.
+ $currentBatch[] = $object;
$currentBatchSize += $objectSize;
}
-
- // Safety check: if a single object is larger than max batch size,
- // create a batch with just that object
+
+ // Safety check: if a single object is larger than max batch size,.
+ // create a batch with just that object.
if (count($currentBatch) === 1 && $objectSize > $maxBatchSizeBytes) {
- $this->logger->warning('Very large object detected, creating single-object batch', [
- 'object_id' => $object['@self']['id'] ?? 'unknown',
- 'object_size_bytes' => $objectSize,
- 'max_batch_size_bytes' => $maxBatchSizeBytes
- ]);
- $batches[] = $currentBatch;
- $currentBatch = [];
+ $this->logger->warning(
+ 'Very large object detected, creating single-object batch',
+ [
+ 'object_id' => $object['@self']['id'] ?? 'unknown',
+ 'object_size_bytes' => $objectSize,
+ 'max_batch_size_bytes' => $maxBatchSizeBytes,
+ ]
+ );
+ $batches[] = $currentBatch;
+ $currentBatch = [];
$currentBatchSize = 0;
}
- }
-
- // Add the last batch if it has objects
- if (!empty($currentBatch)) {
+ }//end foreach
+
+ // Add the last batch if it has objects.
+ if (empty($currentBatch) === false) {
$batches[] = $currentBatch;
}
-
- $this->logger->info('Intelligent batching completed', [
- 'total_objects' => count($objects),
- 'total_batches_created' => count($batches),
- 'batch_sizes' => array_map('count', $batches),
- 'estimated_batch_sizes_bytes' => array_map(fn($batch) => array_sum(array_map([$this, 'estimateObjectSize'], $batch)), $batches)
- ]);
-
+
+ $this->logger->info(
+ 'Intelligent batching completed',
+ [
+ 'total_objects' => count($objects),
+ 'total_batches_created' => count($batches),
+ 'batch_sizes' => array_map('count', $batches),
+ 'estimated_batch_sizes_bytes' => array_map(fn($batch) => array_sum(array_map([$this, 'estimateObjectSize'], $batch)), $batches),
+ ]
+ );
+
return $batches;
- }
+ }//end createIntelligentBatches()
/**
* Estimate the average size of objects by sampling
- *
- * @param array $objects Array of objects to sample
- * @param int $sampleSize Number of objects to sample for size estimation
+ *
+ * @param array $objects Array of objects to sample
+ * @param int $sampleSize Number of objects to sample for size estimation
+ *
* @return int Estimated average object size in bytes
*/
private function estimateAverageObjectSize(array $objects, int $sampleSize): int
{
$totalObjects = count($objects);
if ($totalObjects === 0) {
- return 1000; // Default fallback size
+ return 1000;
+ // Default fallback size.
}
-
- // Sample evenly distributed objects
+
+ // Sample evenly distributed objects.
$sampleIndices = [];
if ($totalObjects <= $sampleSize) {
- // Use all objects if we have fewer than sample size
+ // Use all objects if we have fewer than sample size.
$sampleIndices = range(0, $totalObjects - 1);
} else {
- // Sample evenly across the array
+ // Sample evenly across the array.
$step = max(1, intval($totalObjects / $sampleSize));
for ($i = 0; $i < $totalObjects; $i += $step) {
$sampleIndices[] = $i;
@@ -1888,553 +2101,622 @@ private function estimateAverageObjectSize(array $objects, int $sampleSize): int
}
}
}
-
- // Calculate sizes of sampled objects
+
+ // Calculate sizes of sampled objects.
$totalSampleSize = 0;
foreach ($sampleIndices as $index) {
- $totalSampleSize += $this->estimateObjectSize($objects[$index]);
+ $totalSampleSize += $this->estimateObjectSize(object: $objects[$index]);
}
-
+
$averageSize = intval($totalSampleSize / count($sampleIndices));
-
- $this->logger->debug('Object size estimation completed', [
- 'total_objects' => $totalObjects,
- 'sampled_objects' => count($sampleIndices),
- 'total_sample_size_bytes' => $totalSampleSize,
- 'estimated_average_size_bytes' => $averageSize
- ]);
-
- return max(1000, $averageSize); // Minimum 1KB per object
- }
+
+ $this->logger->debug(
+ 'Object size estimation completed',
+ [
+ 'total_objects' => $totalObjects,
+ 'sampled_objects' => count($sampleIndices),
+ 'total_sample_size_bytes' => $totalSampleSize,
+ 'estimated_average_size_bytes' => $averageSize,
+ ]
+ );
+
+ return max(1000, $averageSize);
+ // Minimum 1KB per object.
+ }//end estimateAverageObjectSize()
/**
* Estimate the serialized size of an object for batching purposes
- *
+ *
* @param array $object The object to estimate size for
+ *
* @return int Estimated size in bytes
*/
private function estimateObjectSize(array $object): int
{
- // Quick estimation based on JSON serialization
- // This includes overhead for SQL parameters and structure
+ // Quick estimation based on JSON serialization.
+ // This includes overhead for SQL parameters and structure.
$jsonSize = strlen(json_encode($object));
-
- // Add overhead for SQL INSERT statement structure
- // Each object becomes multiple parameters in a bulk INSERT
- $sqlOverhead = 500; // Estimated overhead per object in SQL
-
+
+ // Add overhead for SQL INSERT statement structure.
+ // Each object becomes multiple parameters in a bulk INSERT.
+ $sqlOverhead = 500;
+ // Estimated overhead per object in SQL.
return $jsonSize + $sqlOverhead;
- }
+ }//end estimateObjectSize()
/**
* Calculate detailed object statistics for import operations
- *
+ *
* @param array $normalizedData Normalized ArchiMate data
- * @param array $savedObjects Objects that were saved to database
+ * @param array $savedObjects Objects that were saved to database
+ *
* @return array Comprehensive statistics
*/
private function calculateObjectStatistics(array $normalizedData, array $savedObjects): array
{
- // Initialize statistics structure
+ // Initialize statistics structure.
$statistics = [
- 'elements' => ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => []],
- 'organizations' => ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => []],
- 'relationships' => ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => []],
- 'views' => ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => []],
- 'property_definitions' => ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => []]
+ 'elements' => ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => []],
+ 'organizations' => ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => []],
+ 'relationships' => ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => []],
+ 'views' => ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => []],
+ 'property_definitions' => ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => []],
];
- // If we have access to the actual save results from ObjectService, use those
+ // If we have access to the actual save results from ObjectService, use those.
if ($this->lastSaveResult !== null) {
$saveResult = $this->lastSaveResult;
-
- // Count objects by section type from the actual processed objects
+
+ // Count objects by section type from the actual processed objects.
$allProcessedObjects = array_merge(
$saveResult['saved'] ?? [],
$saveResult['updated'] ?? [],
$saveResult['skipped'] ?? [],
- // For invalid objects, extract the original object from the error structure
+ // For invalid objects, extract the original object from the error structure.
array_map(fn($item) => $item['object'] ?? [], $saveResult['invalid'] ?? [])
);
-
+
foreach ($allProcessedObjects as $object) {
- // Convert ObjectEntity to array if needed
- if (is_object($object) && method_exists($object, 'jsonSerialize')) {
+ // Convert ObjectEntity to array if needed.
+ if (is_object($object) === true && method_exists($object, 'jsonSerialize') === true) {
$object = $object->jsonSerialize();
}
-
- $sectionType = $object['section'] ?? 'elements'; // Default to elements if section not found
-
- // Map section types to statistics keys
- $sectionKey = match($sectionType) {
+
+ $sectionType = $object['section'] ?? 'elements';
+ // Default to elements if section not found.
+ // Map section types to statistics keys.
+ $sectionKey = match ($sectionType) {
'elements' => 'elements',
- 'relationships' => 'relationships',
+ 'relationships' => 'relationships',
'organizations' => 'organizations',
'views' => 'views',
'property_definitions' => 'property_definitions',
- default => 'elements' // Default fallback
+ default => 'elements'
+ // Default fallback.
};
-
- if (!isset($statistics[$sectionKey])) {
- continue; // Skip unknown section types
+
+ if (isset($statistics[$sectionKey]) === false) {
+ continue;
+ // Skip unknown section types.
}
-
- // Determine if this object was created, updated, or had errors
+
+ // Determine if this object was created, updated, or had errors.
$objectId = $object['@self']['id'] ?? $object['identifier'] ?? null;
-
- // Check if this object is in the saved (created) list
- $wasCreated = !empty(array_filter($saveResult['saved'] ?? [],
- fn($saved) => ($saved->getUuid() === $objectId)));
-
- // Check if this object is in the updated list
- $wasUpdated = !empty(array_filter($saveResult['updated'] ?? [],
- fn($updated) => ($updated->getUuid() === $objectId)));
-
- // Check if this object was skipped (no changes)
- $wasSkipped = !empty(array_filter($saveResult['skipped'] ?? [],
- fn($skipped) => ($skipped->getUuid() === $objectId)));
-
- // Check if this object had validation errors
- $hasErrors = !empty(array_filter($saveResult['invalid'] ?? [],
- fn($invalid) => (($invalid['object']['@self']['id'] ?? null) === $objectId)));
-
- if ($wasCreated) {
+
+ // Check if this object is in the saved (created) list.
+ $wasCreated = empty(
+ array_filter(
+ $saveResult['saved'] ?? [],
+ fn($saved) => ($saved->getUuid() === $objectId)
+ )
+ ) === false;
+
+ // Check if this object is in the updated list.
+ $wasUpdated = empty(
+ array_filter(
+ $saveResult['updated'] ?? [],
+ fn($updated) => ($updated->getUuid() === $objectId)
+ )
+ ) === false;
+
+ // Check if this object was skipped (no changes).
+ $wasSkipped = empty(
+ array_filter(
+ $saveResult['skipped'] ?? [],
+ fn($skipped) => ($skipped->getUuid() === $objectId)
+ )
+ ) === false;
+
+ // Check if this object had validation errors.
+ $hasErrors = empty(
+ array_filter(
+ $saveResult['invalid'] ?? [],
+ fn($invalid) => (($invalid['object']['@self']['id'] ?? null) === $objectId)
+ )
+ ) === false;
+
+ if (empty($wasCreated) === false) {
$statistics[$sectionKey]['created']++;
- } elseif ($wasUpdated) {
+ } else if (empty($wasUpdated) === false) {
$statistics[$sectionKey]['updated']++;
- } elseif ($wasSkipped) {
+ } else if (empty($wasSkipped) === false) {
$statistics[$sectionKey]['skipped']++;
- } elseif ($hasErrors) {
- // Add to errors array for this section
- $errorInfo = array_filter($saveResult['invalid'] ?? [],
- fn($invalid) => (($invalid['object']['@self']['id'] ?? null) === $objectId));
-
- if (!empty($errorInfo)) {
+ } else if (empty($hasErrors) === false) {
+ // Add to errors array for this section.
+ $errorInfo = array_filter(
+ $saveResult['invalid'] ?? [],
+ fn($invalid) => (($invalid['object']['@self']['id'] ?? null) === $objectId)
+ );
+
+ if (empty($errorInfo) === false) {
$statistics[$sectionKey]['errors'][] = array_values($errorInfo)[0]['error'] ?? 'Unknown validation error';
}
} else {
- // This shouldn't happen, but leave as fallback
+ // This shouldn't happen, but leave as fallback.
$statistics[$sectionKey]['skipped']++;
}
- }
+ }//end foreach
} else {
- // Fallback to old method if no save result is available
+ // Fallback to old method if no save result is available.
$sections = ['elements', 'relationships', 'organizations', 'views', 'property_definitions'];
foreach ($sections as $section) {
- if (isset($normalizedData[$section])) {
+ if (isset($normalizedData[$section]) === true) {
$count = count($normalizedData[$section]);
- // Assume all objects were created (legacy behavior)
+ // Assume all objects were created (legacy behavior).
$statistics[$section]['created'] = $count;
}
}
- }
+ }//end if
- // Calculate summary totals from actual statistics
+ // Calculate summary totals from actual statistics.
$summary = [
'total_objects_created' => 0,
'total_objects_updated' => 0,
'total_objects_deleted' => 0,
'total_objects_skipped' => 0,
- 'total_errors' => 0
+ 'total_errors' => 0,
];
foreach ($statistics as $section => $sectionStats) {
- if ($section !== 'summary') { // Skip summary section itself
+ if ($section !== 'summary') {
+ // Skip summary section itself.
$summary['total_objects_created'] += $sectionStats['created'];
$summary['total_objects_updated'] += $sectionStats['updated'];
$summary['total_objects_skipped'] += $sectionStats['skipped'];
- $summary['total_errors'] += count($sectionStats['errors']);
+ $summary['total_errors'] += count($sectionStats['errors']);
}
}
$statistics['summary'] = $summary;
return $statistics;
- }
+ }//end calculateObjectStatistics()
/**
* Extract propertyDefinitions from the parsed XML and build a map
*
* @param array $data Parsed XML data
+ *
* @return array Map of propertyDefinitionRef => property name
*/
private function extractPropertyDefinitionMap(array $data): array
{
- // OPTIMIZATION: Return cached property definition map if available
+ // OPTIMIZATION: Return cached property definition map if available.
if ($this->propertyDefinitionMapCache !== null) {
return $this->propertyDefinitionMapCache;
}
-
+
$map = [];
- // Find propertyDefinitions section (handle possible alternative names)
+ // Find propertyDefinitions section (handle possible alternative names).
$propertyDefs = null;
- if (isset($data['propertyDefinitions'])) {
+ if (isset($data['propertyDefinitions']) === true) {
$propertyDefs = $data['propertyDefinitions'];
- } elseif (isset($data['property_definitions'])) {
+ } else if (isset($data['property_definitions']) === true) {
$propertyDefs = $data['property_definitions'];
- } elseif (isset($data['propertyDefinitions'])) {
+ } else if (isset($data['propertyDefinitions']) === true) {
$propertyDefs = $data['propertyDefinitions'];
}
- if ($propertyDefs && isset($propertyDefs['propertyDefinition'])) {
+
+ if ($propertyDefs !== false && isset($propertyDefs['propertyDefinition']) === true) {
$defs = $propertyDefs['propertyDefinition'];
- if (isset($defs[0])) {
- // Array of propertyDefinition
+ if (isset($defs[0]) === true) {
+ // Array of propertyDefinition.
foreach ($defs as $def) {
- if (isset($def['_attributes']['identifier']) && isset($def['name'])) {
- $map[$def['_attributes']['identifier']] = is_array($def['name']) && isset($def['name']['_value']) ? $def['name']['_value'] : $def['name'];
+ if (isset($def['_attributes']['identifier']) === true && isset($def['name']) === true) {
+ if (is_array($def['name']) === true && isset($def['name']['_value']) === true) {
+ $map[$def['_attributes']['identifier']] = $def['name']['_value'];
+ } else {
+ $map[$def['_attributes']['identifier']] = $def['name'];
+ }
}
}
- } elseif (isset($defs['_attributes']['identifier']) && isset($defs['name'])) {
- // Single propertyDefinition
- $map[$defs['_attributes']['identifier']] = is_array($defs['name']) && isset($defs['name']['_value']) ? $defs['name']['_value'] : $defs['name'];
+ } else if (isset($defs['_attributes']['identifier']) === true && isset($defs['name']) === true) {
+ // Single propertyDefinition.
+ if (is_array($defs['name']) === true && isset($defs['name']['_value']) === true) {
+ $map[$defs['_attributes']['identifier']] = $defs['name']['_value'];
+ } else {
+ $map[$defs['_attributes']['identifier']] = $defs['name'];
+ }
}
- }
-
- // OPTIMIZATION: Cache the result for subsequent calls during the same import
+ }//end if
+
+ // OPTIMIZATION: Cache the result for subsequent calls during the same import.
$this->propertyDefinitionMapCache = $map;
-
+
return $map;
- }
+ }//end extractPropertyDefinitionMap()
/**
* Transform ArchiMate XML data to objects array in batch (OpenRegister pattern)
- *
+ *
* This method follows the same pattern as OpenRegister CSV import:
* - Parse ALL sections at once
* - Create objects directly without intermediate normalization
* - Use cached configuration values
* - Minimize object copying and complex transformations
- *
- * @param array $xmlData Parsed XML data
+ *
+ * @param array $xmlData Parsed XML data
* @param string $modelIdentifier Model identifier
+ *
* @return array Array of objects ready for saveObjects()
*/
private function transformArchiMateXmlToObjectsBatch(array $xmlData, string $modelIdentifier): array
{
$allObjects = [];
-
- // Extract propertyDefinitionMap once for all objects
- $propertyDefinitionMap = $this->extractPropertyDefinitionMap($xmlData);
-
- // Create model object first
- if (isset($xmlData['_attributes']) || isset($xmlData['name'])) {
+
+ // Extract propertyDefinitionMap once for all objects.
+ $propertyDefinitionMap = $this->extractPropertyDefinitionMap(data: $xmlData);
+
+ // Create model object first.
+ if (isset($xmlData['_attributes']) === true || isset($xmlData['name']) === true) {
$modelMetadata = [
- 'identifier' => $modelIdentifier,
- 'name' => $xmlData['name'] ?? '',
- 'documentation' => $xmlData['documentation'] ?? '',
- 'properties' => $xmlData['properties'] ?? [],
- 'propertyDefinitionMap' => $propertyDefinitionMap
+ 'identifier' => $modelIdentifier,
+ 'name' => $xmlData['name'] ?? '',
+ 'documentation' => $xmlData['documentation'] ?? '',
+ 'properties' => $xmlData['properties'] ?? [],
+ 'propertyDefinitionMap' => $propertyDefinitionMap,
];
-
- if (isset($xmlData['_attributes'])) {
+
+ if (isset($xmlData['_attributes']) === true) {
$modelMetadata = array_merge($modelMetadata, $xmlData['_attributes']);
}
-
- $allObjects[] = $this->createModelObjectDirect($modelMetadata, $modelIdentifier);
+
+ $allObjects[] = $this->createModelObjectDirect(metadata: $modelMetadata, modelIdentifier: $modelIdentifier);
}
-
- // Process each section type directly (no intermediate normalization)
+
+ // Process each section type directly (no intermediate normalization).
$sections = [
- 'elements' => 'element',
- 'relationships' => 'relationship',
- 'organizations' => 'organization',
- 'views' => 'view',
- 'property_definitions' => 'property_definition'
+ 'elements' => 'element',
+ 'relationships' => 'relationship',
+ 'organizations' => 'organization',
+ 'views' => 'view',
+ 'property_definitions' => 'property_definition',
];
-
+
foreach ($sections as $sectionName => $schemaType) {
- $sectionData = $this->findSectionData($xmlData, $sectionName);
- if (!empty($sectionData)) {
+ $sectionData = $this->findSectionData(xmlData: $xmlData, sectionName: $sectionName);
+ if (empty($sectionData) === false) {
$sectionObjects = $this->transformSectionObjectsBatch(
- $sectionData,
- $schemaType,
- $modelIdentifier,
- $propertyDefinitionMap
+ sectionData: $sectionData,
+ schemaType: $schemaType,
+ modelIdentifier: $modelIdentifier,
+ propertyDefinitionMap: $propertyDefinitionMap
);
- $allObjects = array_merge($allObjects, $sectionObjects);
+ $allObjects = array_merge($allObjects, $sectionObjects);
}
}
-
+
return $allObjects;
- }
+ }//end transformArchiMateXmlToObjectsBatch()
/**
* Create model object directly with cached configuration
- *
- * @param array $metadata Model metadata
+ *
+ * @param array $metadata Model metadata
* @param string $modelIdentifier Model identifier
+ *
* @return array Model object with @self structure
*/
private function createModelObjectDirect(array $metadata, string $modelIdentifier): array
{
return [
- '@self' => [
- 'register' => $this->cachedConfig['registerId'],
- 'schema' => $this->cachedConfig['schemaIds']['model'],
- 'id' => $modelIdentifier,
- 'published' => date('Y-m-d\TH:i:s\Z')
+ '@self' => [
+ 'register' => $this->cachedConfig['registerId'],
+ 'schema' => $this->cachedConfig['schemaIds']['model'],
+ 'id' => $modelIdentifier,
+ 'published' => date('Y-m-d\TH:i:s\Z'),
],
- 'identifier' => $modelIdentifier,
- 'section' => 'model',
- 'model_identifier' => $modelIdentifier
+ 'identifier' => $modelIdentifier,
+ 'section' => 'model',
+ 'model_identifier' => $modelIdentifier,
] + $metadata;
- }
+ }//end createModelObjectDirect()
/**
* Find section data efficiently without complex nested searches
- *
- * @param array $xmlData Parsed XML data
+ *
+ * @param array $xmlData Parsed XML data
* @param string $sectionName Section name to find
+ *
* @return array Section data or empty array
*/
private function findSectionData(array $xmlData, string $sectionName): array
{
- // Direct lookup first
- if (isset($xmlData[$sectionName])) {
+ // Direct lookup first.
+ if (isset($xmlData[$sectionName]) === true) {
return $xmlData[$sectionName];
}
-
- // Alternative names lookup
+
+ // Alternative names lookup.
$alternatives = [
- 'views' => ['diagrams'],
- 'organizations' => ['organisation'],
- 'property_definitions' => ['propertyDefinitions', 'propertydefinitions']
+ 'views' => ['diagrams'],
+ 'organizations' => ['organisation'],
+ 'property_definitions' => ['propertyDefinitions', 'propertydefinitions'],
];
-
- if (isset($alternatives[$sectionName])) {
+
+ if (isset($alternatives[$sectionName]) === true) {
foreach ($alternatives[$sectionName] as $altName) {
- if (isset($xmlData[$altName])) {
+ if (isset($xmlData[$altName]) === true) {
return $xmlData[$altName];
}
}
}
-
+
return [];
- }
+ }//end findSectionData()
/**
* Transform section objects in batch with minimal overhead
- *
- * @param array $sectionData Section data from XML
- * @param string $schemaType Schema type (singular)
- * @param string $modelIdentifier Model identifier
- * @param array $propertyDefinitionMap Property definition map
+ *
+ * @param array $sectionData Section data from XML
+ * @param string $schemaType Schema type (singular)
+ * @param string $modelIdentifier Model identifier
+ * @param array $propertyDefinitionMap Property definition map
+ *
* @return array Array of transformed objects
*/
private function transformSectionObjectsBatch(
- array $sectionData,
- string $schemaType,
- string $modelIdentifier,
+ array $sectionData,
+ string $schemaType,
+ string $modelIdentifier,
array $propertyDefinitionMap
): array {
$objects = [];
-
- // Find items in section (simplified version)
- $items = $this->findItemsSimplified($sectionData, $schemaType);
-
+
+ // Find items in section (simplified version).
+ $items = $this->findItemsSimplified(sectionData: $sectionData, sectionType: $schemaType);
+
foreach ($items as $item) {
- if (!is_array($item)) {
+ if (is_array($item) === false) {
continue;
}
-
- $identifier = $this->extractIdentifier($item, $schemaType);
- if (!$identifier) {
+
+ $identifier = $this->extractIdentifier(item: $item, sectionName: $schemaType);
+ if ($identifier === null) {
continue;
}
-
- // Create object directly (minimal processing)
+
+ // Create object directly (minimal processing).
$object = [
- '@self' => [
- 'register' => $this->cachedConfig['registerId'],
- 'schema' => $this->cachedConfig['schemaIds'][$schemaType],
- 'id' => $identifier,
- 'published' => date('Y-m-d\TH:i:s\Z')
+ '@self' => [
+ 'register' => $this->cachedConfig['registerId'],
+ 'schema' => $this->cachedConfig['schemaIds'][$schemaType],
+ 'id' => $identifier,
+ 'published' => date('Y-m-d\TH:i:s\Z'),
],
- 'identifier' => $identifier,
- 'section' => $schemaType,
+ 'identifier' => $identifier,
+ 'section' => $schemaType,
'model_identifier' => $modelIdentifier,
- 'xml' => $this->extractEssentialXmlData($item) // OPTIMIZATION: Store only essential XML data
+ 'xml' => $this->extractEssentialXmlData(item: $item),
+ // OPTIMIZATION: Store only essential XML data.
];
-
- // Extract name from XML if it exists
- if (isset($item['name'])) {
- if (is_array($item['name']) && isset($item['name']['_value'])) {
+
+ // Extract name from XML if it exists.
+ if (isset($item['name']) === true) {
+ if (is_array($item['name']) === true && isset($item['name']['_value']) === true) {
$object['name'] = $item['name']['_value'];
- } elseif (is_string($item['name'])) {
+ } else if (is_string($item['name']) === true) {
$object['name'] = $item['name'];
}
}
-
- // Extract documentation from XML if it exists and set to summary
- if (isset($item['documentation'])) {
- if (is_array($item['documentation']) && isset($item['documentation']['_value'])) {
+
+ // Extract documentation from XML if it exists and set to summary.
+ if (isset($item['documentation']) === true) {
+ if (is_array($item['documentation']) === true && isset($item['documentation']['_value']) === true) {
$object['summary'] = $item['documentation']['_value'];
- } elseif (is_string($item['documentation'])) {
+ } else if (is_string($item['documentation']) === true) {
$object['summary'] = $item['documentation'];
}
}
-
- // Flatten properties efficiently (if present)
- if (isset($item['properties']['property']) && !empty($propertyDefinitionMap)) {
- $this->flattenPropertiesBatch($object, $item['properties']['property'], $propertyDefinitionMap);
+
+ // Flatten properties efficiently (if present).
+ if (isset($item['properties']['property']) === true && empty($propertyDefinitionMap) === false) {
+ $this->flattenPropertiesBatch(
+ object: $object,
+ properties: $item['properties']['property'],
+ propertyDefinitionMap: $propertyDefinitionMap
+ );
}
-
+
$objects[] = $object;
- }
-
+ }//end foreach
+
return $objects;
- }
+ }//end transformSectionObjectsBatch()
/**
* Simplified item finding for better performance
- *
- * @param array $sectionData Section data
+ *
+ * @param array $sectionData Section data
* @param string $sectionType Section type
+ *
* @return array Items array
*/
private function findItemsSimplified(array $sectionData, string $sectionType): array
{
- // Handle views with diagrams structure
- if ($sectionType === 'view' && isset($sectionData['diagrams']['view'])) {
+ // Handle views with diagrams structure.
+ if ($sectionType === 'view' && isset($sectionData['diagrams']['view']) === true) {
$viewData = $sectionData['diagrams']['view'];
- return isset($viewData[0]) ? $viewData : [$viewData];
+ if (isset($viewData[0]) === true) {
+ return $viewData;
+ } else {
+ return [$viewData];
+ }
}
-
- // Try common patterns
+
+ // Try common patterns.
$patterns = [
- $sectionType, // singular: element, relationship, etc.
- $sectionType . 's', // plural: elements, relationships, etc.
- 'item', // organizations use 'item'
- 'propertyDefinition' // property definitions
+ // Singular: element, relationship, etc.
+ $sectionType,
+ // Plural: elements, relationships, etc.
+ $sectionType.'s',
+ // Organizations use 'item'.
+ 'item',
+ // Property definitions.
+ 'propertyDefinition',
];
-
+
foreach ($patterns as $pattern) {
- if (isset($sectionData[$pattern])) {
+ if (isset($sectionData[$pattern]) === true) {
$data = $sectionData[$pattern];
- return is_array($data) && isset($data[0]) ? $data : [$data];
+ if (is_array($data) === true && isset($data[0]) === true) {
+ return $data;
+ } else {
+ return [$data];
+ }
}
}
-
- // Fallback: treat section data as single item
+
+ // Fallback: treat section data as single item.
return [$sectionData];
- }
+ }//end findItemsSimplified()
/**
* Flatten properties in batch for better performance
- *
- * @param array &$object Object to add properties to (by reference)
- * @param array $properties Properties array from XML
- * @param array $propertyDefinitionMap Property definition map
+ *
+ * @param array $object Object to add properties to (by reference).
+ * @param array $properties Properties array from XML.
+ * @param array $propertyDefinitionMap Property definition map.
+ *
* @return void
*/
private function flattenPropertiesBatch(array &$object, array $properties, array $propertyDefinitionMap): void
{
- $props = isset($properties[0]) ? $properties : [$properties];
+ if (isset($properties[0]) === true) {
+ $props = $properties;
+ } else {
+ $props = [$properties];
+ }
+
$processedProperties = [];
-
+
foreach ($props as $prop) {
- if (!isset($prop['_attributes']['propertyDefinitionRef'])) {
+ if (isset($prop['_attributes']['propertyDefinitionRef']) === false) {
continue;
}
-
+
$defRef = $prop['_attributes']['propertyDefinitionRef'];
- $value = $prop['value']['_value'] ?? $prop['value'] ?? null;
-
- if ($value !== null && isset($propertyDefinitionMap[$defRef])) {
- $propertyName = $propertyDefinitionMap[$defRef];
- $camelCaseName = $this->convertToCamelCase($propertyName);
+ $value = $prop['value']['_value'] ?? $prop['value'] ?? null;
+
+ if ($value !== null && isset($propertyDefinitionMap[$defRef]) === true) {
+ $propertyName = $propertyDefinitionMap[$defRef];
+ $camelCaseName = $this->convertToCamelCase(propertyName: $propertyName);
$object[$camelCaseName] = $value;
-
- // Store property mapping for reference
- if (!isset($object['_propertyMapping'])) {
+
+ // Store property mapping for reference.
+ if (isset($object['_propertyMapping']) === false) {
$object['_propertyMapping'] = [];
}
+
$object['_propertyMapping'][$camelCaseName] = $propertyName;
-
+
$processedProperties[] = [
- 'original' => $propertyName,
+ 'original' => $propertyName,
'camelCase' => $camelCaseName,
- 'value' => $value
+ 'value' => $value,
];
-
- // Set slug for Object ID property
+
+ // Set slug for Object ID property.
if (strtolower($propertyName) === 'object id') {
$object['@self']['slug'] = $value;
}
- }
- }
-
- // OPTIMIZATION: Removed debug logging from tight loop for performance
- }
+ }//end if
+ }//end foreach
+
+ // OPTIMIZATION: Removed debug logging from tight loop for performance.
+ }//end flattenPropertiesBatch()
/**
* Convert property names with spaces to camelCase for better database compatibility
- *
+ *
* Examples:
* - "Object ID" -> "objectId"
* - "Business Unit" -> "businessUnit"
* - "System Name" -> "systemName"
- *
+ *
* @param string $propertyName Property name that may contain spaces
+ *
* @return string CamelCase version of the property name
*/
private function convertToCamelCase(string $propertyName): string
{
- // OPTIMIZATION: Check cache first to avoid redundant conversions
- if (isset($this->camelCaseCache[$propertyName])) {
+ // OPTIMIZATION: Check cache first to avoid redundant conversions.
+ if (isset($this->camelCaseCache[$propertyName]) === true) {
return $this->camelCaseCache[$propertyName];
}
-
- // Remove any leading/trailing whitespace
+
+ // Remove any leading/trailing whitespace.
$propertyName = trim($propertyName);
-
- // Split by spaces and convert to camelCase
+
+ // Split by spaces and convert to camelCase.
$words = explode(' ', $propertyName);
-
+
if (count($words) === 1) {
- // Single word, just lowercase it
+ // Single word, just lowercase it.
$result = strtolower($words[0]);
} else {
- // First word is lowercase, subsequent words are capitalized
+ // First word is lowercase, subsequent words are capitalized.
$camelCase = strtolower($words[0]);
-
+
for ($i = 1; $i < count($words); $i++) {
$camelCase .= ucfirst(strtolower($words[$i]));
}
-
+
$result = $camelCase;
}
-
- // OPTIMIZATION: Cache the result for future use
+
+ // OPTIMIZATION: Cache the result for future use.
$this->camelCaseCache[$propertyName] = $result;
-
+
return $result;
- }
+ }//end convertToCamelCase()
/**
* Get property mapping information for debugging and reference
- *
+ *
* This method returns a mapping of original property names to their camelCase equivalents
* which can be useful for understanding how properties are being processed.
- *
+ *
* @param array $propertyDefinitionMap The original property definition map
+ *
* @return array Mapping of original names to camelCase names
*/
public function getPropertyNameMapping(array $propertyDefinitionMap): array
{
$mapping = [];
-
+
foreach ($propertyDefinitionMap as $propertyRef => $originalName) {
- $mapping[$originalName] = $this->convertToCamelCase($originalName);
+ $mapping[$originalName] = $this->convertToCamelCase(propertyName: $originalName);
}
-
+
return $mapping;
- }
+ }//end getPropertyNameMapping()
/**
* Calculate optimized statistics for performance reporting
- *
+ *
* @param array $savedObjects Saved objects from ObjectService::saveObjects
+ *
* @return array Statistics array
*/
private function calculateOptimizedStatistics(array $savedObjects): array
@@ -2445,357 +2727,398 @@ private function calculateOptimizedStatistics(array $savedObjects): array
'total_objects_updated' => 0,
'total_objects_deleted' => 0,
'total_objects_skipped' => 0,
- 'total_errors' => 0
- ]
+ 'total_errors' => 0,
+ ],
];
if ($this->lastSaveResult !== null) {
- $saveResult = $this->lastSaveResult;
+ $saveResult = $this->lastSaveResult;
$statistics['summary'] = [
'total_objects_created' => count($saveResult['saved'] ?? []),
'total_objects_updated' => count($saveResult['updated'] ?? []),
'total_objects_deleted' => 0,
'total_objects_skipped' => count($saveResult['skipped'] ?? []),
- 'total_errors' => count($saveResult['invalid'] ?? [])
+ 'total_errors' => count($saveResult['invalid'] ?? []),
];
}
return $statistics;
- }
+ }//end calculateOptimizedStatistics()
/**
* Extract GEMMA type from an object using multiple possible property names
- *
+ *
* This method tries different variations of GEMMA type property names to ensure
* compatibility with different ArchiMate model variations.
- *
+ *
* @param array $object The object to extract GEMMA type from
+ *
* @return string|null The GEMMA type value or null if not found
*/
private function extractGemmaType(array $object): ?string
{
- // Try various possible property names for GEMMA type
+ // Try various possible property names for GEMMA type.
$possiblePropertyNames = [
- 'gemmaType', // Standard camelCase conversion of "GEMMA Type"
- 'gemmatype', // Lowercase version
- 'GemmaType', // PascalCase version
- 'GEMMA_Type', // Underscore version
- 'gemma_type', // Lowercase underscore version
- 'GEMMAType', // All caps first word
- 'type', // Sometimes just "Type" in models
- 'elementType', // Alternative naming
- 'componentType' // Another alternative
+ 'gemmaType',
+ // Standard camelCase conversion of "GEMMA Type".
+ 'gemmatype',
+ // Lowercase version.
+ 'GemmaType',
+ // PascalCase version.
+ 'GEMMA_Type',
+ // Underscore version.
+ 'gemma_type',
+ // Lowercase underscore version.
+ 'GEMMAType',
+ // All caps first word.
+ 'type',
+ // Sometimes just "Type" in models.
+ 'elementType',
+ // Alternative naming.
+ 'componentType',
+ // Another alternative.
];
-
+
foreach ($possiblePropertyNames as $propertyName) {
- if (isset($object[$propertyName]) && !empty($object[$propertyName])) {
+ if (isset($object[$propertyName]) === true && empty($object[$propertyName]) === false) {
$value = (string) $object[$propertyName];
-
- // Log the first successful match for debugging
- if (!$this->gemmaTypePropertyFound) {
- $this->logger->debug('GEMMA Type property found', [
- 'property_name' => $propertyName,
- 'value' => $value,
- 'object_id' => $object['identifier'] ?? 'unknown'
- ]);
+
+ // Log the first successful match for debugging.
+ if ($this->gemmaTypePropertyFound === false) {
+ $this->logger->debug(
+ 'GEMMA Type property found',
+ [
+ 'property_name' => $propertyName,
+ 'value' => $value,
+ 'object_id' => $object['identifier'] ?? 'unknown',
+ ]
+ );
$this->gemmaTypePropertyFound = true;
}
-
+
return $value;
}
}
-
- // If no direct property found, check _propertyMapping for original property names
- if (isset($object['_propertyMapping'])) {
+
+ // If no direct property found, check _propertyMapping for original property names.
+ if (isset($object['_propertyMapping']) === true) {
foreach ($object['_propertyMapping'] as $camelCase => $original) {
- // Check if the original property name contains "gemma" or "type"
+ // Check if the original property name contains "gemma" or "type".
if (stripos($original, 'gemma') !== false && stripos($original, 'type') !== false) {
- if (isset($object[$camelCase]) && !empty($object[$camelCase])) {
- $this->logger->debug('GEMMA Type found via property mapping', [
- 'camel_case_name' => $camelCase,
- 'original_name' => $original,
- 'value' => $object[$camelCase],
- 'object_id' => $object['identifier'] ?? 'unknown'
- ]);
+ if (isset($object[$camelCase]) === true && empty($object[$camelCase]) === false) {
+ $this->logger->debug(
+ 'GEMMA Type found via property mapping',
+ [
+ 'camel_case_name' => $camelCase,
+ 'original_name' => $original,
+ 'value' => $object[$camelCase],
+ 'object_id' => $object['identifier'] ?? 'unknown',
+ ]
+ );
return (string) $object[$camelCase];
}
}
}
}
-
+
return null;
- }
+ }//end extractGemmaType()
/**
* Process GEMMA Referentiecomponent-Standaard relationships with Verbindingsrol support
- *
+ *
* This method analyzes all objects to find Referentiecomponenten and Standaarden,
* then uses relationships to link them together based on Verbindingsrol property.
* Each Referentiecomponent gets two properties:
* - 'aanbevolenStandaarden' array for standards with Verbindingsrol = "Aanbevolen"
* - 'verplichteStandaarden' array for standards with Verbindingsrol = "Verplicht"
- *
+ *
* @param array $objects All objects from the import
+ *
* @return array Objects with enhanced Referentiecomponent data
*/
private function processGemmaReferenceComponentStandards(array $objects): array
{
$this->logger->info('Processing GEMMA Referentiecomponent-Standaard relationships with optimized single-pass algorithm');
-
- // OPTIMIZATION: Single-pass processing - collect all data types at once
+
+ // OPTIMIZATION: Single-pass processing - collect all data types at once.
$referentieComponenten = [];
- $standaarden = [];
- $gemmaRelationshipMap = [];
-
- // Debug: Count objects and property variations
- $elementCount = 0;
+ $standaarden = [];
+ $gemmaRelationshipMap = [];
+
+ // Debug: Count objects and property variations.
+ $elementCount = 0;
$elementsWithGemmaType = 0;
- $gemmaTypeVariations = [];
-
- // PASS 1: Collect Referentiecomponenten and Standaarden, process relationships immediately
+ $gemmaTypeVariations = [];
+
+ // PASS 1: Collect Referentiecomponenten and Standaarden, process relationships immediately.
foreach ($objects as $index => $object) {
- // Debug: Count elements and GEMMA types
- if (isset($object['section']) && $object['section'] === 'element') {
+ // Debug: Count elements and GEMMA types.
+ if (isset($object['section']) === true && $object['section'] === 'element') {
$elementCount++;
-
- // Check for various possible GEMMA type property names
- $gemmaTypeValue = $this->extractGemmaType($object);
+
+ // Check for various possible GEMMA type property names.
+ $gemmaTypeValue = $this->extractGemmaType(object: $object);
if ($gemmaTypeValue !== null) {
$elementsWithGemmaType++;
-
- // Track GEMMA type variations for debugging
- if (!isset($gemmaTypeVariations[$gemmaTypeValue])) {
+
+ // Track GEMMA type variations for debugging.
+ if (isset($gemmaTypeVariations[$gemmaTypeValue]) === false) {
$gemmaTypeVariations[$gemmaTypeValue] = 0;
}
+
$gemmaTypeVariations[$gemmaTypeValue]++;
-
+
if ($gemmaTypeValue === 'Referentiecomponent') {
$referentieComponenten[$object['identifier']] = $index;
- } elseif ($gemmaTypeValue === 'Standaard') {
+ } else if ($gemmaTypeValue === 'Standaard') {
$standaarden[$object['identifier']] = $index;
}
}
+ }//end if
+
+ // Process relationships immediately when found (no separate collection needed).
+ if (isset($object['section']) === true && $object['section'] === 'relationship') {
+ $this->processRelationshipImmediate(
+ relationship: $object,
+ referentieComponenten: $referentieComponenten,
+ standaarden: $standaarden,
+ gemmaRelationshipMap: $gemmaRelationshipMap
+ );
}
-
- // Process relationships immediately when found (no separate collection needed)
- if (isset($object['section']) && $object['section'] === 'relationship') {
- $this->processRelationshipImmediate($object, $referentieComponenten, $standaarden, $gemmaRelationshipMap);
- }
- }
-
- // Enhanced debug logging
- $this->logger->info('GEMMA objects processing complete', [
- 'total_elements' => $elementCount,
- 'elements_with_gemma_type' => $elementsWithGemmaType,
- 'gemma_type_variations' => $gemmaTypeVariations,
- 'referentiecomponenten_count' => count($referentieComponenten),
- 'standaarden_count' => count($standaarden),
- 'processed_relationships' => count($gemmaRelationshipMap)
- ]);
-
- // Additional debugging if no GEMMA types found
+ }//end foreach
+
+ // Enhanced debug logging.
+ $this->logger->info(
+ 'GEMMA objects processing complete',
+ [
+ 'total_elements' => $elementCount,
+ 'elements_with_gemma_type' => $elementsWithGemmaType,
+ 'gemma_type_variations' => $gemmaTypeVariations,
+ 'referentiecomponenten_count' => count($referentieComponenten),
+ 'standaarden_count' => count($standaarden),
+ 'processed_relationships' => count($gemmaRelationshipMap),
+ ]
+ );
+
+ // Additional debugging if no GEMMA types found.
if ($elementsWithGemmaType === 0 && $elementCount > 0) {
- $this->logger->warning('No GEMMA types found in any elements', [
- 'total_elements_processed' => $elementCount,
- 'sample_element_keys' => 'Will need to examine individual objects'
- ]);
+ $this->logger->warning(
+ 'No GEMMA types found in any elements',
+ [
+ 'total_elements_processed' => $elementCount,
+ 'sample_element_keys' => 'Will need to examine individual objects',
+ ]
+ );
}
-
- // STEP 2: Apply the processed relationship mappings to Referentiecomponenten
+
+ // STEP 2: Apply the processed relationship mappings to Referentiecomponenten.
$enhancedCount = 0;
foreach ($gemmaRelationshipMap as $referentieComponentId => $standaardenMap) {
- if (isset($referentieComponenten[$referentieComponentId])) {
+ if (isset($referentieComponenten[$referentieComponentId]) === true) {
$objectIndex = $referentieComponenten[$referentieComponentId];
-
- // Remove duplicates and add the properties
+
+ // Remove duplicates and add the properties.
$aanbevolenStandaarden = array_unique($standaardenMap['aanbevolen']);
$verplichteStandaarden = array_unique($standaardenMap['verplicht']);
-
+
$objects[$objectIndex]['aanbevolenStandaarden'] = $aanbevolenStandaarden;
$objects[$objectIndex]['verplichteStandaarden'] = $verplichteStandaarden;
-
- // Also add combined array for backward compatibility
+
+ // Also add combined array for backward compatibility.
$allStandaarden = array_unique(array_merge($aanbevolenStandaarden, $verplichteStandaarden));
$objects[$objectIndex]['standaarden'] = $allStandaarden;
-
- $this->logger->info('Enhanced Referentiecomponent with categorized standaarden', [
- 'referentiecomponent_id' => $referentieComponentId,
- 'referentiecomponent_name' => $objects[$objectIndex]['name'] ?? 'Unknown',
- 'aanbevolen_count' => count($aanbevolenStandaarden),
- 'verplicht_count' => count($verplichteStandaarden),
- 'aanbevolen_ids' => $aanbevolenStandaarden,
- 'verplicht_ids' => $verplichteStandaarden
- ]);
-
+
+ $this->logger->info(
+ 'Enhanced Referentiecomponent with categorized standaarden',
+ [
+ 'referentiecomponent_id' => $referentieComponentId,
+ 'referentiecomponent_name' => $objects[$objectIndex]['name'] ?? 'Unknown',
+ 'aanbevolen_count' => count($aanbevolenStandaarden),
+ 'verplicht_count' => count($verplichteStandaarden),
+ 'aanbevolen_ids' => $aanbevolenStandaarden,
+ 'verplicht_ids' => $verplichteStandaarden,
+ ]
+ );
+
$enhancedCount++;
- }
- }
-
- $this->logger->info('GEMMA Referentiecomponent-Standaard processing completed', [
- 'referentiecomponenten_enhanced' => $enhancedCount,
- 'total_referentiecomponenten' => count($referentieComponenten),
- 'total_relationships_processed' => count($gemmaRelationshipMap)
- ]);
-
+ }//end if
+ }//end foreach
+
+ $this->logger->info(
+ 'GEMMA Referentiecomponent-Standaard processing completed',
+ [
+ 'referentiecomponenten_enhanced' => $enhancedCount,
+ 'total_referentiecomponenten' => count($referentieComponenten),
+ 'total_relationships_processed' => count($gemmaRelationshipMap),
+ ]
+ );
+
return $objects;
- }
+ }//end processGemmaReferenceComponentStandards()
/**
* OPTIMIZATION: Process relationship immediately when found (single-pass algorithm)
- *
- * @param array $relationship The relationship object
- * @param array $referentieComponenten Array of Referentiecomponent identifiers
- * @param array $standaarden Array of Standaard identifiers
- * @param array &$gemmaRelationshipMap The relationship map to update (by reference)
+ *
+ * @param array $relationship The relationship object.
+ * @param array $referentieComponenten Array of Referentiecomponent identifiers.
+ * @param array $standaarden Array of Standaard identifiers.
+ * @param array $gemmaRelationshipMap The relationship map to update (by reference).
+ *
* @return void
*/
- private function processRelationshipImmediate(array $relationship, array $referentieComponenten, array $standaarden, array &$gemmaRelationshipMap): void
- {
- // Get source and target from relationship XML or flattened properties
- $source = $this->extractRelationshipEndpoint($relationship, 'source');
- $target = $this->extractRelationshipEndpoint($relationship, 'target');
-
- if (!$source || !$target) {
+ private function processRelationshipImmediate(
+ array $relationship,
+ array $referentieComponenten,
+ array $standaarden,
+ array &$gemmaRelationshipMap
+ ): void {
+ // Get source and target from relationship XML or flattened properties.
+ $source = $this->extractRelationshipEndpoint(relationship: $relationship, endpoint: 'source');
+ $target = $this->extractRelationshipEndpoint(relationship: $relationship, endpoint: 'target');
+
+ if ($source === null || $target === false) {
return;
}
-
- // Get Verbindingsrol from flattened properties (camelCase: verbindingsrol)
+
+ // Get Verbindingsrol from flattened properties (camelCase: verbindingsrol).
$verbindingsrol = $relationship['verbindingsrol'] ?? null;
-
- // Skip if no Verbindingsrol is defined
- if (!$verbindingsrol) {
+
+ // Skip if no Verbindingsrol is defined.
+ if ($verbindingsrol === null) {
return;
}
-
- // Check if one end is a Referentiecomponent and the other is a Standaard
- $refCompId = null;
+
+ // Check if one end is a Referentiecomponent and the other is a Standaard.
+ $refCompId = null;
$standaardId = null;
-
- if (isset($referentieComponenten[$source]) && isset($standaarden[$target])) {
- // Referentiecomponent -> Standaard
- $refCompId = $source;
+
+ if (isset($referentieComponenten[$source]) === true && isset($standaarden[$target]) === true) {
+ // Referentiecomponent -> Standaard.
+ $refCompId = $source;
$standaardId = $target;
- } elseif (isset($standaarden[$source]) && isset($referentieComponenten[$target])) {
- // Standaard -> Referentiecomponent (reverse direction)
- $refCompId = $target;
+ } else if (isset($standaarden[$source]) === true && isset($referentieComponenten[$target]) === true) {
+ // Standaard -> Referentiecomponent (reverse direction).
+ $refCompId = $target;
$standaardId = $source;
}
-
- if ($refCompId && $standaardId) {
- // Initialize arrays if not exists
- if (!isset($gemmaRelationshipMap[$refCompId])) {
+
+ if ($refCompId !== false && $standaardId === true) {
+ // Initialize arrays if not exists.
+ if (isset($gemmaRelationshipMap[$refCompId]) === false) {
$gemmaRelationshipMap[$refCompId] = [
'aanbevolen' => [],
- 'verplicht' => []
+ 'verplicht' => [],
];
}
-
- // Add to appropriate array based on Verbindingsrol
+
+ // Add to appropriate array based on Verbindingsrol.
if (strtolower($verbindingsrol) === 'aanbevolen') {
$gemmaRelationshipMap[$refCompId]['aanbevolen'][] = $standaardId;
- } elseif (strtolower($verbindingsrol) === 'verplicht') {
+ } else if (strtolower($verbindingsrol) === 'verplicht') {
$gemmaRelationshipMap[$refCompId]['verplicht'][] = $standaardId;
}
}
- }
+ }//end processRelationshipImmediate()
/**
* Extract relationship endpoint (source or target) from relationship object
- *
- * @param array $relationship The relationship object
- * @param string $endpoint Either 'source' or 'target'
+ *
+ * @param array $relationship The relationship object
+ * @param string $endpoint Either 'source' or 'target'
+ *
* @return string|null The endpoint identifier or null if not found
*/
private function extractRelationshipEndpoint(array $relationship, string $endpoint): ?string
{
- // Try flattened camelCase property first
- if (isset($relationship[$endpoint])) {
+ // Try flattened camelCase property first.
+ if (isset($relationship[$endpoint]) === true) {
return $relationship[$endpoint];
}
-
- // Try XML structure
- if (isset($relationship['xml'][$endpoint])) {
+
+ // Try XML structure.
+ if (isset($relationship['xml'][$endpoint]) === true) {
$endpointData = $relationship['xml'][$endpoint];
-
- // Handle different XML structures
- if (is_string($endpointData)) {
+
+ // Handle different XML structures.
+ if (is_string($endpointData) === true) {
return $endpointData;
- } elseif (is_array($endpointData)) {
- // Try _attributes.href or _value
- if (isset($endpointData['_attributes']['href'])) {
+ } else if (is_array($endpointData) === true) {
+ // Try _attributes.href or _value.
+ if (isset($endpointData['_attributes']['href']) === true) {
return $endpointData['_attributes']['href'];
- } elseif (isset($endpointData['_value'])) {
+ } else if (isset($endpointData['_value']) === true) {
return $endpointData['_value'];
}
}
}
-
- // Try direct XML access for ArchiMate format
- if (isset($relationship['xml']['_attributes'])) {
+
+ // Try direct XML access for ArchiMate format.
+ if (isset($relationship['xml']['_attributes']) === true) {
$attr = $relationship['xml']['_attributes'];
- if ($endpoint === 'source' && isset($attr['source'])) {
+ if ($endpoint === 'source' && isset($attr['source']) === true) {
return $attr['source'];
- } elseif ($endpoint === 'target' && isset($attr['target'])) {
+ } else if ($endpoint === 'target' && isset($attr['target']) === true) {
return $attr['target'];
}
}
-
+
return null;
- }
+ }//end extractRelationshipEndpoint()
/**
* OPTIMIZATION: Extract only essential XML data to reduce memory usage by 20-30%
- *
+ *
* Instead of storing the complete XML structure, this method extracts only
* the essential data needed for round-trip fidelity and export functionality.
- *
+ *
* @param array $item The complete XML item data
+ *
* @return array Essential XML data for storage
*/
private function extractEssentialXmlData(array $item): array
{
$essential = [];
-
- // Always preserve core attributes (needed for export)
- if (isset($item['_attributes'])) {
+
+ // Always preserve core attributes (needed for export).
+ if (isset($item['_attributes']) === true) {
$essential['_attributes'] = $item['_attributes'];
}
-
- // Preserve name and documentation (already extracted to root level but needed for export)
- if (isset($item['name'])) {
+
+ // Preserve name and documentation (already extracted to root level but needed for export).
+ if (isset($item['name']) === true) {
$essential['name'] = $item['name'];
}
-
- if (isset($item['documentation'])) {
+
+ if (isset($item['documentation']) === true) {
$essential['documentation'] = $item['documentation'];
}
-
- // Preserve properties structure (needed for property mapping)
- if (isset($item['properties'])) {
+
+ // Preserve properties structure (needed for property mapping).
+ if (isset($item['properties']) === true) {
$essential['properties'] = $item['properties'];
}
-
- // For relationships, preserve source/target information
- if (isset($item['source'])) {
+
+ // For relationships, preserve source/target information.
+ if (isset($item['source']) === true) {
$essential['source'] = $item['source'];
}
-
- if (isset($item['target'])) {
+
+ if (isset($item['target']) === true) {
$essential['target'] = $item['target'];
}
-
- // Preserve any other critical ArchiMate-specific fields
+
+ // Preserve any other critical ArchiMate-specific fields.
$criticalFields = ['type', 'viewpoint', 'accessType', 'isDirected'];
foreach ($criticalFields as $field) {
- if (isset($item[$field])) {
+ if (isset($item[$field]) === true) {
$essential[$field] = $item[$field];
}
}
-
- // Add a marker to indicate this is essential data (for debugging)
+
+ // Add a marker to indicate this is essential data (for debugging).
$essential['_essential_data'] = true;
-
- return $essential;
- }
-}
\ No newline at end of file
+ return $essential;
+ }//end extractEssentialXmlData()
+}//end class
diff --git a/lib/Service/ArchiMateService_backup.php b/lib/Service/ArchiMateService_backup.php
deleted file mode 100644
index 528a8e40..00000000
--- a/lib/Service/ArchiMateService_backup.php
+++ /dev/null
@@ -1,5164 +0,0 @@
-
- * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @link https://github.com/ConductionNL/SoftwareCatalog
- * @version 2.0.0
- */
-
-namespace OCA\SoftwareCatalog\Service;
-
-use OCP\IAppConfig;
-use OCP\Files\IRootFolder;
-use OCP\IUserSession;
-use Psr\Log\LoggerInterface;
-use React\Promise\Promise;
-use function React\Promise\all;
-use React\Promise\Deferred;
-use React\EventLoop\Loop;
-use OCP\App\IAppManager;
-use Psr\Container\ContainerInterface;
-
-/**
- * Clean ArchiMate Service with ReactPHP Parallel Processing
- *
- * This service provides streamlined functionality to import ArchiMate files and convert them
- * to OpenRegister objects using ReactPHP for parallel processing and streaming XML parsing.
- *
- * @category Service
- * @package OCA\SoftwareCatalog\Service
- * @author Conduction b.v.
- * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @link https://github.com/ConductionNL/SoftwareCatalog
- * @version 2.0.0
- */
-class ArchiMateService
-{
- /**
- * The application name
- */
- private const APP_NAME = 'softwarecatalog';
-
- /**
- * Cached objects indexed by type and ID for efficient lookups during import
- *
- * @var array>
- */
- private array $cachedObjects = [];
-
- /**
- * ArchiMateService constructor
- */
- public function __construct(
- private readonly IAppConfig $config,
- private readonly IRootFolder $rootFolder,
- private readonly IUserSession $userSession,
- private readonly IAppManager $appManager,
- private readonly ContainerInterface $container,
- private readonly LoggerInterface $logger
- ) {
- // Initialize import/export services
- $this->importService = new ArchiMateImportService($logger);
- $this->exportService = new ArchiMateExportService($logger);
- }
-
- private readonly ArchiMateImportService $importService;
- private readonly ArchiMateExportService $exportService;
-
- /**
- * Import ArchiMate file from path with ReactPHP parallel processing
- *
- * @todo Create or update a model object during import to store model metadata
- * (name, documentation, properties, identifier) for use during export
- */
- public function importArchiMateFileFromPath(array $options = []): array
- {
- // Atomic check and lock to prevent concurrent imports
- if (!$this->acquireImportLock()) {
- $currentStatus = $this->getArchiMateStatus();
- $errorMessage = 'Another ArchiMate operation is already in progress';
-
- if ($this->isImportInProgress()) {
- $errorMessage = 'An ArchiMate import is already in progress';
- $this->logger->warning('ArchiMate import blocked: import already running', [
- 'current_import_status' => $currentStatus['import'] ?? null,
- 'request_options' => $options
- ]);
- } elseif ($this->isExportInProgress()) {
- $errorMessage = 'An ArchiMate export is already in progress';
- $this->logger->warning('ArchiMate import blocked: export already running', [
- 'current_export_status' => $currentStatus['export'] ?? null,
- 'request_options' => $options
- ]);
- } else {
- $this->logger->warning('ArchiMate import blocked: unknown operation in progress', [
- 'current_status' => $currentStatus,
- 'request_options' => $options
- ]);
- }
-
- return [
- 'success' => false,
- 'error' => $errorMessage,
- 'current_status' => $currentStatus,
- 'blocked_at' => date('Y-m-d H:i:s')
- ];
- }
-
- $startTime = microtime(true);
- $startMemory = memory_get_usage(true);
-
- // Initialize import status
- $importStatus = [
- 'status' => 'running',
- 'start_time' => date('Y-m-d H:i:s'),
- 'progress' => 0,
- 'current_step' => 'Initializing',
- 'file_info' => [
- 'name' => $options['fileName'] ?? 'unknown',
- 'size' => 0
- ],
- 'model_info' => [
- 'identifier' => '',
- 'name' => '',
- 'action' => '' // 'created' or 'updated'
- ],
- 'statistics' => [
- 'elements_processed' => 0,
- 'relationships_processed' => 0,
- 'views_processed' => 0,
- 'properties_found' => 0,
- 'objects_created' => 0,
- 'objects_updated' => 0,
- 'objects_skipped' => 0,
- 'errors' => []
- ],
- 'schema_progress' => [
- 'elements' => ['found' => 0, 'created' => 0, 'updated' => 0, 'skipped' => 0, 'progress' => 0],
- 'relationships' => ['found' => 0, 'created' => 0, 'updated' => 0, 'skipped' => 0, 'progress' => 0],
- 'views' => ['found' => 0, 'created' => 0, 'updated' => 0, 'skipped' => 0, 'progress' => 0],
- 'organizations' => ['found' => 0, 'created' => 0, 'updated' => 0, 'skipped' => 0, 'progress' => 0],
- 'property_definitions' => ['found' => 0, 'created' => 0, 'updated' => 0, 'skipped' => 0, 'progress' => 0]
- ]
- ];
-
- $this->setArchiMateImportStatus($importStatus);
-
- $this->logger->info('=== ARCHIMATE IMPORT START ===', [
- 'file_path' => $options['filePath'] ?? 'unknown',
- 'file_name' => $options['fileName'] ?? 'unknown',
- 'start_memory_mb' => round($startMemory / 1024 / 1024, 2),
- 'memory_limit' => ini_get('memory_limit')
- ]);
-
- // Set default options based on processing mode
- $processingMode = $options['processingMode'] ?? 'speed';
-
- if ($processingMode === 'speed') {
- // High-performance defaults
- $defaultOptions = [
- 'batch_size' => 100, // Larger batches for better throughput
- 'parallel_batches' => 4, // Process 4 batches concurrently
- 'updateExisting' => true,
- 'preserveIds' => true,
- 'deleteOrphaned' => false
- ];
- } else {
- // Memory-efficient defaults
- $defaultOptions = [
- 'batch_size' => 50, // Smaller batches for memory efficiency
- 'parallel_batches' => 2, // Fewer concurrent batches
- 'updateExisting' => true,
- 'preserveIds' => true,
- 'deleteOrphaned' => false
- ];
- }
-
- $options = array_merge($defaultOptions, $options);
-
- try {
- // Step 1: Validate file
- $validationStart = microtime(true);
- $importStatus['current_step'] = 'Validating file';
- $importStatus['progress'] = 5;
- $this->setArchiMateImportStatus($importStatus);
-
- $this->validateArchiMateFileFromPath(
- $options['filePath'],
- $options['fileName'],
- $options['mimeType'] ?? 'text/xml'
- );
- $validationTime = microtime(true) - $validationStart;
-
- $this->logger->info('File validation completed', [
- 'validation_time_seconds' => round($validationTime, 3),
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- // Step 2: Parse XML with streaming
- $parseStart = microtime(true);
- $importStatus['current_step'] = 'Parsing XML file';
- $importStatus['progress'] = 15;
- $this->setArchiMateImportStatus($importStatus);
-
- $archiMateData = $this->parseArchiMateXmlStreaming($options['filePath']);
- $parseTime = microtime(true) - $parseStart;
-
- // Update file info and statistics
- $importStatus['file_info']['size'] = filesize($options['filePath']);
- $importStatus['statistics']['elements_processed'] = count($archiMateData['elements'] ?? []);
- $importStatus['statistics']['relationships_processed'] = count($archiMateData['relationships'] ?? []);
- $importStatus['statistics']['views_processed'] = count($archiMateData['views'] ?? []);
-
- // Count property definitions (separate objects) and model properties (metadata)
- $propertyDefinitionsCount = count($archiMateData['property_definitions'] ?? []);
- $modelPropertiesCount = 0;
- if (isset($archiMateData['model_metadata']['properties'])) {
- $modelPropertiesCount = count($archiMateData['model_metadata']['properties']);
- }
- $importStatus['statistics']['property_definitions_found'] = $propertyDefinitionsCount;
- $importStatus['statistics']['model_properties_found'] = $modelPropertiesCount;
-
- // Initialize schema progress with found counts
- $importStatus['schema_progress']['elements']['found'] = count($archiMateData['elements'] ?? []);
- $importStatus['schema_progress']['relationships']['found'] = count($archiMateData['relationships'] ?? []);
- $importStatus['schema_progress']['views']['found'] = count($archiMateData['views'] ?? []);
- $importStatus['schema_progress']['organizations']['found'] = count($archiMateData['organizations'] ?? []);
- $importStatus['schema_progress']['property_definitions']['found'] = $propertyDefinitionsCount;
-
- $importStatus['progress'] = 25;
- $this->setArchiMateImportStatus($importStatus);
-
- $this->logger->info('XML parsing completed', [
- 'parse_time_seconds' => round($parseTime, 3),
- 'elements_count' => count($archiMateData['elements'] ?? []),
- 'relationships_count' => count($archiMateData['relationships'] ?? []),
- 'views_count' => count($archiMateData['views'] ?? []),
- 'property_definitions_count' => $propertyDefinitionsCount,
- 'model_properties_count' => $modelPropertiesCount,
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- // Step 3: Extract model identifier and create/update model object
- $modelStart = microtime(true);
- $importStatus['current_step'] = 'Processing model metadata';
- $importStatus['progress'] = 30;
- $this->setArchiMateImportStatus($importStatus);
-
- $modelIdentifier = $archiMateData['model_metadata']['identifier'] ?? '';
- $modelName = $archiMateData['model_metadata']['name'] ?? '';
-
- // Update model info in import status
- $importStatus['model_info']['identifier'] = $modelIdentifier;
- $importStatus['model_info']['name'] = $modelName;
-
- if (!empty($modelIdentifier)) {
- $this->logger->info('ArchiMateService: Processing model metadata', [
- 'model_identifier' => $modelIdentifier,
- 'model_name' => $modelName
- ]);
-
- $modelResult = $this->createOrUpdateModelObject($archiMateData['model_metadata']);
- if ($modelResult['success']) {
- $importStatus['model_info']['action'] = $modelResult['action'] ?? 'unknown';
- $this->logger->info('ArchiMateService: Model object processed successfully', [
- 'action' => $modelResult['action'],
- 'object_id' => $modelResult['object_id'] ?? 'new'
- ]);
- } else {
- $this->logger->warning('ArchiMateService: Failed to process model object', [
- 'error' => $modelResult['error']
- ]);
- }
-
- // Add model identifier to options for all object handlers
- $options['model_identifier'] = $modelIdentifier;
- } else {
- $this->logger->warning('ArchiMateService: No model identifier found in imported data');
- }
-
- // Update import status with model info
- $this->setArchiMateImportStatus($importStatus);
-
- $modelTime = microtime(true) - $modelStart;
-
- // Step 4: Convert to OpenRegister objects with ReactPHP parallel processing
- $convertStart = microtime(true);
- $importStatus['current_step'] = 'Converting to OpenRegister objects';
- $importStatus['progress'] = 35;
- $this->setArchiMateImportStatus($importStatus);
-
- // Create a thread-safe callback for status updates during processing
- $statusUpdateCallback = function($schemaType, $progress, $stats) use (&$importStatus) {
- // Use atomic update mechanism to prevent race conditions
- $this->updateSchemaStatsSafely($schemaType, $stats);
- };
-
- // Choose processing method based on user preference and dataset size
- $totalObjects = count($archiMateData['elements'] ?? []) +
- count($archiMateData['relationships'] ?? []) +
- count($archiMateData['organizations'] ?? []) +
- count($archiMateData['views'] ?? []) +
- count($archiMateData['property_definitions'] ?? []);
-
- $processingMode = $options['processingMode'] ?? 'speed';
-
- // Use synchronous processing for better debugging
- $this->logger->info('Using synchronous processing method', [
- 'total_objects' => $totalObjects,
- 'processing_mode' => 'synchronous',
- 'method' => 'synchronous-batch-processing'
- ]);
-
- // Convert ArchiMate data to OpenRegister objects using synchronous processing
- $convertResults = $this->convertToOpenRegisterObjectsSynchronous($archiMateData, $options, $statusUpdateCallback);
- $convertTime = microtime(true) - $convertStart;
-
- $totalTime = microtime(true) - $startTime;
- $endMemory = memory_get_usage(true);
- $peakMemory = memory_get_peak_usage(true);
-
- // Update final statistics
- $importStatus['statistics']['objects_created'] = $convertResults['objects_created'];
- $importStatus['statistics']['objects_updated'] = $convertResults['objects_updated'];
- $importStatus['statistics']['objects_skipped'] = $convertResults['objects_skipped'];
- $importStatus['statistics']['errors'] = $convertResults['errors'];
- $importStatus['progress'] = 100;
- $importStatus['status'] = 'completed';
- $importStatus['current_step'] = 'Import completed';
- $importStatus['end_time'] = date('Y-m-d H:i:s');
- $importStatus['total_time_seconds'] = round($totalTime, 3);
-
- // Update schema_progress with final results
- if (isset($convertResults['schema_statistics']) && is_array($convertResults['schema_statistics'])) {
- foreach ($convertResults['schema_statistics'] as $schemaType => $stats) {
- if (isset($importStatus['schema_progress'][$schemaType])) {
- $importStatus['schema_progress'][$schemaType]['created'] = $stats['created'] ?? 0;
- $importStatus['schema_progress'][$schemaType]['updated'] = $stats['updated'] ?? 0;
- $importStatus['schema_progress'][$schemaType]['skipped'] = $stats['skipped'] ?? 0;
-
- // Update progress to 100% for completed schemas
- $importStatus['schema_progress'][$schemaType]['progress'] = 100;
- }
- }
- }
-
- // Add final results to the status for frontend display
- $importStatus['final_results'] = [
- 'summary' => [
- 'total_objects_created' => $convertResults['objects_created'],
- 'total_objects_updated' => $convertResults['objects_updated'],
- 'total_objects_deleted' => $convertResults['objects_deleted'],
- 'total_objects_skipped' => $convertResults['objects_skipped'],
- 'total_errors' => count($convertResults['errors'])
- ],
- 'performance_metrics' => [
- 'items_per_second' => $this->calculateItemsPerSecond($archiMateData, $totalTime),
- 'processing_method' => 'synchronous_batch_processing',
- 'batch_size_used' => $options['batch_size'],
- 'dataset_size' => $totalObjects
- ],
- 'processing_times' => [
- 'total_time_seconds' => round($totalTime, 3),
- 'validation_time_seconds' => round($validationTime, 3),
- 'parse_time_seconds' => round($parseTime, 3),
- 'convert_time_seconds' => round($convertTime, 3),
- ],
- 'file_info' => [
- 'name' => $options['fileName'],
- 'size' => filesize($options['filePath']),
- 'mime_type' => $options['mimeType'] ?? 'text/xml'
- ],
- 'schema_statistics' => $convertResults['schema_statistics']
- ];
-
- $this->setArchiMateImportStatus($importStatus);
-
- $results = [
- 'success' => true,
- 'file_info' => [
- 'name' => $options['fileName'],
- 'size' => filesize($options['filePath']),
- 'mime_type' => $options['mimeType'] ?? 'text/xml'
- ],
- 'model_info' => [
- 'identifier' => $modelIdentifier,
- 'exists' => !empty($modelIdentifier),
- 'action' => $importStatus['model_info']['action'] ?? 'unknown'
- ],
- 'imported_objects' => $convertResults['objects_created'] + $convertResults['objects_updated'],
- 'round_trip_fidelity' => 'enabled', // Indicates complete XML data is stored
- 'storage_format' => 'json_blob', // Shows data is stored as JSON blob
- 'processing_times' => [
- 'total_time_seconds' => round($totalTime, 3),
- 'validation_time_seconds' => round($validationTime, 3),
- 'parse_time_seconds' => round($parseTime, 3),
- 'convert_time_seconds' => round($convertTime, 3),
- 'performance_breakdown' => [
- 'validation_percent' => round(($validationTime / $totalTime) * 100, 1),
- 'parse_percent' => round(($parseTime / $totalTime) * 100, 1),
- 'convert_percent' => round(($convertTime / $totalTime) * 100, 1)
- ]
- ],
- 'memory_usage' => [
- 'start_mb' => round($startMemory / 1024 / 1024, 2),
- 'end_mb' => round($endMemory / 1024 / 1024, 2),
- 'peak_mb' => round($peakMemory / 1024 / 1024, 2),
- 'total_used_mb' => round(($endMemory - $startMemory) / 1024 / 1024, 2)
- ],
- 'statistics' => $convertResults['schema_statistics'],
- 'summary' => [
- 'total_objects_created' => $convertResults['objects_created'],
- 'total_objects_updated' => $convertResults['objects_updated'],
- 'total_objects_deleted' => $convertResults['objects_deleted'],
- 'total_objects_skipped' => $convertResults['objects_skipped'],
- 'total_errors' => count($convertResults['errors'])
- ],
- 'performance_metrics' => [
- 'items_per_second' => $this->calculateItemsPerSecond($archiMateData, $totalTime),
- 'processing_method' => 'synchronous_batch_processing',
- 'batch_size_used' => $options['batch_size'],
- 'dataset_size' => $totalObjects
- ]
- ];
-
- $this->logger->info('=== ARCHIMATE IMPORT COMPLETED ===', [
- 'total_time_seconds' => round($totalTime, 3),
- 'objects_created' => $convertResults['objects_created'],
- 'objects_updated' => $convertResults['objects_updated'],
- 'objects_skipped' => $convertResults['objects_skipped'],
- 'errors_count' => count($convertResults['errors'])
- ]);
-
- // Keep import status with 'completed' status so frontend can display final results
- // Don't clear the status - the frontend will handle showing completed results
- $this->logger->info('ArchiMate import completed successfully - status preserved for frontend display');
-
- return $results;
-
- } catch (\Exception $e) {
- $totalTime = microtime(true) - $startTime;
-
- // Update status with error before releasing lock
- $importStatus['status'] = 'failed';
- $importStatus['current_step'] = 'Import failed';
- $importStatus['end_time'] = date('Y-m-d H:i:s');
- $importStatus['error'] = $e->getMessage();
- $importStatus['total_time_seconds'] = round($totalTime, 3);
- $this->setArchiMateImportStatus($importStatus);
-
- $this->logger->error('=== ARCHIMATE IMPORT FAILED ===', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString(),
- 'total_time_seconds' => round($totalTime, 3)
- ]);
-
- // Note: Lock will be released by the failed status, but we could also add:
- // $this->releaseImportLock(); if we want immediate cleanup
-
- return [
- 'success' => false,
- 'error' => $e->getMessage(),
- 'failed_at' => date('Y-m-d H:i:s')
- ];
- }
- }
-
- /**
- * Export OpenRegister objects to ArchiMate format
- */
- public function exportToArchiMate(array $criteria = [], array $options = []): array
- {
- // Check if an operation is already in progress
- if ($this->isOperationInProgress()) {
- $currentStatus = $this->getArchiMateStatus();
- $errorMessage = 'Another ArchiMate operation is already in progress';
-
- if ($this->isImportInProgress()) {
- $errorMessage = 'An ArchiMate import is already in progress';
- } elseif ($this->isExportInProgress()) {
- $errorMessage = 'An ArchiMate export is already in progress';
- }
-
- $this->logger->warning('ArchiMate export blocked: operation already in progress', [
- 'current_status' => $currentStatus
- ]);
-
- return [
- 'success' => false,
- 'error' => $errorMessage,
- 'current_status' => $currentStatus
- ];
- }
-
- $startTime = microtime(true);
-
- // Initialize export status
- $exportStatus = [
- 'status' => 'running',
- 'start_time' => date('Y-m-d H:i:s'),
- 'progress' => 0,
- 'current_step' => 'Initializing export',
- 'criteria' => $criteria,
- 'statistics' => [
- 'objects_found' => 0,
- 'objects_exported' => 0,
- 'xml_size_bytes' => 0,
- 'errors' => []
- ]
- ];
-
- $this->setArchiMateExportStatus($exportStatus);
-
- $this->logger->info('=== ARCHIMATE EXPORT START ===', [
- 'criteria' => $criteria,
- 'options' => $options
- ]);
-
- try {
- // Step 1: Get objects for export
- $exportStatus['current_step'] = 'Retrieving objects from database';
- $exportStatus['progress'] = 25;
- $this->setArchiMateExportStatus($exportStatus);
-
- $objects = $this->getObjectsForExport($criteria);
- $exportStatus['statistics']['objects_found'] = count($objects);
- $exportStatus['progress'] = 50;
- $this->setArchiMateExportStatus($exportStatus);
-
- // Step 2: Convert to ArchiMate format
- $exportStatus['current_step'] = 'Converting to ArchiMate format';
- $exportStatus['progress'] = 75;
- $this->setArchiMateExportStatus($exportStatus);
-
- $archiMateData = $this->convertFromOpenRegisterObjects($objects, $options);
-
- // Step 3: Generate XML file
- $exportStatus['current_step'] = 'Generating XML file';
- $exportStatus['progress'] = 90;
- $this->setArchiMateExportStatus($exportStatus);
-
- $xmlContent = $this->generateArchiMateXml($archiMateData);
-
- $totalTime = microtime(true) - $startTime;
-
- // Update final status
- $exportStatus['statistics']['objects_exported'] = count($objects);
- $exportStatus['statistics']['xml_size_bytes'] = strlen($xmlContent);
- $exportStatus['progress'] = 100;
- $exportStatus['status'] = 'completed';
- $exportStatus['current_step'] = 'Export completed';
- $exportStatus['end_time'] = date('Y-m-d H:i:s');
- $exportStatus['total_time_seconds'] = round($totalTime, 3);
-
- // Add final results to the status for frontend display
- $exportStatus['final_results'] = [
- 'summary' => [
- 'objects_exported' => count($objects),
- 'xml_size_bytes' => strlen($xmlContent),
- 'xml_size_mb' => round(strlen($xmlContent) / 1024 / 1024, 2)
- ],
- 'performance_metrics' => [
- 'total_time_seconds' => round($totalTime, 3),
- 'objects_per_second' => count($objects) > 0 ? round(count($objects) / $totalTime, 2) : 0
- ],
- 'file_info' => [
- 'name' => $fileName,
- 'size_bytes' => strlen($xmlContent)
- ]
- ];
-
- $this->setArchiMateExportStatus($exportStatus);
-
- $this->logger->info('=== ARCHIMATE EXPORT COMPLETED ===', [
- 'total_time_seconds' => round($totalTime, 3),
- 'objects_exported' => count($objects),
- 'xml_size_bytes' => strlen($xmlContent)
- ]);
-
- // Save the exported file to user's folder for download
- $fileName = 'archimate_export_' . date('Y-m-d_H-i-s') . '.xml';
- try {
- $userFolder = $this->rootFolder->getUserFolder($this->userSession->getUser()->getUID());
-
- // Create or overwrite the file
- if ($userFolder->nodeExists($fileName)) {
- $file = $userFolder->get($fileName);
- $file->putContent($xmlContent);
- } else {
- $userFolder->newFile($fileName, $xmlContent);
- }
-
- $this->logger->info('ArchiMate export file saved', [
- 'file_name' => $fileName,
- 'file_size' => strlen($xmlContent)
- ]);
-
- } catch (\Exception $fileException) {
- $this->logger->error('Failed to save ArchiMate export file', [
- 'file_name' => $fileName,
- 'error' => $fileException->getMessage()
- ]);
- // Continue anyway, return the content directly
- }
-
- // Keep export status with 'completed' status so frontend can display final results
- // Don't clear the status - the frontend will handle showing completed results
- $this->logger->info('ArchiMate export completed successfully - status preserved for frontend display');
-
- return [
- 'success' => true,
- 'xml_content' => $xmlContent,
- 'file_name' => $fileName,
- 'statistics' => [
- 'objects_exported' => count($objects),
- 'xml_size_bytes' => strlen($xmlContent)
- ]
- ];
-
- } catch (\Exception $e) {
- $totalTime = microtime(true) - $startTime;
-
- // Update status with error
- $exportStatus['status'] = 'failed';
- $exportStatus['current_step'] = 'Export failed';
- $exportStatus['end_time'] = date('Y-m-d H:i:s');
- $exportStatus['error'] = $e->getMessage();
- $exportStatus['total_time_seconds'] = round($totalTime, 3);
- $this->setArchiMateExportStatus($exportStatus);
-
- $this->logger->error('=== ARCHIMATE EXPORT FAILED ===', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString(),
- 'total_time_seconds' => round($totalTime, 3)
- ]);
-
- return [
- 'success' => false,
- 'error' => $e->getMessage()
- ];
- }
- }
-
- /**
- * Validate ArchiMate file from path
- */
- private function validateArchiMateFileFromPath(string $filePath, string $fileName, string $mimeType): void
- {
- $this->logger->info('Validating ArchiMate file', [
- 'file_path' => $filePath,
- 'file_name' => $fileName,
- 'mime_type' => $mimeType
- ]);
-
- if (!file_exists($filePath)) {
- throw new \RuntimeException("File not found: {$filePath}");
- }
-
- if (!is_readable($filePath)) {
- throw new \RuntimeException("File not readable: {$filePath}");
- }
-
- $fileSize = filesize($filePath);
- if ($fileSize === false || $fileSize === 0) {
- throw new \RuntimeException("Invalid file size: {$filePath}");
- }
-
- $this->logger->info('File validation passed', [
- 'file_size_bytes' => $fileSize,
- 'file_size_mb' => round($fileSize / 1024 / 1024, 2)
- ]);
- }
-
- /**
- * Parse ArchiMate XML file using streaming approach
- */
- private function parseArchiMateXmlStreaming(string $filePath): array
- {
- $this->logger->info('=== XML PARSING START ===', [
- 'file_path' => $filePath,
- 'file_size_bytes' => filesize($filePath),
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- $content = file_get_contents($filePath);
- if ($content === false) {
- throw new \RuntimeException('Could not read file content');
- }
-
- $this->logger->info('File content loaded', [
- 'content_length' => strlen($content),
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- // Load XML with error handling
- libxml_use_internal_errors(true);
- $xml = simplexml_load_string($content);
-
- if ($xml === false) {
- $errors = libxml_get_errors();
- $errorMessages = array_map(fn($error) => trim($error->message), $errors);
- throw new \RuntimeException('Invalid XML format: ' . implode(', ', $errorMessages));
- }
-
- $this->logger->info('XML loaded successfully', [
- 'xml_name' => $xml->getName(),
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- // Parse XML structure
- $data = $this->parseXmlElementWithProperties($xml);
-
- $this->logger->info('XML parsed to data structure', [
- 'data_keys' => array_keys($data),
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- // Debug: Log the structure of elements to see what we're working with
- if (isset($data['elements'])) {
- $this->logger->info('Elements structure debug', [
- 'elements_type' => gettype($data['elements']),
- 'elements_count' => is_array($data['elements']) ? count($data['elements']) : 'not_array',
- 'first_element_sample' => is_array($data['elements']) && !empty($data['elements']) ? array_slice($data['elements'], 0, 1, true) : 'no_elements'
- ]);
- }
-
- // Debug: Log the structure of views to see what we're working with
- if (isset($data['views'])) {
- $this->logger->info('Views structure debug', [
- 'views_type' => gettype($data['views']),
- 'views_count' => is_array($data['views']) ? count($data['views']) : 'not_array',
- 'views_keys' => is_array($data['views']) ? array_keys($data['views']) : 'not_array',
- 'first_view_sample' => is_array($data['views']) && !empty($data['views']) ? array_slice($data['views'], 0, 1, true) : 'no_views'
- ]);
- } else {
- $this->logger->warning('No views found in parsed XML data', [
- 'data_keys' => array_keys($data)
- ]);
- }
-
- // Normalize and extract ArchiMate components
- $normalizedData = $this->normalizeArchiMateData($data);
-
- $this->logger->info('=== XML PARSING COMPLETED ===', [
- 'elements_count' => count($normalizedData['elements'] ?? []),
- 'relationships_count' => count($normalizedData['relationships'] ?? []),
- 'organizations_count' => count($normalizedData['organizations'] ?? []),
- 'views_count' => count($normalizedData['views'] ?? []),
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- return $normalizedData;
- }
-
- /**
- * Parse XML element preserving attributes and child elements - USING NEW IMPORT SERVICE
- */
- private function parseXmlElementWithProperties(\SimpleXMLElement $xml): array
- {
- // Use our new import service for consistent XML-to-array conversion
- return $this->importService->xmlToArray($xml);
- }
-
- /**
- * Normalize ArchiMate data to consistent format - SIMPLIFIED RAW APPROACH
- */
- private function normalizeArchiMateData(array $data): array
- {
- $this->logger->info('=== NORMALIZING ARCHIMATE DATA (SIMPLIFIED) ===', [
- 'data_keys' => array_keys($data),
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- $normalized = [
- 'elements' => [],
- 'relationships' => [],
- 'organizations' => [],
- 'views' => [],
- 'property_definitions' => [],
- 'model_metadata' => []
- ];
-
- // Extract model metadata (name, documentation, properties, identifier)
- if (isset($data['_attributes'])) {
- $normalized['model_metadata']['identifier'] = $data['_attributes']['identifier'] ?? '';
- $normalized['model_metadata']['attributes'] = $data['_attributes'];
- }
-
- if (isset($data['name'])) {
- $normalized['model_metadata']['name'] = $data['name']['_value'] ?? '';
- }
-
- if (isset($data['documentation'])) {
- $normalized['model_metadata']['documentation'] = $data['documentation']['_value'] ?? '';
- }
-
- // Store all model-level properties and folders as raw data for round-tripping
- $normalized['model_metadata']['properties'] = [];
- if (isset($data['properties'])) {
- $normalized['model_metadata']['properties']['properties'] = $data['properties'];
- }
- if (isset($data['folder'])) {
- $normalized['model_metadata']['properties']['folders'] = $data['folder'];
- }
-
- // SIMPLIFIED: Just extract raw XML nodes for each type and store as-is
- $this->extractRawXmlNodes($data, $normalized, 'elements', 'element');
- $this->extractRawXmlNodes($data, $normalized, 'relationships', 'relationship');
- $this->extractRawXmlNodes($data, $normalized, 'views', 'view');
- $this->extractRawXmlNodes($data, $normalized, 'organizations', 'item');
- $this->extractRawXmlNodes($data, $normalized, 'property_definitions', 'propertyDefinition');
-
- $this->logger->info('=== NORMALIZATION COMPLETED (SIMPLIFIED) ===', [
- 'elements_count' => count($normalized['elements']),
- 'relationships_count' => count($normalized['relationships']),
- 'organizations_count' => count($normalized['organizations']),
- 'views_count' => count($normalized['views']),
- 'property_definitions_count' => count($normalized['property_definitions']),
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- return $normalized;
- }
-
- /**
- * Extract raw XML nodes and store them with their identifier as key
- *
- * @param array $data The parsed XML data
- * @param array $normalized The normalized data structure to populate
- * @param string $section The section name (elements, relationships, etc.)
- * @param string $childTag The child tag name (element, relationship, etc.)
- */
- private function extractRawXmlNodes(array $data, array &$normalized, string $section, string $childTag): void
- {
- if (!isset($data[$section])) {
- return;
- }
-
- $sectionData = $data[$section];
- $items = [];
-
- // Handle different structures
- if (isset($sectionData[$childTag])) {
- // Structure: section -> childTag -> [items...]
- $items = is_array($sectionData[$childTag]) ? $sectionData[$childTag] : [$sectionData[$childTag]];
- } else {
- // Direct array structure: section -> [items...]
- $items = is_array($sectionData) ? $sectionData : [$sectionData];
- }
-
- foreach ($items as $index => $item) {
- // Get identifier from various possible locations
- $identifier = null;
- if (isset($item['_attributes']['identifier'])) {
- $identifier = $item['_attributes']['identifier'];
- } elseif (isset($item['_identifier'])) {
- $identifier = $item['_identifier'];
- } elseif (isset($item['identifier'])) {
- $identifier = $item['identifier'];
- }
-
- if ($identifier) {
- // Store the complete raw XML data structure
- $normalized[$section][$identifier] = [
- 'xml_data' => $item,
- 'identifier' => $identifier,
- 'section' => $section,
- 'child_tag' => $childTag
- ];
-
- $this->logger->debug("Extracted raw XML for {$section}", [
- 'identifier' => $identifier,
- 'keys' => array_keys($item)
- ]);
- } else {
- $this->logger->warning("Missing identifier in {$section} item {$index}", [
- 'item_keys' => array_keys($item),
- 'item_structure' => array_slice($item, 0, 3) // First 3 keys for debugging
- ]);
- }
- }
-
- $this->logger->info("Extracted {$section} raw XML nodes", [
- 'count' => count($normalized[$section])
- ]);
- }
-
- /**
- * Convert normalized ArchiMate data to OpenRegister objects - SIMPLIFIED VERSION
- *
- * @param array $data Normalized ArchiMate data
- * @param array $options Processing options
- * @return array Converted OpenRegister objects
- */
- private function convertToOpenRegisterObjects(array $data, array $options = []): array
- {
- $this->logger->info('Processing elements for normalization', [
- 'elements_count' => count($data['elements'])
- ]);
-
- // Handle different element structures
- if (isset($data['elements']['element'])) {
- // Structure: elements -> element -> [0, 1, 2, ...]
- $elementArray = $data['elements']['element'];
- $this->logger->info('Found element array structure', [
- 'element_count' => count($elementArray)
- ]);
-
- foreach ($elementArray as $index => $element) {
- $this->logger->info("Processing element {$index}", [
- 'element_keys' => array_keys($element),
- 'has_attributes' => isset($element['_attributes']),
- 'has_identifier' => isset($element['_attributes']['identifier'])
- ]);
-
- if (isset($element['_attributes']['identifier'])) {
- $normalized['elements'][$element['_attributes']['identifier']] = $this->normalizeElement($element);
- } else {
- $this->logger->warning("Element {$index} missing identifier", [
- 'element_structure' => $element
- ]);
- }
- }
- } else {
- // Direct array structure: elements -> [0, 1, 2, ...]
- foreach ($data['elements'] as $index => $element) {
- $this->logger->info("Processing element {$index}", [
- 'element_keys' => array_keys($element),
- 'has_attributes' => isset($element['_attributes']),
- 'has_identifier' => isset($element['_attributes']['identifier'])
- ]);
-
- if (isset($element['_attributes']['identifier'])) {
- $normalized['elements'][$element['_attributes']['identifier']] = $this->normalizeElement($element);
- } else {
- $this->logger->warning("Element {$index} missing identifier", [
- 'element_structure' => $element
- ]);
- }
- }
- }
-
- // Extract relationships
- if (isset($data['relationships'])) {
- if (isset($data['relationships']['relationship'])) {
- $relationshipArray = $data['relationships']['relationship'];
- foreach ($relationshipArray as $relationship) {
- if (isset($relationship['_attributes']['identifier'])) {
- $normalized['relationships'][$relationship['_attributes']['identifier']] = $this->normalizeRelationship($relationship);
- }
- }
- } else {
- foreach ($data['relationships'] as $relationship) {
- if (isset($relationship['_attributes']['identifier'])) {
- $normalized['relationships'][$relationship['_attributes']['identifier']] = $this->normalizeRelationship($relationship);
- }
- }
- }
- }
-
- // Extract views - handle nested structure under
- if (isset($data['views'])) {
- $this->logger->info('Processing views for normalization', [
- 'views_structure' => gettype($data['views']),
- 'views_keys' => is_array($data['views']) ? array_keys($data['views']) : 'not_array',
- 'views_count' => is_array($data['views']) ? count($data['views']) : 'not_array'
- ]);
-
- // Handle nested structure:
- if (isset($data['views']['diagrams'])) {
- $this->logger->info('Found diagrams structure in views', [
- 'diagrams_structure' => gettype($data['views']['diagrams']),
- 'diagrams_keys' => is_array($data['views']['diagrams']) ? array_keys($data['views']['diagrams']) : 'not_array'
- ]);
-
- if (isset($data['views']['diagrams']['view'])) {
- $viewArray = $data['views']['diagrams']['view'];
- $this->logger->info('Found view array in diagrams structure', [
- 'view_array_count' => is_array($viewArray) ? count($viewArray) : 'not_array',
- 'is_single_view' => !isset($viewArray[0]) && isset($viewArray['_attributes']),
- 'first_view_sample' => is_array($viewArray) && !empty($viewArray) ? array_keys($viewArray) : 'no_views'
- ]);
-
- // Handle single view vs array of views
- if (!isset($viewArray[0]) && isset($viewArray['_attributes'])) {
- // Single view
- if (isset($viewArray['_attributes']['identifier'])) {
- $normalized['views'][$viewArray['_attributes']['identifier']] = $this->normalizeView($viewArray);
- $this->logger->info('Processed single view', [
- 'view_id' => $viewArray['_attributes']['identifier']
- ]);
- }
- } else {
- // Array of views
- foreach ($viewArray as $view) {
- if (isset($view['_attributes']['identifier'])) {
- $normalized['views'][$view['_attributes']['identifier']] = $this->normalizeView($view);
- }
- }
- }
- }
- } else {
- // Direct views structure
- if (isset($data['views']['view'])) {
- $viewArray = $data['views']['view'];
- foreach ($viewArray as $view) {
- if (isset($view['_attributes']['identifier'])) {
- $normalized['views'][$view['_attributes']['identifier']] = $this->normalizeView($view);
- }
- }
- }
- }
- }
-
- // Extract organizations
- if (isset($data['organizations'])) {
- if (isset($data['organizations']['organization'])) {
- $organizationArray = $data['organizations']['organization'];
- foreach ($organizationArray as $organization) {
- if (isset($organization['_attributes']['identifier'])) {
- $normalized['organizations'][$organization['_attributes']['identifier']] = $this->normalizeOrganizationItem($organization);
- }
- }
- } else {
- foreach ($data['organizations'] as $organization) {
- if (isset($organization['_attributes']['identifier'])) {
- $normalized['organizations'][$organization['_attributes']['identifier']] = $this->normalizeOrganizationItem($organization);
- }
- }
- }
- }
-
- // Extract property definitions
- if (isset($data['propertydefinitions'])) {
- if (isset($data['propertydefinitions']['propertydefinition'])) {
- $propertyDefArray = $data['propertydefinitions']['propertydefinition'];
- foreach ($propertyDefArray as $propertyDef) {
- if (isset($propertyDef['_attributes']['identifier'])) {
- $normalized['property_definitions'][$propertyDef['_attributes']['identifier']] = $this->normalizePropertyDefinition($propertyDef);
- }
- }
- } else {
- foreach ($data['propertydefinitions'] as $propertyDef) {
- if (isset($propertyDef['_attributes']['identifier'])) {
- $normalized['property_definitions'][$propertyDef['_attributes']['identifier']] = $this->normalizePropertyDefinition($propertyDef);
- }
- }
- }
- }
-
- // Extract folders
- if (isset($data['folders'])) {
- if (isset($data['folders']['folder'])) {
- $folderArray = $data['folders']['folder'];
- foreach ($folderArray as $folder) {
- if (isset($folder['_attributes']['identifier'])) {
- $normalized['folders'][$folder['_attributes']['identifier']] = $this->normalizeFolder($folder);
- }
- }
- } else {
- foreach ($data['folders'] as $folder) {
- if (isset($folder['_attributes']['identifier'])) {
- $normalized['folders'][$folder['_attributes']['identifier']] = $this->normalizeFolder($folder);
- }
- }
- }
- }
-
- // Extract properties
- if (isset($data['properties'])) {
- $normalized['properties'] = $this->extractProperties($data['properties']);
- }
-
- return $normalized;
- }
-
- /**
- * Convert to OpenRegister objects using synchronous processing with memory optimization
- */
- private function convertToOpenRegisterObjectsSynchronous(array $archiMateData, array $options, callable $statusCallback = null): array
- {
- $startTime = microtime(true);
-
- $this->logger->info('=== SYNCHRONOUS CONVERSION START ===', [
- 'elements_count' => count($archiMateData['elements'] ?? []),
- 'relationships_count' => count($archiMateData['relationships'] ?? []),
- 'organizations_count' => count($archiMateData['organizations'] ?? []),
- 'views_count' => count($archiMateData['views'] ?? []),
- 'property_definitions_count' => count($archiMateData['property_definitions'] ?? []),
- 'batch_size' => $options['batch_size'] ?? 100,
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- // Preload existing objects for fast lookup
- $preloadStart = microtime(true);
- $this->preloadExistingObjects();
- $preloadTime = microtime(true) - $preloadStart;
-
- $this->logger->info('Existing objects preloaded for fast lookup', [
- 'preload_time_seconds' => round($preloadTime, 3),
- 'cached_objects_count' => array_sum(array_map('count', $this->cachedObjects)),
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- // Initialize results structure
- $results = [
- 'objects_created' => 0,
- 'objects_updated' => 0,
- 'objects_deleted' => 0,
- 'objects_skipped' => 0,
- 'errors' => [],
- 'schema_statistics' => [
- 'elements' => ['found' => 0, 'created' => 0, 'updated' => 0, 'deleted' => 0, 'skipped' => 0, 'errors' => []],
- 'organizations' => ['found' => 0, 'created' => 0, 'updated' => 0, 'deleted' => 0, 'skipped' => 0, 'errors' => []],
- 'relationships' => ['found' => 0, 'created' => 0, 'updated' => 0, 'deleted' => 0, 'skipped' => 0, 'errors' => []],
- 'views' => ['found' => 0, 'created' => 0, 'updated' => 0, 'deleted' => 0, 'skipped' => 0, 'errors' => []],
- 'property_definitions' => ['found' => 0, 'created' => 0, 'updated' => 0, 'deleted' => 0, 'skipped' => 0, 'errors' => []]
- ]
- ];
-
- // Process different schema types with high-performance batch processing
- $schemaTypes = ['elements', 'organizations', 'relationships', 'views', 'property_definitions'];
-
- foreach ($schemaTypes as $schemaType) {
- if (!empty($archiMateData[$schemaType])) {
- $this->logger->info("Starting synchronous batch processing of {$schemaType}", [
- 'count' => count($archiMateData[$schemaType]),
- 'batch_size' => $options['batch_size'] ?? 100,
- 'parallel_batches' => $options['parallel_batches'] ?? 4,
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- // Process schema type synchronously
- $schemaStart = microtime(true);
- $schemaResult = $this->processSchemaTypeSynchronous(
- $archiMateData[$schemaType],
- $schemaType,
- $options,
- $statusCallback
- );
- $schemaTime = microtime(true) - $schemaStart;
-
- // Unset processed data to free memory
- unset($archiMateData[$schemaType]);
- $this->logger->info("{$schemaType} array unset from memory", [
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- // Merge results
- $results['objects_created'] += $schemaResult['created'];
- $results['objects_updated'] += $schemaResult['updated'];
- $results['objects_deleted'] += $schemaResult['deleted'] ?? 0;
- $results['objects_skipped'] += $schemaResult['skipped'] ?? 0;
- $results['errors'] = array_merge($results['errors'], $schemaResult['errors']);
- $results['schema_statistics'][$schemaType] = $schemaResult;
-
- // Update status with schema completion
- if ($statusCallback) {
- $statusCallback($schemaType, 100, $schemaResult);
- }
-
- $this->logger->info("Synchronous batch processing completed for {$schemaType}", [
- 'processing_time_seconds' => round($schemaTime, 3),
- 'created' => $schemaResult['created'],
- 'updated' => $schemaResult['updated'],
- 'deleted' => $schemaResult['deleted'] ?? 0,
- 'skipped' => $schemaResult['skipped'] ?? 0,
- 'errors' => count($schemaResult['errors']),
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
- }
- }
-
- $totalTime = microtime(true) - $startTime;
-
- $this->logger->info('=== SYNCHRONOUS CONVERSION COMPLETED ===', [
- 'total_time_seconds' => round($totalTime, 3),
- 'objects_created' => $results['objects_created'],
- 'objects_updated' => $results['objects_updated'],
- 'objects_deleted' => $results['objects_deleted'],
- 'objects_skipped' => $results['objects_skipped'],
- 'total_errors' => count($results['errors']),
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- return $results;
- }
-
- // Helper methods for normalization
- private function normalizeElement(array $element): array
- {
- return [
- 'id' => $element['_attributes']['identifier'] ?? '',
- 'name' => $element['name']['_value'] ?? '',
- 'type' => $element['_attributes']['xsi:type'] ?? '',
- 'documentation' => $element['documentation']['_value'] ?? '',
- 'properties' => $this->extractProperties($element['properties'] ?? [])
- ];
- }
-
- private function normalizeRelationship(array $relationship): array
- {
- // Handle source and target - they can be attributes or child elements
- $source = $relationship['_attributes']['source'] ?? $relationship['source']['_attributes']['ref'] ?? '';
- $target = $relationship['_attributes']['target'] ?? $relationship['target']['_attributes']['ref'] ?? '';
-
- $normalized = [
- 'id' => $relationship['_attributes']['identifier'] ?? '',
- 'name' => $relationship['name']['_value'] ?? '',
- 'type' => $relationship['_attributes']['xsi:type'] ?? '',
- 'documentation' => $relationship['documentation']['_value'] ?? '',
- 'source' => $source,
- 'target' => $target,
- 'properties' => $this->extractProperties($relationship['properties'] ?? [])
- ];
-
- // Capture all additional attributes from the relationship element
- if (isset($relationship['_attributes']) && is_array($relationship['_attributes'])) {
- foreach ($relationship['_attributes'] as $key => $value) {
- // Skip the basic attributes we already captured
- if (!in_array($key, ['identifier', 'xsi:type'])) {
- $normalized[$key] = $value;
- }
- }
- }
-
- return $normalized;
- }
-
- private function normalizeView(array $view): array
- {
- $normalized = [
- 'id' => $view['_attributes']['identifier'] ?? '',
- 'name' => $view['name']['_value'] ?? '',
- 'type' => $view['_attributes']['xsi:type'] ?? '',
- 'documentation' => $view['documentation']['_value'] ?? '',
- 'properties' => $this->extractProperties($view['properties'] ?? [])
- ];
-
- // Capture any additional view-specific data like nodes, connections, etc.
- // This preserves the view structure for round-trip compatibility
- foreach ($view as $key => $value) {
- if (!in_array($key, ['_attributes', 'name', 'documentation', 'properties']) && !empty($value)) {
- $normalized[$key] = $value;
- }
- }
-
- return $normalized;
- }
-
- private function normalizePropertyDefinition(array $propertyDefinition): array
- {
- return [
- 'id' => $propertyDefinition['_attributes']['identifier'] ?? '',
- 'name' => $propertyDefinition['name']['_value'] ?? '',
- 'type' => $propertyDefinition['_attributes']['xsi:type'] ?? '',
- 'documentation' => $propertyDefinition['documentation']['_value'] ?? '',
- 'properties' => $this->extractProperties($propertyDefinition['properties'] ?? [])
- ];
- }
-
- private function extractProperties(array $propertiesData): array
- {
- $properties = [];
- if (isset($propertiesData['property'])) {
- foreach ($propertiesData['property'] as $property) {
- $key = $property['_attributes']['propertyDefinitionRef'] ?? '';
- $value = $property['value']['_value'] ?? '';
- $properties[$key] = $value;
- }
- }
- return $properties;
- }
-
- /**
- * Extract folders from ArchiMate data for storage as model properties
- */
- private function extractFolders(array $foldersData): array
- {
- $folders = [];
-
- // Handle both single folder and array of folders
- if (isset($foldersData['_attributes'])) {
- // Single folder
- $folders[] = $this->normalizeFolder($foldersData);
- } else {
- // Array of folders
- foreach ($foldersData as $folder) {
- if (isset($folder['_attributes'])) {
- $folders[] = $this->normalizeFolder($folder);
- }
- }
- }
-
- return $folders;
- }
-
- /**
- * Normalize a single folder for storage
- */
- private function normalizeFolder(array $folder): array
- {
- $normalized = [
- 'id' => $folder['_attributes']['identifier'] ?? $folder['_attributes']['id'] ?? '',
- 'name' => $folder['_attributes']['name'] ?? $folder['name']['_value'] ?? '',
- 'type' => $folder['_attributes']['type'] ?? '',
- 'documentation' => $folder['documentation']['_value'] ?? '',
- 'properties' => $this->extractProperties($folder['properties'] ?? [])
- ];
-
- // Capture any additional attributes
- if (isset($folder['_attributes']) && is_array($folder['_attributes'])) {
- foreach ($folder['_attributes'] as $key => $value) {
- if (!in_array($key, ['identifier', 'id', 'name', 'type']) && !empty($value)) {
- $normalized[$key] = $value;
- }
- }
- }
-
- return $normalized;
- }
-
- /**
- * Normalize an organization item from the organizations section
- */
- private function normalizeOrganizationItem(array $orgItem): array
- {
- return [
- 'id' => $orgItem['_attributes']['identifier'] ?? '',
- 'name' => $orgItem['label']['_value'] ?? '',
- 'type' => 'Organization',
- 'documentation' => $orgItem['documentation']['_value'] ?? '',
- 'properties' => $this->extractProperties($orgItem['properties'] ?? [])
- ];
- }
-
- private function extractOrganizations(array $elements): array
- {
- $organizations = [];
- foreach ($elements as $element) {
- // Only map BusinessActor to organizations; keep BusinessRole as an element for round-trip fidelity
- if (str_contains($element['type'] ?? '', 'BusinessActor')) {
- $organizations[$element['id']] = $element;
- }
- }
- return $organizations;
- }
-
- // ReactPHP parallel processing methods with memory cleanup
- private function processElementsParallelWithCleanup(array $elements, array $options): Promise
- {
- $deferred = new Deferred();
-
- $this->logger->info('Starting parallel processing of elements with memory cleanup', [
- 'count' => count($elements),
- 'batch_size' => $options['batch_size'],
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- // Process in batches with progressive memory release
- $chunks = array_chunk($elements, $options['batch_size'], true);
- $promises = [];
-
- foreach ($chunks as $chunkIndex => $chunk) {
- $promises[] = $this->processChunkParallelWithCleanup($chunk, $options, 'element');
-
- // Force garbage collection every 5 chunks
- if ($chunkIndex % 5 === 0) {
- gc_collect_cycles();
- $this->logger->info("Garbage collection after chunk {$chunkIndex}", [
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
- }
- }
-
- all($promises)->then(
- function ($results) use ($deferred, $elements) {
- $totalCreated = 0;
- $totalUpdated = 0;
- $totalSkipped = 0;
- $totalErrors = [];
-
- foreach ($results as $result) {
- $totalCreated += $result['created'];
- $totalUpdated += $result['updated'];
- $totalSkipped += $result['skipped'] ?? 0;
- $totalErrors = array_merge($totalErrors, $result['errors']);
- }
-
- // Final cleanup of elements array
- unset($elements);
- gc_collect_cycles();
-
- $deferred->resolve([
- 'created' => $totalCreated,
- 'updated' => $totalUpdated,
- 'skipped' => $totalSkipped,
- 'errors' => $totalErrors
- ]);
- },
- function ($error) use ($deferred) {
- $deferred->reject($error);
- }
- );
-
- return $deferred->promise();
- }
-
- private function processOrganizationsParallelWithCleanup(array $organizations, array $options): Promise
- {
- $deferred = new Deferred();
-
- $this->logger->info('Starting parallel processing of organizations with memory cleanup', [
- 'count' => count($organizations),
- 'batch_size' => $options['batch_size'],
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- // Process in batches with progressive memory release
- $chunks = array_chunk($organizations, $options['batch_size'], true);
- $promises = [];
-
- foreach ($chunks as $chunkIndex => $chunk) {
- $promises[] = $this->processChunkParallelWithCleanup($chunk, $options, 'organization');
-
- // Force garbage collection every 5 chunks
- if ($chunkIndex % 5 === 0) {
- gc_collect_cycles();
- $this->logger->info("Garbage collection after chunk {$chunkIndex}", [
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
- }
- }
-
- all($promises)->then(
- function ($results) use ($deferred, $organizations) {
- $totalCreated = 0;
- $totalUpdated = 0;
- $totalSkipped = 0;
- $totalErrors = [];
-
- foreach ($results as $result) {
- $totalCreated += $result['created'];
- $totalUpdated += $result['updated'];
- $totalSkipped += $result['skipped'] ?? 0;
- $totalErrors = array_merge($totalErrors, $result['errors']);
- }
-
- // Final cleanup of organizations array
- unset($organizations);
- gc_collect_cycles();
-
- $deferred->resolve([
- 'created' => $totalCreated,
- 'updated' => $totalUpdated,
- 'skipped' => $totalSkipped,
- 'errors' => $totalErrors
- ]);
- },
- function ($error) use ($deferred) {
- $deferred->reject($error);
- }
- );
-
- return $deferred->promise();
- }
-
- private function processRelationshipsParallelWithCleanup(array $relationships, array $options): Promise
- {
- $deferred = new Deferred();
-
- $this->logger->info('Starting parallel processing of relationships with memory cleanup', [
- 'count' => count($relationships),
- 'batch_size' => $options['batch_size'],
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- // Process in batches with progressive memory release
- $chunks = array_chunk($relationships, $options['batch_size'], true);
- $promises = [];
-
- foreach ($chunks as $chunkIndex => $chunk) {
- $promises[] = $this->processChunkParallelWithCleanup($chunk, $options, 'relationship');
-
- // Force garbage collection every 5 chunks
- if ($chunkIndex % 5 === 0) {
- gc_collect_cycles();
- $this->logger->info("Garbage collection after chunk {$chunkIndex}", [
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
- }
- }
-
- all($promises)->then(
- function ($results) use ($deferred, $relationships) {
- $totalCreated = 0;
- $totalUpdated = 0;
- $totalSkipped = 0;
- $totalErrors = [];
-
- foreach ($results as $result) {
- $totalCreated += $result['created'];
- $totalUpdated += $result['updated'];
- $totalSkipped += $result['skipped'] ?? 0;
- $totalErrors = array_merge($totalErrors, $result['errors']);
- }
-
- // Final cleanup of relationships array
- unset($relationships);
- gc_collect_cycles();
-
- $deferred->resolve([
- 'created' => $totalCreated,
- 'updated' => $totalUpdated,
- 'skipped' => $totalSkipped,
- 'errors' => $totalErrors
- ]);
- },
- function ($error) use ($deferred) {
- $deferred->reject($error);
- }
- );
-
- return $deferred->promise();
- }
-
- private function processViewsParallelWithCleanup(array $views, array $options): Promise
- {
- $deferred = new Deferred();
-
- $this->logger->info('Starting parallel processing of views with memory cleanup', [
- 'count' => count($views),
- 'batch_size' => $options['batch_size'],
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- // Process in batches with progressive memory release
- $chunks = array_chunk($views, $options['batch_size'], true);
- $promises = [];
-
- foreach ($chunks as $chunkIndex => $chunk) {
- $promises[] = $this->processChunkParallelWithCleanup($chunk, $options, 'view');
-
- // Force garbage collection every 5 chunks
- if ($chunkIndex % 5 === 0) {
- gc_collect_cycles();
- $this->logger->info("Garbage collection after chunk {$chunkIndex}", [
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
- }
- }
-
- all($promises)->then(
- function ($results) use ($deferred, $views) {
- $totalCreated = 0;
- $totalUpdated = 0;
- $totalSkipped = 0;
- $totalErrors = [];
-
- foreach ($results as $result) {
- $totalCreated += $result['created'];
- $totalUpdated += $result['updated'];
- $totalSkipped += $result['skipped'] ?? 0;
- $totalErrors = array_merge($totalErrors, $result['errors']);
- }
-
- // Final cleanup of views array
- unset($views);
- gc_collect_cycles();
-
- $deferred->resolve([
- 'created' => $totalCreated,
- 'updated' => $totalUpdated,
- 'skipped' => $totalSkipped,
- 'errors' => $totalErrors
- ]);
- },
- function ($error) use ($deferred) {
- $deferred->reject($error);
- }
- );
-
- return $deferred->promise();
- }
-
- private function processPropertyDefinitionsParallelWithCleanup(array $propertyDefinitions, array $options): Promise
- {
- $deferred = new Deferred();
-
- $this->logger->info('Starting parallel processing of property definitions with memory cleanup', [
- 'count' => count($propertyDefinitions),
- 'batch_size' => $options['batch_size'],
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- // Process in batches with progressive memory release
- $chunks = array_chunk($propertyDefinitions, $options['batch_size'], true);
- $promises = [];
-
- foreach ($chunks as $chunkIndex => $chunk) {
- $promises[] = $this->processChunkParallelWithCleanup($chunk, $options, 'property_definition');
-
- // Force garbage collection every 5 chunks
- if ($chunkIndex % 5 === 0) {
- gc_collect_cycles();
- $this->logger->info("Garbage collection after chunk {$chunkIndex}", [
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
- }
- }
-
- all($promises)->then(
- function ($results) use ($deferred, $propertyDefinitions) {
- $totalCreated = 0;
- $totalUpdated = 0;
- $totalSkipped = 0;
- $totalErrors = [];
-
- foreach ($results as $result) {
- $totalCreated += $result['created'];
- $totalUpdated += $result['updated'];
- $totalSkipped += $result['skipped'] ?? 0;
- $totalErrors = array_merge($totalErrors, $result['errors']);
- }
-
- // Final cleanup of property definitions array
- unset($propertyDefinitions);
- gc_collect_cycles();
-
- $deferred->resolve([
- 'created' => $totalCreated,
- 'updated' => $totalUpdated,
- 'skipped' => $totalSkipped,
- 'errors' => $totalErrors
- ]);
- },
- function ($error) use ($deferred) {
- $deferred->reject($error);
- }
- );
-
- return $deferred->promise();
- }
-
- private function processChunkParallelWithCleanup(array $chunk, array $options, string $type): Promise
- {
- $deferred = new Deferred();
-
- $this->logger->info("Processing chunk of {$type}s with memory cleanup", [
- 'chunk_size' => count($chunk),
- 'type' => $type,
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- $created = 0;
- $updated = 0;
- $skipped = 0;
- $errors = [];
- $processedItems = [];
-
- foreach ($chunk as $itemId => $item) {
- try {
- $this->logger->info("Processing {$type} item", [
- 'item_id' => $item['id'],
- 'item_name' => $item['name'] ?? 'unknown'
- ]);
-
- // Check if object already exists
- // Use OpenRegister's built-in duplicate detection via UUID
- // No need for custom findExistingObject logic - OpenRegister handles it automatically
- $this->logger->info("Saving {$type} object (OpenRegister will handle create/update)", [
- 'item_id' => $item['id'],
- 'item_name' => $item['name'] ?? 'unknown'
- ]);
-
- $modelIdentifier = $options['model_identifier'] ?? null;
- $savedObject = $this->saveObject($item, $type, $modelIdentifier);
-
- // Determine if the object was created or updated based on timestamps
- $action = $this->determineObjectAction($savedObject);
- if ($action === 'created') {
- $created++;
- } else {
- $updated++;
- }
-
- // Mark item as processed for cleanup
- $processedItems[] = $itemId;
-
- } catch (\Exception $e) {
- $this->logger->error("Error processing {$type} item", [
- 'item_id' => $item['id'] ?? 'unknown',
- 'error' => $e->getMessage()
- ]);
- $errors[] = $e->getMessage();
- $processedItems[] = $itemId;
- }
- }
-
- // Remove processed items from chunk to free memory
- foreach ($processedItems as $itemId) {
- unset($chunk[$itemId]);
- }
-
- $this->logger->info("Chunk processing completed for {$type}s", [
- 'created' => $created,
- 'updated' => $updated,
- 'skipped' => $skipped,
- 'errors' => count($errors),
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- $deferred->resolve([
- 'created' => $created,
- 'updated' => $updated,
- 'skipped' => $skipped,
- 'errors' => $errors
- ]);
-
- return $deferred->promise();
- }
-
- /**
- * Process a schema type with synchronous batch processing
- *
- * This method uses synchronous processing with batch operations
- * to maintain memory efficiency while being easier to debug.
- *
- * @param array $items Items to process
- * @param string $schemaType Type of schema being processed
- * @param array $options Processing options
- * @param callable|null $statusCallback Status update callback
- * @return array Processing results
- */
- private function processSchemaTypeSynchronous(array $items, string $schemaType, array $options, callable $statusCallback = null): array
- {
- $this->logger->info("Starting synchronous batch processing of {$schemaType}", [
- 'count' => count($items),
- 'batch_size' => $options['batch_size'] ?? 100,
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- // Split items into chunks for processing
- $batchSize = $options['batch_size'] ?? 100;
- $chunks = array_chunk($items, $batchSize, true);
-
- // Process all chunks and collect objects
- $allProcessedObjects = [];
- $totalErrors = [];
- $totalCreated = 0;
- $totalUpdated = 0;
- $totalSaved = 0;
- $schemaId = null;
- $registerId = null;
-
- foreach ($chunks as $chunkIndex => $chunk) {
- $this->logger->debug("Processing chunk {$chunkIndex} of {$schemaType}", [
- 'chunk_size' => count($chunk),
- 'chunk_index' => $chunkIndex,
- 'total_chunks' => count($chunks)
- ]);
-
- $chunkResult = $this->processChunkSynchronous($chunk, $options, $schemaType);
-
- // Collect processed objects
- if (!empty($chunkResult['objects'])) {
- $allProcessedObjects = array_merge($allProcessedObjects, $chunkResult['objects']);
- $schemaId = $chunkResult['schema_id'];
- $registerId = $chunkResult['register_id'];
-
- $this->logger->debug("Chunk {$chunkIndex} processed successfully", [
- 'processed_objects' => count($chunkResult['objects']),
- 'schema_id' => $schemaId,
- 'register_id' => $registerId
- ]);
- } else {
- $this->logger->warning("Chunk {$chunkIndex} produced no objects", [
- 'chunk_size' => count($chunk),
- 'errors' => count($chunkResult['errors'])
- ]);
- }
-
- // Collect errors
- $totalErrors = array_merge($totalErrors, $chunkResult['errors']);
-
- // Update progress
- if ($statusCallback) {
- $progress = min(90, ($chunkIndex / count($chunks)) * 100);
- $statusCallback($schemaType, $progress, ['processing_batch' => $chunkIndex + 1, 'total_batches' => count($chunks)]);
- }
- }
-
- $this->logger->info("All chunks processed for {$schemaType}", [
- 'total_processed_objects' => count($allProcessedObjects),
- 'schema_id' => $schemaId,
- 'register_id' => $registerId,
- 'total_errors' => count($totalErrors)
- ]);
-
- // Perform batch save if we have objects to save
- $totalSaved = 0;
- if (!empty($allProcessedObjects) && $schemaId && $registerId) {
- try {
- $this->logger->info("Performing batch save for {$schemaType}", [
- 'object_count' => count($allProcessedObjects),
- 'schema_id' => $schemaId,
- 'register_id' => $registerId
- ]);
-
- $objectService = $this->getObjectService();
- if ($objectService) {
- $this->logger->debug("ObjectService obtained, calling saveObjects", [
- 'objects_count' => count($allProcessedObjects),
- 'register_id' => $registerId,
- 'schema_id' => $schemaId
- ]);
-
- $saveResult = $objectService->saveObjects(
- objects: $allProcessedObjects,
- register: $registerId,
- schema: $schemaId
- );
-
- // Extract saved objects from the new structured return format
- $savedObjects = array_merge(
- $saveResult['saved'] ?? [],
- $saveResult['updated'] ?? []
- );
-
- // Use actual counts from the structured return instead of analyzing objects
- $actionCounts = [
- 'created' => count($saveResult['saved'] ?? []),
- 'updated' => count($saveResult['updated'] ?? [])
- ];
- $totalSaved = count($savedObjects);
-
- // Store the action counts for return value
- $totalCreated = $actionCounts['created'];
- $totalUpdated = $actionCounts['updated'];
-
- $this->logger->info("Batch save completed for {$schemaType}", [
- 'objects_saved' => $totalSaved,
- 'objects_created' => $totalCreated,
- 'objects_updated' => $totalUpdated,
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
- } else {
- $this->logger->error("ObjectService not available for batch save");
- $totalErrors[] = "ObjectService not available for batch save";
- // Initialize variables for the case where ObjectService is not available
- $totalCreated = 0;
- $totalUpdated = 0;
- }
- } catch (\Exception $e) {
- $this->logger->error("Error during batch save for {$schemaType}", [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString(),
- 'object_count' => count($allProcessedObjects)
- ]);
- $totalErrors[] = "Batch save error: " . $e->getMessage();
- // Initialize variables for the case where an exception occurred
- $totalCreated = 0;
- $totalUpdated = 0;
- }
- } else {
- $this->logger->warning("No objects to save for {$schemaType}", [
- 'processed_objects_count' => count($allProcessedObjects),
- 'schema_id' => $schemaId,
- 'register_id' => $registerId
- ]);
- // Initialize variables for the case where no objects to save
- $totalCreated = 0;
- $totalUpdated = 0;
- }
-
- $this->logger->info("High-performance batch processing completed for {$schemaType}", [
- 'total_processed' => count($allProcessedObjects),
- 'total_saved' => $totalSaved,
- 'total_created' => $totalCreated,
- 'total_updated' => $totalUpdated,
- 'total_errors' => count($totalErrors),
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- return [
- 'created' => $totalCreated,
- 'updated' => $totalUpdated,
- 'skipped' => count($allProcessedObjects) - $totalSaved,
- 'errors' => $totalErrors
- ];
- }
-
- /**
- * Process a chunk of items for batch saving
- *
- * This method processes items and prepares them for batch saving.
- *
- * @param array $chunk Items to process
- * @param array $options Processing options
- * @param string $type Type of items being processed
- * @return array Processed objects ready for batch save
- */
- private function processChunkSynchronous(array $chunk, array $options, string $type): array
- {
- $this->logger->debug("Processing synchronous chunk of {$type}s for batch save", [
- 'chunk_size' => count($chunk),
- 'type' => $type,
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- // Process items and collect them for batch saving
- $processedObjects = [];
- $totalErrors = [];
- $modelIdentifier = $options['model_identifier'] ?? null;
-
- foreach ($chunk as $itemId => $item) {
- $this->logger->debug("Processing item in chunk", [
- 'item_id' => $itemId,
- 'item_name' => $item['name'] ?? 'unknown',
- 'type' => $type
- ]);
-
- $result = $this->processItemSynchronous($item, $type, $modelIdentifier);
-
- if ($result['processed'] && $result['data'] !== null) {
- $processedObjects[] = $result['data'];
- $this->logger->debug("Item processed successfully", [
- 'item_id' => $itemId,
- 'schema_id' => $result['schema_id'],
- 'register_id' => $result['register_id']
- ]);
- } else {
- $this->logger->warning("Item processing failed", [
- 'item_id' => $itemId,
- 'errors' => $result['errors']
- ]);
- $totalErrors = array_merge($totalErrors, $result['errors']);
- }
- }
-
- $this->logger->debug("Synchronous chunk processing completed for {$type}s", [
- 'chunk_size' => count($chunk),
- 'processed_objects' => count($processedObjects),
- 'errors' => count($totalErrors),
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- return [
- 'objects' => $processedObjects,
- 'errors' => $totalErrors,
- 'schema_id' => $processedObjects[0]['@self']['schema'] ?? null,
- 'register_id' => $processedObjects[0]['@self']['register'] ?? null
- ];
- }
-
- /**
- * Process a single item to prepare it for batch saving
- *
- * This method converts ArchiMate data to OpenRegister format
- * and prepares it for batch saving with correct schema/register properties.
- *
- * @param array $item Item to process
- * @param string $type Type of item
- * @param string|null $modelIdentifier Model identifier
- * @return array Processed object data ready for batch save
- */
- private function processItemSynchronous(array $item, string $type, ?string $modelIdentifier): array
- {
- try {
- $this->logger->debug("Processing {$type} item for batch save", [
- 'item_id' => $item['id'],
- 'item_name' => $item['name'] ?? 'unknown'
- ]);
-
- // Convert ArchiMate data to OpenRegister format
- $openRegisterData = $this->convertToOpenRegisterFormat($item, $type, $modelIdentifier);
-
- // Ensure archimate_id is always set
- $openRegisterData['archimate_id'] = $item['id'];
-
- // Get schema and register IDs for this type
- $schemaId = $this->getAmefSchemaIdForType($type);
- $registerId = $this->getAmefRegisterId();
-
- $this->logger->debug("Retrieved schema and register IDs", [
- 'type' => $type,
- 'schema_id' => $schemaId,
- 'register_id' => $registerId
- ]);
-
- // Add schema and register properties for batch processing
- // Include the ArchiMate ID as the UUID in @self.id to preserve the original ID
- $openRegisterData['@self'] = [
- 'id' => $item['id'], // Set the ArchiMate ID as the UUID
- 'schema' => $schemaId,
- 'register' => $registerId
- ];
-
- $this->logger->debug("Item converted successfully", [
- 'item_id' => $item['id'],
- 'openregister_data_keys' => array_keys($openRegisterData),
- 'self_keys' => array_keys($openRegisterData['@self'])
- ]);
-
- return [
- 'data' => $openRegisterData,
- 'schema_id' => $schemaId,
- 'register_id' => $registerId,
- 'archimate_id' => $item['id'],
- 'processed' => true,
- 'errors' => []
- ];
-
- } catch (\Exception $e) {
- $this->logger->error("Error processing {$type} item for batch save", [
- 'item_id' => $item['id'] ?? 'unknown',
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
-
- return [
- 'data' => null,
- 'schema_id' => null,
- 'register_id' => null,
- 'archimate_id' => $item['id'] ?? 'unknown',
- 'processed' => false,
- 'errors' => [$e->getMessage()]
- ];
- }
- }
-
- /**
- * Find existing object by ArchiMate ID and type
- *
- * This method primarily uses the preloaded cache for efficiency.
- * If an object is not found in cache, it will not query the database
- * since all objects should have been preloaded during import initialization.
- */
- private function findExistingObject(string $archiMateId, string $type): ?array
- {
- // Check cache first - this should be the primary lookup method
- if (isset($this->cachedObjects[$type][$archiMateId])) {
- $this->logger->debug("Found existing object in cache", [
- 'archimate_id' => $archiMateId,
- 'archimate_type' => $type
- ]);
- return $this->cachedObjects[$type][$archiMateId];
- }
-
- // If not found in cache, log a warning since all objects should be preloaded
- $this->logger->warning("Object not found in preloaded cache - this may indicate a preloading issue", [
- 'archimate_id' => $archiMateId,
- 'archimate_type' => $type,
- 'cache_keys_available' => array_keys($this->cachedObjects[$type] ?? [])
- ]);
-
- // Note: We don't query the database here since all objects should be preloaded
- // This ensures consistency with our proven object retrieval methods
- return null;
- }
-
- /**
- * Compare two objects to determine if they are equal
- */
- private function areObjectsEqual(array $existingObject, array $newObjectData): bool
- {
- $this->logger->debug("Comparing objects", [
- 'existing_id' => $existingObject['id'] ?? 'unknown',
- 'new_id' => $newObjectData['id'] ?? 'unknown'
- ]);
-
- // Normalize objects for comparison
- $existingNormalized = $this->normalizeObjectForComparison($existingObject, ['id', 'created', 'updated']);
- $newNormalized = $this->normalizeObjectForComparison($newObjectData, ['id', 'created', 'updated']);
-
- // Sort arrays recursively for consistent comparison
- $this->sortArrayRecursively($existingNormalized);
- $this->sortArrayRecursively($newNormalized);
-
- $areEqual = $this->deepArrayCompare($existingNormalized, $newNormalized);
-
- $this->logger->debug("Object comparison result", [
- 'existing_id' => $existingObject['id'] ?? 'unknown',
- 'new_id' => $newObjectData['id'] ?? 'unknown',
- 'are_equal' => $areEqual
- ]);
-
- return $areEqual;
- }
-
- /**
- * Normalize object for comparison by removing specified fields
- */
- private function normalizeObjectForComparison(array $object, array $ignoreFields): array
- {
- $normalized = $object;
-
- foreach ($ignoreFields as $field) {
- unset($normalized[$field]);
- }
-
- return $normalized;
- }
-
- /**
- * Sort array recursively for consistent comparison
- */
- private function sortArrayRecursively(array &$array): void
- {
- foreach ($array as &$value) {
- if (is_array($value)) {
- $this->sortArrayRecursively($value);
- }
- }
-
- if ($this->isAssociativeArray($array)) {
- ksort($array);
- } else {
- sort($array);
- }
- }
-
- /**
- * Check if array is associative
- */
- private function isAssociativeArray(array $array): bool
- {
- if (empty($array)) {
- return false;
- }
-
- return array_keys($array) !== range(0, count($array) - 1);
- }
-
- /**
- * Deep array comparison
- */
- private function deepArrayCompare(array $array1, array $array2): bool
- {
- if (count($array1) !== count($array2)) {
- return false;
- }
-
- foreach ($array1 as $key => $value1) {
- if (!array_key_exists($key, $array2)) {
- return false;
- }
-
- $value2 = $array2[$key];
-
- if (is_array($value1) && is_array($value2)) {
- if (!$this->deepArrayCompare($value1, $value2)) {
- return false;
- }
- } elseif ($value1 !== $value2) {
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * Save (create or update) an object in OpenRegister
- * Uses ArchiMate ID as UUID for automatic duplicate detection and updating
- *
- * @param array $objectData The ArchiMate object data to save
- * @param string $type The type of object being saved (element, organization, etc.)
- * @param string|null $modelIdentifier Optional model identifier
- * @return array The saved object data including @self.created and @self.updated timestamps
- * @throws \Exception If the save operation fails
- */
- private function saveObject(array $objectData, string $type, ?string $modelIdentifier = null): array
- {
- $this->logger->info("Saving {$type} object", [
- 'object_id' => $objectData['id'],
- 'object_name' => $objectData['name'] ?? 'unknown'
- ]);
-
- try {
- $objectService = $this->getObjectService();
- if (!$objectService) {
- throw new \RuntimeException('ObjectService not available');
- }
-
- // Convert ArchiMate data to OpenRegister format
- $openRegisterData = $this->convertToOpenRegisterFormat($objectData, $type, $modelIdentifier);
-
- // Ensure archimate_id is always set as a safety measure
- $openRegisterData['archimate_id'] = $objectData['id'];
-
- // Set the UUID in @self.id to ensure it's properly handled by OpenRegister
- $openRegisterData['@self'] = [
- 'id' => $objectData['id']
- ];
-
- $this->logger->info("Saving object to OpenRegister", [
- 'type' => $type,
- 'archimate_id' => $objectData['id'],
- 'model_identifier' => $modelIdentifier ?? 'none',
- 'will_use_uuid' => $objectData['id'],
- 'openregister_data_keys' => array_keys($openRegisterData),
- 'has_self_id' => isset($openRegisterData['@self']['id'])
- ]);
-
- // Remove schema_id and register_id from the data as they should be passed as separate parameters
- $schemaId = $openRegisterData['schema_id'];
- $registerId = $openRegisterData['register_id'];
- unset($openRegisterData['schema_id'], $openRegisterData['register_id']);
-
- // Ensure IDs are integers for type safety
- $schemaId = (int) $schemaId;
- $registerId = (int) $registerId;
-
- $this->logger->info("Saving object with schema and register IDs", [
- 'schema_id' => $schemaId,
- 'schema_id_type' => gettype($schemaId),
- 'register_id' => $registerId,
- 'register_id_type' => gettype($registerId),
- 'uuid' => $objectData['id'],
- 'uuid_type' => gettype($objectData['id']),
- 'data_keys' => array_keys($openRegisterData),
- 'self_data' => $openRegisterData['@self'] ?? 'not_set'
- ]);
-
- // Create/update the object using ArchiMate ID as UUID for automatic duplicate detection
- $createdObject = $objectService->saveObject(
- object: $openRegisterData,
- extend: [],
- register: $registerId,
- schema: $schemaId,
- uuid: $objectData['id'] // Use ArchiMate ID as UUID for built-in duplicate detection
- );
-
- // Convert ObjectEntity to array for caching and logging
- $createdObjectArray = $createdObject->jsonSerialize();
-
- // Cache the result
- $this->cachedObjects[$type][$objectData['id']] = $createdObjectArray;
-
- $this->logger->info("Object save completed", [
- 'archimate_id' => $objectData['id'],
- 'openregister_uuid' => $createdObjectArray['uuid'] ?? 'unknown',
- 'openregister_id' => $createdObjectArray['id'] ?? 'unknown',
- 'stored_archimate_id' => $createdObjectArray['archimate_id'] ?? 'unknown',
- 'stored_model_id' => $createdObjectArray['model_id'] ?? 'unknown',
- 'stored_model_property' => $createdObjectArray['properties']['model'] ?? 'unknown',
- 'type' => $type,
- 'action' => 'saved (created or updated)',
- 'uuid_matches_archimate_id' => ($createdObjectArray['uuid'] ?? '') === $objectData['id'],
- 'created_timestamp' => $createdObjectArray['@self']['created'] ?? 'unknown',
- 'updated_timestamp' => $createdObjectArray['@self']['updated'] ?? 'unknown'
- ]);
-
- return $createdObjectArray;
- } catch (\Exception $e) {
- $this->logger->error("Error saving {$type} object", [
- 'object_id' => $objectData['id'],
- 'error' => $e->getMessage()
- ]);
- throw $e;
- }
- }
-
- /**
- * Determine if an object was created or updated based on @self.created and @self.updated timestamps
- *
- * @param array $savedObject The saved object containing @self metadata
- * @return string 'created' if object was newly created, 'updated' if it was modified
- */
- private function determineObjectAction(array $savedObject): string
- {
- $created = $savedObject['@self']['created'] ?? null;
- $updated = $savedObject['@self']['updated'] ?? null;
-
- // If both timestamps are exactly the same, the object was created
- // If they differ, the object was updated
- if ($created && $updated && $created === $updated) {
- return 'created';
- } else {
- return 'updated';
- }
- }
-
- /**
- * Analyze a batch of saved objects to determine how many were created vs updated
- *
- * @param array $savedObjects Array of saved objects from saveObjects method
- * @return array Array with 'created' and 'updated' counts
- */
- private function analyzeBatchObjectActions(array $savedObjects): array
- {
- $created = 0;
- $updated = 0;
-
- foreach ($savedObjects as $savedObject) {
- // Convert ObjectEntity to array if needed
- if (is_object($savedObject) && method_exists($savedObject, 'jsonSerialize')) {
- $savedObject = $savedObject->jsonSerialize();
- }
-
- $action = $this->determineObjectAction($savedObject);
- if ($action === 'created') {
- $created++;
- } else {
- $updated++;
- }
- }
-
- return [
- 'created' => $created,
- 'updated' => $updated
- ];
- }
-
- /**
- * Update an existing object in OpenRegister
- */
- private function updateObject(int $objectId, array $objectData, string $type, ?string $modelIdentifier = null): void
- {
- $this->logger->info("Updating {$type} object", [
- 'object_id' => $objectId,
- 'archimate_id' => $objectData['id'],
- 'object_name' => $objectData['name'] ?? 'unknown'
- ]);
-
- try {
- $objectService = $this->getObjectService();
- if (!$objectService) {
- throw new \RuntimeException('ObjectService not available');
- }
-
- // Convert ArchiMate data to OpenRegister format
- $openRegisterData = $this->convertToOpenRegisterFormat($objectData, $type, $modelIdentifier);
- $openRegisterData['id'] = $objectId; // Set the existing ID
-
- $this->logger->info("Updating object in OpenRegister", [
- 'type' => $type,
- 'object_id' => $objectId,
- 'archimate_id' => $objectData['id'],
- 'openregister_data' => $openRegisterData
- ]);
-
- // Remove schema_id and register_id from the data as they should be passed as separate parameters
- $schemaId = $openRegisterData['schema_id'];
- $registerId = $openRegisterData['register_id'];
- unset($openRegisterData['schema_id'], $openRegisterData['register_id']);
-
- $this->logger->info("Updating object with schema and register IDs", [
- 'schema_id' => $schemaId,
- 'register_id' => $registerId,
- 'data_keys' => array_keys($openRegisterData)
- ]);
-
- // Update the object with named parameters
- $updatedObject = $objectService->saveObject(
- object: $openRegisterData,
- extend: [],
- register: $registerId,
- schema: $schemaId
- );
-
- // Convert ObjectEntity to array for caching
- $updatedObjectArray = $updatedObject->jsonSerialize();
-
- // Update cache
- $this->cachedObjects[$type][$objectData['id']] = $updatedObjectArray;
-
- $this->logger->info("Object update completed", [
- 'object_id' => $objectId,
- 'archimate_id' => $objectData['id'],
- 'type' => $type
- ]);
- } catch (\Exception $e) {
- $this->logger->error("Error updating {$type} object", [
- 'object_id' => $objectId,
- 'archimate_id' => $objectData['id'],
- 'error' => $e->getMessage()
- ]);
- throw $e;
- }
- }
-
- // Utility methods
- private function preloadExistingObjects(): void
- {
- $this->logger->info('Preloading existing objects using proven retrieval methods');
-
- // Initialize empty cache structure
- $this->cachedObjects = [
- 'element' => [],
- 'organization' => [],
- 'relationship' => [],
- 'view' => [],
- 'property_definition' => []
- ];
-
- try {
- // Use our proven object retrieval methods that are already tested and working
- $elementObjects = $this->getElementObjects();
- $organizationObjects = $this->getOrganizationObjects();
- $viewObjects = $this->getViewObjects();
- $relationshipObjects = $this->getRelationshipObjects();
- $propertyDefinitionObjects = $this->getPropertyDefinitionObjects();
-
- // Convert ObjectEntity instances to arrays and index by ArchiMate ID for fast lookup
- foreach ($elementObjects as $object) {
- $objectArray = $object instanceof \OCA\OpenRegister\Db\ObjectEntity ? $object->jsonSerialize() : $object;
- if (isset($objectArray['archimate_id'])) {
- $this->cachedObjects['element'][$objectArray['archimate_id']] = $objectArray;
- }
- }
-
- foreach ($organizationObjects as $object) {
- $objectArray = $object instanceof \OCA\OpenRegister\Db\ObjectEntity ? $object->jsonSerialize() : $object;
- if (isset($objectArray['archimate_id'])) {
- $this->cachedObjects['organization'][$objectArray['archimate_id']] = $objectArray;
- }
- }
-
- foreach ($viewObjects as $object) {
- $objectArray = $object instanceof \OCA\OpenRegister\Db\ObjectEntity ? $object->jsonSerialize() : $object;
- if (isset($objectArray['archimate_id'])) {
- $this->cachedObjects['view'][$objectArray['archimate_id']] = $objectArray;
- }
- }
-
- foreach ($relationshipObjects as $object) {
- $objectArray = $object instanceof \OCA\OpenRegister\Db\ObjectEntity ? $object->jsonSerialize() : $object;
- if (isset($objectArray['archimate_id'])) {
- $this->cachedObjects['relationship'][$objectArray['archimate_id']] = $objectArray;
- }
- }
-
- foreach ($propertyDefinitionObjects as $object) {
- $objectArray = $object instanceof \OCA\OpenRegister\Db\ObjectEntity ? $object->jsonSerialize() : $object;
- if (isset($objectArray['archimate_id'])) {
- $this->cachedObjects['property_definition'][$objectArray['archimate_id']] = $objectArray;
- }
- }
-
- $totalCached = array_sum(array_map('count', $this->cachedObjects));
- $this->logger->info('Existing objects preload completed using proven methods', [
- 'total_cached_objects' => $totalCached,
- 'by_type' => [
- 'elements' => count($this->cachedObjects['element']),
- 'organizations' => count($this->cachedObjects['organization']),
- 'views' => count($this->cachedObjects['view']),
- 'relationships' => count($this->cachedObjects['relationship']),
- 'property_definitions' => count($this->cachedObjects['property_definition'])
- ],
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- } catch (\Exception $e) {
- $this->logger->error('Error preloading existing objects using proven methods', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
- }
- }
-
- private function waitForPromise(Promise $promise, int $timeout = 300): mixed
- {
- // For now, let's use a simple approach that doesn't block
- // We'll resolve the promise immediately and return the result
- $result = null;
- $error = null;
- $resolved = false;
-
- $promise->then(
- function ($value) use (&$result, &$resolved) {
- $result = $value;
- $resolved = true;
- },
- function ($reason) use (&$error, &$resolved) {
- $error = $reason;
- $resolved = true;
- }
- );
-
- // Use ReactPHP's event loop to process the promise
- $loop = \React\EventLoop\Loop::get();
-
- // Process the event loop until the promise is resolved
- $startTime = time();
- while (!$resolved && (time() - $startTime) < $timeout) {
- $loop->tick();
- }
-
- if (!$resolved) {
- throw new \RuntimeException('Promise timeout after ' . $timeout . ' seconds');
- }
-
- if ($error !== null) {
- throw new \RuntimeException('Promise rejected: ' . $error);
- }
-
- return $result;
- }
-
- private function calculateItemsPerSecond(array $archiMateData, float $totalTime): float
- {
- $totalItems = count($archiMateData['elements'] ?? []) +
- count($archiMateData['relationships'] ?? []) +
- count($archiMateData['organizations'] ?? []) +
- count($archiMateData['views'] ?? []);
-
- return $totalTime > 0 ? $totalItems / $totalTime : 0;
- }
-
- // OpenRegister integration methods
- private function getObjectService(): ?\OCA\OpenRegister\Service\ObjectService
- {
- try {
- if (!$this->appManager->isEnabledForUser('openregister')) {
- $this->logger->warning('OpenRegister app is not enabled');
- return null;
- }
-
- return $this->container->get(\OCA\OpenRegister\Service\ObjectService::class);
- } catch (\Exception $e) {
- $this->logger->error('Error getting ObjectService', ['error' => $e->getMessage()]);
- return null;
- }
- }
-
- private function getSchemaIdForType(string $type): int
- {
- switch ($type) {
- case 'element':
- return $this->getArchiMateElementSchemaId() ?? 0;
- case 'organization':
- return $this->getOrganizationSchemaId() ?? 0;
- case 'relationship':
- return $this->getRelationshipSchemaId() ?? 0;
- case 'view':
- return $this->getViewSchemaId() ?? 0;
- default:
- return 0;
- }
- }
-
- private function convertToOpenRegisterFormat(array $archiMateData, string $type, ?string $modelIdentifier = null): array
- {
- $baseData = [
- 'archimate_id' => $archiMateData['id'],
- 'name' => $archiMateData['name'] ?? '',
- 'documentation' => $archiMateData['documentation'] ?? '',
- 'properties' => $archiMateData['properties'] ?? [],
- 'original_archimate_type' => $archiMateData['type'] ?? '' // Store the original ArchiMate type
- ];
-
- // Always add model identifier (even if empty/null) - both as root field and in properties
- $baseData['model_id'] = $modelIdentifier ?? '';
- $baseData['properties']['model'] = $modelIdentifier ?? '';
-
- // Also keep the old 'modal' field for backward compatibility (fix typo but maintain compatibility)
- $baseData['properties']['modal'] = $modelIdentifier ?? '';
-
- // Get AMEF-specific schema and register IDs
- $schemaId = $this->getAmefSchemaIdForType($type);
- $registerId = $this->getAmefRegisterId();
-
- $this->logger->info("Converting ArchiMate data to OpenRegister format", [
- 'archimate_type' => $type,
- 'archimate_id' => $archiMateData['id'],
- 'schema_id' => $schemaId,
- 'register_id' => $registerId
- ]);
-
- // Validate that we have both schema and register IDs
- if (!$schemaId) {
- throw new \RuntimeException("AMEF schema ID not configured for type: {$type}");
- }
- if (!$registerId) {
- throw new \RuntimeException("AMEF register ID not configured");
- }
-
- switch ($type) {
- case 'element':
- return array_merge($baseData, [
- 'archimate_type' => $archiMateData['type'] ?? '',
- 'schema_id' => $schemaId,
- 'register_id' => $registerId
- ]);
- case 'organization':
- return array_merge($baseData, [
- 'archimate_type' => $archiMateData['type'] ?? '',
- 'schema_id' => $schemaId,
- 'register_id' => $registerId
- ]);
- case 'relationship':
- return array_merge($baseData, [
- 'archimate_type' => $archiMateData['type'] ?? '',
- 'source_id' => $archiMateData['source'] ?? '',
- 'target_id' => $archiMateData['target'] ?? '',
- 'schema_id' => $schemaId,
- 'register_id' => $registerId
- ]);
- case 'view':
- return array_merge($baseData, [
- 'archimate_type' => $archiMateData['type'] ?? '',
- 'schema_id' => $schemaId,
- 'register_id' => $registerId
- ]);
- case 'model':
- return array_merge($baseData, [
- 'archimate_type' => 'model',
- 'schema_id' => $schemaId,
- 'register_id' => $registerId
- ]);
- case 'property':
- return array_merge($baseData, [
- 'archimate_type' => 'property',
- 'schema_id' => $schemaId,
- 'register_id' => $registerId
- ]);
- case 'property_definition':
- return array_merge($baseData, [
- 'archimate_type' => 'property_definition',
- 'schema_id' => $schemaId,
- 'register_id' => $registerId
- ]);
- default:
- return array_merge($baseData, [
- 'archimate_type' => $type,
- 'schema_id' => $schemaId,
- 'register_id' => $registerId
- ]);
- }
- }
-
- /**
- * Get AMEF register ID from configuration
- *
- * This method retrieves the register ID from the AMEF configuration.
- * The register ID must be configured and must be a positive integer.
- *
- * @return int The register ID for AMEF operations
- * @throws \RuntimeException When no register ID is configured or when it's invalid
- *
- * @phpstan-return positive-int
- * @psalm-return positive-int
- */
- private function getAmefRegisterId(): ?int
- {
- // Retrieve AMEF configuration
- $amefConfig = $this->getAmefConfig();
-
- // Try JSON config keys first: support both 'register_id' and 'register'
- $rawRegisterId = $amefConfig['register_id']
- ?? $amefConfig['register']
- ?? null;
-
- // Fallback to legacy individual app config keys if not present in JSON
- if ($rawRegisterId === null || $rawRegisterId === '') {
- $rawRegisterId = $this->config->getValueString(self::APP_NAME, 'amef_register', '')
- ?: $this->config->getValueString(self::APP_NAME, 'amef_register_id', '');
- }
-
- // Validate and normalize to positive int
- if ($rawRegisterId !== null && $rawRegisterId !== '' && is_numeric((string) $rawRegisterId)) {
- $registerId = (int) $rawRegisterId;
- return $registerId > 0 ? $registerId : null;
- }
-
- return null;
- }
-
- /**
- * Get AMEF schema ID for a specific ArchiMate type
- *
- * This method retrieves the schema ID for a given ArchiMate type from the AMEF configuration.
- * It looks for the schema ID using the pattern '{type}_schema' in the configuration.
- *
- * @param string $archiMateType The ArchiMate type (e.g., 'element', 'organization', 'relationship')
- * @return int The schema ID for the given type
- * @throws \RuntimeException When no schema ID is configured for the given type
- *
- * @phpstan-param non-empty-string $archiMateType
- * @phpstan-return int
- * @psalm-param non-empty-string $archiMateType
- * @psalm-return int
- */
- private function getAmefSchemaIdForType(string $archiMateType): ?int
- {
- // Get AMEF configuration
- $amefConfig = $this->getAmefConfig();
-
- // Normalize plural → singular
- $typeMapping = [
- 'elements' => 'element',
- 'organizations' => 'organization',
- // Accept both 'relationships' (AMEF wording) and UI term 'relation'
- 'relationships' => 'relationship',
- 'views' => 'view',
- 'models' => 'model',
- 'properties' => 'property',
- // Accept both underscored and dashed naming conventions
- 'property_definitions' => 'property_definition'
- ];
- $normalizedType = $typeMapping[$archiMateType] ?? $archiMateType;
-
- // Candidate keys: accept plural and singular styles and UI variants
- $schemaKeyCandidatesByType = [
- 'element' => ['elements_schema', 'element_schema'],
- 'organization' => ['organizations_schema', 'organization_schema'],
- 'relationship' => ['relationships_schema', 'relationship_schema', 'relations_schema', 'relation_schema'],
- 'view' => ['views_schema', 'view_schema'],
- 'model' => ['models_schema', 'model_schema'],
- 'property' => ['properties_schema', 'property_schema'],
- 'property_definition' => ['property_definitions_schema', 'property_definition_schema', 'property-definition_schema']
- ];
-
- $candidates = $schemaKeyCandidatesByType[$normalizedType] ?? [$normalizedType . '_schema'];
-
- // 1) Try JSON config
- foreach ($candidates as $key) {
- if (array_key_exists($key, $amefConfig)) {
- $raw = $amefConfig[$key];
- if ($raw !== '' && $raw !== null && is_numeric((string) $raw)) {
- $id = (int) $raw;
- if ($id > 0) {
- return $id;
- }
- }
- }
- }
-
- // 2) Fallback to legacy individual app config keys
- foreach ($candidates as $key) {
- $raw = $this->config->getValueString(self::APP_NAME, 'amef_' . $key, '')
- ?: $this->config->getValueString(self::APP_NAME, $key, '');
- if ($raw !== '' && is_numeric((string) $raw)) {
- $id = (int) $raw;
- if ($id > 0) {
- return $id;
- }
- }
- }
-
- return null;
- }
-
- // Schema ID getters
- private function getArchiMateElementSchemaId(): ?int
- {
- // Use AMEF configuration instead of hardcoded values
- return $this->getAmefSchemaIdForType('element');
- }
-
- private function getOrganizationSchemaId(): ?int
- {
- $voorzieningenConfig = $this->getVoorzieningenConfig();
- return isset($voorzieningenConfig['organisatie_schema']) ? (int) $voorzieningenConfig['organisatie_schema'] : null;
- }
-
- private function getRelationshipSchemaId(): ?int
- {
- // Use AMEF configuration instead of hardcoded values
- return $this->getAmefSchemaIdForType('relationship');
- }
-
- private function getViewSchemaId(): ?int
- {
- // Use AMEF configuration instead of hardcoded values
- return $this->getAmefSchemaIdForType('view');
- }
-
- /**
- * Get objects from database for export using our proven object retrieval methods
- *
- * This method uses our new get*Objects() methods that have been tested and proven
- * to work correctly for retrieving AMEF objects from the database.
- *
- * @param array $criteria Export criteria including filters and options
- * @return array Array of objects to export grouped by type
- */
- private function getObjectsForExport(array $criteria): array
- {
- $this->logger->info('Getting objects for export using proven retrieval methods', ['criteria' => $criteria]);
-
- try {
- // Use our proven object retrieval methods that are already tested and working
- $elementObjects = $this->getElementObjects();
- $organizationObjects = $this->getOrganizationObjects();
- $viewObjects = $this->getViewObjects();
- $relationshipObjects = $this->getRelationshipObjects();
- $modelObjects = $this->getModelObjects();
- $propertyDefinitionObjects = $this->getPropertyDefinitionObjects();
-
- // Convert ObjectEntity instances to arrays for compatibility with conversion methods
- $elementObjectsArray = array_map(function($object) {
- return $object instanceof \OCA\OpenRegister\Db\ObjectEntity ? $object->jsonSerialize() : $object;
- }, $elementObjects);
-
- $organizationObjectsArray = array_map(function($object) {
- return $object instanceof \OCA\OpenRegister\Db\ObjectEntity ? $object->jsonSerialize() : $object;
- }, $organizationObjects);
-
- $viewObjectsArray = array_map(function($object) {
- return $object instanceof \OCA\OpenRegister\Db\ObjectEntity ? $object->jsonSerialize() : $object;
- }, $viewObjects);
-
- $relationshipObjectsArray = array_map(function($object) {
- return $object instanceof \OCA\OpenRegister\Db\ObjectEntity ? $object->jsonSerialize() : $object;
- }, $relationshipObjects);
-
- $modelObjectsArray = array_map(function($object) {
- return $object instanceof \OCA\OpenRegister\Db\ObjectEntity ? $object->jsonSerialize() : $object;
- }, $modelObjects);
-
- $propertyDefinitionObjectsArray = array_map(function($object) {
- return $object instanceof \OCA\OpenRegister\Db\ObjectEntity ? $object->jsonSerialize() : $object;
- }, $propertyDefinitionObjects);
-
- $allObjects = [
- 'elements' => $elementObjectsArray,
- 'organizations' => $organizationObjectsArray,
- 'views' => $viewObjectsArray,
- 'relationships' => $relationshipObjectsArray,
- 'models' => $modelObjectsArray,
- 'properties' => $propertyDefinitionObjectsArray
- ];
-
- $totalObjects = array_sum(array_map('count', $allObjects));
-
- $this->logger->info('Export object retrieval completed using proven methods', [
- 'total_objects' => $totalObjects,
- 'by_type' => [
- 'elements' => count($elementObjectsArray),
- 'organizations' => count($organizationObjectsArray),
- 'views' => count($viewObjectsArray),
- 'relationships' => count($relationshipObjectsArray),
- 'models' => count($modelObjectsArray),
- 'property_definitions' => count($propertyDefinitionObjectsArray)
- ]
- ]);
-
- return $allObjects;
-
- } catch (\Exception $e) {
- $this->logger->error('Error getting objects for export using proven methods', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
- return [];
- }
- }
-
- /**
- * Convert OpenRegister objects to ArchiMate format
- *
- * @param array $objects Objects from database grouped by type
- * @param array $options Export options
- * @return array ArchiMate data structure matching import format
- */
- private function convertFromOpenRegisterObjects(array $objects, array $options): array
- {
- $this->logger->info('Converting OpenRegister objects to ArchiMate format', [
- 'objects_count' => array_sum(array_map('count', $objects)),
- 'object_types' => array_keys($objects)
- ]);
-
- $archiMateData = [
- 'elements' => [],
- 'relationships' => [],
- 'organizations' => [],
- 'views' => [],
- 'property_definitions' => [],
- 'model_metadata' => []
- ];
-
- try {
- // Convert elements
- if (!empty($objects['elements'])) {
- $this->logger->info('Converting elements to ArchiMate format', [
- 'elements_count' => count($objects['elements'])
- ]);
- foreach ($objects['elements'] as $object) {
- $element = $this->convertObjectToArchiMateElement($object);
- if ($element) {
- $archiMateData['elements'][$element['id']] = $element;
- $this->logger->debug('Element converted successfully', [
- 'archimate_id' => $element['id'],
- 'name' => $element['name']
- ]);
- } else {
- $this->logger->warning('Element conversion failed', [
- 'object_id' => $object['id'] ?? 'unknown',
- 'object_keys' => array_keys($object)
- ]);
- }
- }
- $this->logger->info('Elements conversion completed', [
- 'converted_count' => count($archiMateData['elements'])
- ]);
- }
-
- // Convert organizations
- if (!empty($objects['organizations'])) {
- foreach ($objects['organizations'] as $object) {
- $organization = $this->convertObjectToArchiMateOrganization($object);
- if ($organization) {
- $archiMateData['organizations'][$organization['id']] = $organization;
- }
- }
- }
-
- // Convert relationships
- if (!empty($objects['relationships'])) {
- foreach ($objects['relationships'] as $object) {
- $relationship = $this->convertObjectToArchiMateRelationship($object);
- if ($relationship) {
- $archiMateData['relationships'][$relationship['id']] = $relationship;
- }
- }
- }
-
- // Convert views
- if (!empty($objects['views'])) {
- foreach ($objects['views'] as $object) {
- $view = $this->convertObjectToArchiMateView($object);
- if ($view) {
- $archiMateData['views'][$view['id']] = $view;
- }
- }
- }
-
- // Convert property definitions
- if (!empty($objects['properties'])) {
- $this->logger->info('Converting property definitions to ArchiMate format', [
- 'properties_count' => count($objects['properties'])
- ]);
- foreach ($objects['properties'] as $object) {
- $propertyDef = $this->convertObjectToArchiMatePropertyDefinition($object);
- if ($propertyDef) {
- $archiMateData['property_definitions'][$propertyDef['id']] = $propertyDef;
- }
- }
- }
-
- // Process model objects to extract folders from model metadata
- if (!empty($objects['models'])) {
- $this->logger->info('Processing model objects for metadata extraction', [
- 'models_count' => count($objects['models'])
- ]);
-
- foreach ($objects['models'] as $modelObject) {
- if (isset($modelObject['properties']['folders'])) {
- $archiMateData['model_metadata']['folders'] = $modelObject['properties']['folders'];
- $this->logger->info('Extracted folders from model object', [
- 'model_id' => $modelObject['id'] ?? 'unknown',
- 'folders_length' => strlen($modelObject['properties']['folders'])
- ]);
- break; // Only need one model object with folders
- }
- }
- }
-
- $this->logger->info('Conversion to ArchiMate format completed', [
- 'elements_count' => count($archiMateData['elements']),
- 'organizations_count' => count($archiMateData['organizations']),
- 'relationships_count' => count($archiMateData['relationships']),
- 'views_count' => count($archiMateData['views']),
- 'property_definitions_count' => count($archiMateData['property_definitions']),
- 'has_model_folders' => isset($archiMateData['model_metadata']['folders'])
- ]);
-
- return $archiMateData;
-
- } catch (\Exception $e) {
- $this->logger->error('Error converting objects to ArchiMate format', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
- return $archiMateData;
- }
- }
-
- /**
- * Flatten an OpenRegister object structure into a single-level associative array
- *
- * The OpenRegister API and database can return objects in multiple shapes:
- * - Plain associative array containing all fields on the top level
- * - Array with an 'object' key that either contains a JSON string or nested array
- * - Optional JSON-encoded 'properties' field
- *
- * This method normalizes these shapes into a consistent array so exporter
- * conversion methods can rely on predictable keys.
- *
- * @phpstan-param array $object
- * @phpstan-return array
- * @psalm-param array $object
- * @psalm-return array
- */
- private function flattenOpenRegisterObject(array $object): array
- {
- // Start with the original object
- $flattened = $object;
-
- // If data is nested under 'object', merge it into top-level
- if (isset($object['object'])) {
- $nested = $object['object'];
-
- // If it's a JSON string, decode it
- if (is_string($nested)) {
- $decoded = json_decode($nested, true);
- if (is_array($decoded)) {
- // Merge decoded fields; keep top-level values if keys collide
- $flattened = array_merge($decoded, $flattened);
- }
- } elseif (is_array($nested)) {
- // Merge nested array; keep top-level values if keys collide
- $flattened = array_merge($nested, $flattened);
- }
- }
-
- // Normalize 'properties' to array
- if (isset($flattened['properties'])) {
- if (is_string($flattened['properties'])) {
- $propsDecoded = json_decode($flattened['properties'], true);
- $flattened['properties'] = is_array($propsDecoded) ? $propsDecoded : [];
- } elseif (!is_array($flattened['properties'])) {
- $flattened['properties'] = [];
- }
- }
-
- // Normalize identifier fields
- if (!isset($flattened['archimate_id']) && isset($flattened['uuid'])) {
- $flattened['archimate_id'] = $flattened['uuid'];
- }
- if (!isset($flattened['uuid']) && isset($flattened['archimate_id'])) {
- $flattened['uuid'] = $flattened['archimate_id'];
- }
-
- // Normalize type fields
- if (!isset($flattened['original_archimate_type']) && isset($flattened['archimate_type'])) {
- $flattened['original_archimate_type'] = $flattened['archimate_type'];
- }
-
- // Normalize relationship endpoints
- if (!isset($flattened['source_id']) && isset($flattened['source'])) {
- $flattened['source_id'] = $flattened['source'];
- }
- if (!isset($flattened['target_id']) && isset($flattened['target'])) {
- $flattened['target_id'] = $flattened['target'];
- }
-
- return $flattened;
- }
-
- /**
- * Convert OpenRegister object to ArchiMate element format
- */
- private function convertObjectToArchiMateElement(array $object): ?array
- {
- try {
- // Flatten when data is nested under 'object' (as returned by OpenRegister)
- $object = $this->flattenOpenRegisterObject($object);
-
- // Handle both API response format and ObjectEntity format
- $archiMateId = $object['archimate_id'] ?? $object['uuid'] ?? null;
- if (!$archiMateId) {
- $this->logger->warning('Object missing ArchiMate ID', [
- 'object_id' => $object['id'] ?? 'unknown',
- 'available_keys' => array_keys($object)
- ]);
- return null;
- }
-
- $element = [
- 'id' => $archiMateId,
- 'name' => $object['name'] ?? '',
- 'type' => $object['original_archimate_type'] ?? $object['archimate_type'] ?? $object['type'] ?? 'Element',
- 'properties' => [],
- 'documentation' => $object['documentation'] ?? ''
- ];
-
- // Extract properties from the object
- if (isset($object['properties']) && is_array($object['properties'])) {
- $element['properties'] = $object['properties'];
- }
-
- $this->logger->debug('Converted element', [
- 'archimate_id' => $archiMateId,
- 'name' => $element['name'],
- 'type' => $element['type'],
- 'archimate_type_from_object' => $object['archimate_type'] ?? 'MISSING',
- 'type_from_object' => $object['type'] ?? 'MISSING',
- 'original_archimate_type' => $object['original_archimate_type'] ?? 'MISSING',
- 'documentation' => $element['documentation'],
- 'properties_count' => count($element['properties'])
- ]);
-
- return $element;
- } catch (\Exception $e) {
- $this->logger->error('Error converting object to ArchiMate element', [
- 'object_id' => $object['id'] ?? 'unknown',
- 'error' => $e->getMessage()
- ]);
- return null;
- }
- }
-
- /**
- * Convert OpenRegister object to ArchiMate organization format
- */
- private function convertObjectToArchiMateOrganization(array $object): ?array
- {
- try {
- // Flatten when data is nested under 'object'
- $object = $this->flattenOpenRegisterObject($object);
-
- // Handle both API response format and ObjectEntity format
- $archiMateId = $object['archimate_id'] ?? $object['uuid'] ?? null;
- if (!$archiMateId) {
- $this->logger->warning('Organization object missing ArchiMate ID', [
- 'object_id' => $object['id'] ?? 'unknown',
- 'available_keys' => array_keys($object)
- ]);
- return null;
- }
-
- $organization = [
- 'id' => $archiMateId,
- 'name' => $object['name'] ?? '',
- 'type' => $object['archimate_type'] ?? 'BusinessActor',
- 'properties' => []
- ];
-
- // Extract properties from the object
- if (isset($object['properties']) && is_array($object['properties'])) {
- $organization['properties'] = $object['properties'];
- }
-
- return $organization;
- } catch (\Exception $e) {
- $this->logger->error('Error converting object to ArchiMate organization', [
- 'object_id' => $object['id'] ?? 'unknown',
- 'error' => $e->getMessage()
- ]);
- return null;
- }
- }
-
- /**
- * Convert OpenRegister object to ArchiMate relationship format
- */
- private function convertObjectToArchiMateRelationship(array $object): ?array
- {
- try {
- // Flatten when data is nested under 'object'
- $object = $this->flattenOpenRegisterObject($object);
-
- // Handle both API response format and ObjectEntity format
- $archiMateId = $object['archimate_id'] ?? $object['uuid'] ?? null;
- if (!$archiMateId) {
- $this->logger->warning('Relationship object missing ArchiMate ID', [
- 'object_id' => $object['id'] ?? 'unknown',
- 'available_keys' => array_keys($object)
- ]);
- return null;
- }
-
- $relationship = [
- 'id' => $archiMateId,
- 'name' => $object['name'] ?? '',
- 'type' => $object['original_archimate_type'] ?? $object['archimate_type'] ?? 'Relationship',
- 'documentation' => $object['documentation'] ?? '',
- 'source' => $object['source_id'] ?? $object['source'] ?? '',
- 'target' => $object['target_id'] ?? $object['target'] ?? '',
- 'properties' => []
- ];
-
- // Extract properties from the object
- if (isset($object['properties']) && is_array($object['properties'])) {
- $relationship['properties'] = $object['properties'];
- }
-
- return $relationship;
- } catch (\Exception $e) {
- $this->logger->error('Error converting object to ArchiMate relationship', [
- 'object_id' => $object['id'] ?? 'unknown',
- 'error' => $e->getMessage()
- ]);
- return null;
- }
- }
-
- /**
- * Convert OpenRegister object to ArchiMate view format
- */
- private function convertObjectToArchiMateView(array $object): ?array
- {
- try {
- // Flatten when data is nested under 'object'
- $object = $this->flattenOpenRegisterObject($object);
-
- // Handle both API response format and ObjectEntity format
- $archiMateId = $object['archimate_id'] ?? $object['uuid'] ?? null;
- if (!$archiMateId) {
- $this->logger->warning('View object missing ArchiMate ID', [
- 'object_id' => $object['id'] ?? 'unknown',
- 'available_keys' => array_keys($object)
- ]);
- return null;
- }
-
- $view = [
- 'id' => $archiMateId,
- 'name' => $object['name'] ?? '',
- 'type' => $object['original_archimate_type'] ?? $object['archimate_type'] ?? 'View',
- 'documentation' => $object['documentation'] ?? '',
- 'properties' => []
- ];
-
- // Extract properties from the object
- if (isset($object['properties']) && is_array($object['properties'])) {
- $view['properties'] = $object['properties'];
- }
-
- // Preserve any additional view-specific data that was captured during import
- foreach ($object as $key => $value) {
- if (!in_array($key, ['id', 'archimate_id', 'uuid', 'name', 'archimate_type', 'documentation', 'properties', 'schema_id', 'register_id']) && !empty($value)) {
- $view[$key] = $value;
- }
- }
-
- return $view;
- } catch (\Exception $e) {
- $this->logger->error('Error converting object to ArchiMate view', [
- 'object_id' => $object['id'] ?? 'unknown',
- 'error' => $e->getMessage()
- ]);
- return null;
- }
- }
-
- /**
- * Convert OpenRegister object to ArchiMate property definition format
- */
- private function convertObjectToArchiMatePropertyDefinition(array $object): ?array
- {
- try {
- // Flatten when data is nested under 'object'
- $object = $this->flattenOpenRegisterObject($object);
-
- // Handle both API response format and ObjectEntity format
- $archiMateId = $object['archimate_id'] ?? $object['uuid'] ?? null;
- if (!$archiMateId) {
- $this->logger->warning('Property definition object missing ArchiMate ID', [
- 'object_id' => $object['id'] ?? 'unknown',
- 'available_keys' => array_keys($object)
- ]);
- return null;
- }
-
- $propertyDef = [
- 'id' => $archiMateId,
- 'name' => $object['name'] ?? '',
- 'type' => $object['type'] ?? 'string',
- 'documentation' => $object['documentation'] ?? '',
- 'properties' => []
- ];
-
- // Extract properties from the object
- if (isset($object['properties']) && is_array($object['properties'])) {
- $propertyDef['properties'] = $object['properties'];
- }
-
- return $propertyDef;
- } catch (\Exception $e) {
- $this->logger->error('Error converting object to ArchiMate property definition', [
- 'object_id' => $object['id'] ?? 'unknown',
- 'error' => $e->getMessage()
- ]);
- return null;
- }
- }
-
- /**
- * Generate ArchiMate XML from data structure
- *
- * @param array $archiMateData ArchiMate data structure
- * @return string XML content matching the import format exactly
- */
- private function generateArchiMateXml(array $archiMateData): string
- {
- $this->logger->info('Generating ArchiMate XML', [
- 'elements_count' => count($archiMateData['elements'] ?? []),
- 'organizations_count' => count($archiMateData['organizations'] ?? []),
- 'relationships_count' => count($archiMateData['relationships'] ?? []),
- 'views_count' => count($archiMateData['views'] ?? []),
- 'property_definitions_count' => count($archiMateData['property_definitions'] ?? [])
- ]);
-
- try {
- // Start XML document with hardcoded model metadata to match GEMMA_release.xml format
- $xml = '' . "\n";
- $xml .= '' . "\n";
-
- // Hardcoded model name and documentation to match GEMMA_release.xml
- $xml .= ' GEMMA release (test) ' . "\n";
- $xml .= ' De GEMeentelijk Model Architectuur (GEMMA) bevat een blauwdruk van de gemeente en haar informatievoorziening. De GEMMA kan worden gebruikt als basis voor de projectmodellen ' . "\n";
-
- // Hardcoded model properties to match GEMMA_release.xml
- $xml .= ' ' . "\n";
- $xml .= ' ' . "\n";
- $xml .= ' Softwarecatalogus en GEMMA Online en redactie ' . "\n";
- $xml .= ' ' . "\n";
- $xml .= ' ' . "\n";
- $xml .= ' In gebruik ' . "\n";
- $xml .= ' ' . "\n";
- $xml .= ' ' . "\n";
- $xml .= ' Archi ' . "\n";
- $xml .= ' ' . "\n";
- $xml .= ' ' . "\n";
- $xml .= ' Kernmodel ' . "\n";
- $xml .= ' ' . "\n";
- $xml .= ' ' . "\n";
- $xml .= ' 2b2b88ba-8efe-46d3-8b40-47af290bc418 ' . "\n";
- $xml .= ' ' . "\n";
- $xml .= ' ' . "\n";
- $xml .= ' Ja ' . "\n";
- $xml .= ' ' . "\n";
- $xml .= ' ' . "\n";
- $xml .= ' 2025-04-01 ' . "\n";
- $xml .= ' ' . "\n";
- $xml .= ' ' . "\n";
-
- // Add folders section if they exist in model metadata
- if (isset($archiMateData['model_metadata']['folders'])) {
- $xml .= $this->generateFoldersXml($archiMateData['model_metadata']['folders']);
- }
-
- // Add elements section
- if (!empty($archiMateData['elements'])) {
- $xml .= ' ' . "\n";
- foreach ($archiMateData['elements'] as $element) {
- $xml .= $this->generateElementXml($element);
- }
- $xml .= ' ' . "\n";
- }
-
- // Add relationships section
- if (!empty($archiMateData['relationships'])) {
- $xml .= ' ' . "\n";
- foreach ($archiMateData['relationships'] as $relationship) {
- $xml .= $this->generateRelationshipXml($relationship);
- }
- $xml .= ' ' . "\n";
- }
-
- // Add organizations section
- if (!empty($archiMateData['organizations'])) {
- $xml .= ' ' . "\n";
- foreach ($archiMateData['organizations'] as $organization) {
- $xml .= $this->generateOrganizationXml($organization);
- }
- $xml .= ' ' . "\n";
- }
-
- // Add propertyDefinitions section
- if (!empty($archiMateData['property_definitions'])) {
- $xml .= ' ' . "\n";
- foreach ($archiMateData['property_definitions'] as $propertyDef) {
- $xml .= $this->generatePropertyDefinitionXml($propertyDef);
- }
- $xml .= ' ' . "\n";
- }
-
- // Add views section with diagrams wrapper
- if (!empty($archiMateData['views'])) {
- $xml .= ' ' . "\n";
- $xml .= ' ' . "\n";
- foreach ($archiMateData['views'] as $view) {
- $xml .= $this->generateViewXml($view);
- }
- $xml .= ' ' . "\n";
- $xml .= ' ' . "\n";
- }
-
- $xml .= ' ';
-
- $this->logger->info('ArchiMate XML generation completed', [
- 'xml_length' => strlen($xml)
- ]);
-
- return $xml;
-
- } catch (\Exception $e) {
- $this->logger->error('Error generating ArchiMate XML', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
-
- // Return minimal valid XML on error
- return ' ';
- }
- }
-
- /**
- * Generate XML for an ArchiMate element
- */
- private function generateElementXml(array $element): string
- {
- $id = $element['id'] ?? '';
- $type = $element['type'] ?? '';
- $name = $element['name'] ?? '';
- $documentation = $element['documentation'] ?? '';
-
- $xml = ' ' . "\n";
-
- if (!empty($name)) {
- $xml .= ' ' . htmlspecialchars($name) . ' ' . "\n";
- }
-
- // Add documentation if present
- if (!empty($documentation)) {
- $xml .= ' ' . htmlspecialchars($documentation) . ' ' . "\n";
- }
-
- if (!empty($element['properties'])) {
- $xml .= ' ' . "\n";
- foreach ($element['properties'] as $key => $value) {
- $key = $key ?? '';
- $value = $value ?? '';
-
- // Only include properties with valid propertyDefinitionRef values
- // Skip internal properties like 'model', 'modal', etc.
- // Skip empty keys, whitespace-only keys, or internal properties
- if (empty($key) || trim($key) === '' || in_array($key, ['model', 'modal', 'schema_id', 'register_id', 'archimate_id', 'archimate_type', 'original_archimate_type', 'model_id'])) {
- continue;
- }
-
- $xml .= ' ' . "\n";
- $xml .= ' ' . htmlspecialchars($value) . ' ' . "\n";
- $xml .= ' ' . "\n";
- }
- $xml .= ' ' . "\n";
- }
-
- $xml .= ' ' . "\n";
- return $xml;
- }
-
- /**
- * Generate XML for an ArchiMate relationship
- */
- private function generateRelationshipXml(array $relationship): string
- {
- $id = $relationship['id'] ?? '';
- $type = $relationship['type'] ?? '';
- $name = $relationship['name'] ?? '';
- $documentation = $relationship['documentation'] ?? '';
- $source = $relationship['source'] ?? '';
- $target = $relationship['target'] ?? '';
-
- // Start with relationship tag (not element) to match import format
- $xml = ' $value) {
- // Skip the basic attributes we already handled
- if (!in_array($key, ['id', 'name', 'type', 'documentation', 'source', 'target', 'properties']) && !empty($value)) {
- $xml .= ' ' . htmlspecialchars($key) . '="' . htmlspecialchars($value) . '"';
- }
- }
-
- // Close the opening tag and add content elements
- $hasContent = !empty($name) || !empty($documentation) || !empty($relationship['properties']);
-
- if ($hasContent) {
- $xml .= '>' . "\n";
-
- // Add name if present
- if (!empty($name)) {
- $xml .= ' ' . htmlspecialchars($name) . ' ' . "\n";
- }
-
- // Add documentation if present
- if (!empty($documentation)) {
- $xml .= ' ' . htmlspecialchars($documentation) . ' ' . "\n";
- }
- }
-
- // Check if we have properties to include
- if (!empty($relationship['properties'])) {
- if (!$hasContent) {
- $xml .= '>' . "\n";
- }
- $xml .= ' ' . "\n";
- foreach ($relationship['properties'] as $key => $value) {
- $key = $key ?? '';
- $value = $value ?? '';
-
- // Only include properties with valid propertyDefinitionRef values
- // Skip internal properties like 'model', 'modal', etc.
- // Skip empty keys, whitespace-only keys, or internal properties
- if (empty($key) || trim($key) === '' || in_array($key, ['model', 'modal', 'schema_id', 'register_id', 'archimate_id', 'archimate_type', 'original_archimate_type', 'model_id'])) {
- continue;
- }
-
- $xml .= ' ' . "\n";
- $xml .= ' ' . htmlspecialchars($value) . ' ' . "\n";
- $xml .= ' ' . "\n";
- }
- $xml .= ' ' . "\n";
- $xml .= ' ' . "\n";
- } else if ($hasContent) {
- // We have name/documentation but no properties
- $xml .= ' ' . "\n";
- } else {
- // No content at all, self-closing tag
- $xml .= '/>' . "\n";
- }
-
- return $xml;
- }
-
- /**
- * Generate XML for an ArchiMate view
- */
- private function generateViewXml(array $view): string
- {
- $id = $view['id'] ?? '';
- $type = $view['type'] ?? '';
- $name = $view['name'] ?? '';
- $documentation = $view['documentation'] ?? '';
-
- // Use proper view tag with correct indentation for diagrams section
- $xml = ' ' . "\n";
-
- if (!empty($name)) {
- $xml .= ' ' . htmlspecialchars($name) . ' ' . "\n";
- }
-
- // Add documentation if present
- if (!empty($documentation)) {
- $xml .= ' ' . htmlspecialchars($documentation) . ' ' . "\n";
- }
-
- if (!empty($view['properties'])) {
- $xml .= ' ' . "\n";
- foreach ($view['properties'] as $key => $value) {
- $key = $key ?? '';
- $value = $value ?? '';
-
- // Only include properties with valid propertyDefinitionRef values
- // Skip internal properties like 'model', 'modal', etc.
- // Skip empty keys, whitespace-only keys, or internal properties
- if (empty($key) || trim($key) === '' || in_array($key, ['model', 'modal', 'schema_id', 'register_id', 'archimate_id', 'archimate_type', 'original_archimate_type', 'model_id'])) {
- continue;
- }
-
- $xml .= ' ' . "\n";
- $xml .= ' ' . htmlspecialchars($value) . ' ' . "\n";
- $xml .= ' ' . "\n";
- }
- $xml .= ' ' . "\n";
- }
-
- // Add any additional view-specific elements that were preserved during import
- foreach ($view as $key => $value) {
- if (!in_array($key, ['id', 'type', 'name', 'documentation', 'properties']) && !empty($value) && is_array($value)) {
- // This could include nodes, connections, etc.
- $xml .= ' ' . "\n";
- // Note: Full reconstruction of complex view elements would require more detailed parsing
- }
- }
-
- $xml .= ' ' . "\n";
- return $xml;
- }
-
- /**
- * Generate XML for an ArchiMate organization
- */
- private function generateOrganizationXml(array $organization): string
- {
- $id = $organization['id'] ?? '';
- $name = $organization['name'] ?? '';
- $documentation = $organization['documentation'] ?? '';
-
- $xml = ' - ' . "\n";
-
- if (!empty($name)) {
- $xml .= '
' . htmlspecialchars($name) . ' ' . "\n";
- }
-
- // Add documentation if present
- if (!empty($documentation)) {
- $xml .= ' ' . htmlspecialchars($documentation) . ' ' . "\n";
- }
-
- // Add properties if present
- if (!empty($organization['properties'])) {
- $xml .= ' ' . "\n";
- foreach ($organization['properties'] as $key => $value) {
- if (!empty($key) && !empty($value)) {
- // Skip internal properties
- if (!in_array($key, ['model', 'modal', 'schema_id', 'register_id', 'archimate_id', 'archimate_type'])) {
- $xml .= ' ' . "\n";
- $xml .= ' ' . htmlspecialchars($value) . ' ' . "\n";
- $xml .= ' ' . "\n";
- }
- }
- }
- $xml .= ' ' . "\n";
- }
-
- $xml .= ' ' . "\n";
- return $xml;
- }
-
- /**
- * Generate XML for an ArchiMate property definition
- */
- private function generatePropertyDefinitionXml(array $propertyDef): string
- {
- $id = $propertyDef['id'] ?? '';
- $name = $propertyDef['name'] ?? '';
- $type = $propertyDef['type'] ?? 'string';
- $documentation = $propertyDef['documentation'] ?? '';
-
- $xml = ' ' . "\n";
-
- if (!empty($name)) {
- $xml .= ' ' . htmlspecialchars($name) . ' ' . "\n";
- }
-
- // Add documentation if present
- if (!empty($documentation)) {
- $xml .= ' ' . htmlspecialchars($documentation) . ' ' . "\n";
- }
-
- $xml .= ' ' . "\n";
- return $xml;
- }
-
- /**
- * Generate XML for folders stored in model properties
- */
- private function generateFoldersXml(string $foldersJson): string
- {
- try {
- $folders = json_decode($foldersJson, true);
- if (!is_array($folders) || empty($folders)) {
- return '';
- }
-
- $xml = '';
- foreach ($folders as $folder) {
- $xml .= $this->generateFolderXml($folder);
- }
-
- return $xml;
- } catch (\Exception $e) {
- $this->logger->error('Error generating folders XML', [
- 'error' => $e->getMessage(),
- 'folders_json' => $foldersJson
- ]);
- return '';
- }
- }
-
- /**
- * Generate XML for a single folder
- */
- private function generateFolderXml(array $folder): string
- {
- $id = $folder['id'] ?? '';
- $name = $folder['name'] ?? '';
- $type = $folder['type'] ?? '';
- $documentation = $folder['documentation'] ?? '';
-
- $xml = ' $value) {
- if (!in_array($key, ['id', 'name', 'type', 'documentation', 'properties']) && !empty($value)) {
- $xml .= ' ' . htmlspecialchars($key) . '="' . htmlspecialchars($value) . '"';
- }
- }
-
- // Check if we have content to include
- $hasContent = !empty($documentation) || !empty($folder['properties']);
-
- if ($hasContent) {
- $xml .= '>' . "\n";
-
- // Add documentation if present
- if (!empty($documentation)) {
- $xml .= ' ' . htmlspecialchars($documentation) . ' ' . "\n";
- }
-
- // Add properties if present
- if (!empty($folder['properties'])) {
- $xml .= ' ' . "\n";
- foreach ($folder['properties'] as $key => $value) {
- if (!empty($key) && !empty($value)) {
- $xml .= ' ' . "\n";
- $xml .= ' ' . htmlspecialchars($value) . ' ' . "\n";
- $xml .= ' ' . "\n";
- }
- }
- $xml .= ' ' . "\n";
- }
-
- $xml .= ' ' . "\n";
- } else {
- // Self-closing tag
- $xml .= '/>' . "\n";
- }
-
- return $xml;
- }
-
- /**
- * Test ArchiMate round-trip functionality
- *
- * This method tests the complete ArchiMate import/export cycle:
- * 1. Export current data to ArchiMate format
- * 2. Re-import the exported data
- * 3. Compare results and validate data integrity
- *
- * @return array Test results with success status and details
- */
- public function testRoundTrip(): array
- {
- $this->logger->info('ArchiMate: Starting round-trip test');
-
- try {
- $testResults = [
- 'success' => false,
- 'message' => '',
- 'details' => [],
- 'statistics' => [
- 'export_time' => 0,
- 'import_time' => 0,
- 'total_time' => 0,
- 'elements_exported' => 0,
- 'elements_imported' => 0,
- 'data_integrity_check' => false
- ]
- ];
-
- $startTime = microtime(true);
-
- // Step 1: Export current data to ArchiMate format
- $this->logger->info('ArchiMate: Round-trip test - Step 1: Export');
- $exportStartTime = microtime(true);
-
- $exportResult = $this->exportToArchiMate(
- ['includeRelationships' => true, 'includeViews' => false],
- ['format' => 'xml']
- );
-
- $exportEndTime = microtime(true);
- $testResults['statistics']['export_time'] = $exportEndTime - $exportStartTime;
-
- if (!$exportResult['success']) {
- $testResults['message'] = 'Export failed: ' . ($exportResult['message'] ?? 'Unknown error');
- $testResults['details']['export_error'] = $exportResult;
- return $testResults;
- }
-
- $this->logger->info('ArchiMate: Round-trip test - Export completed', [
- 'export_time' => $testResults['statistics']['export_time']
- ]);
-
- // Step 2: Import the exported data back
- $this->logger->info('ArchiMate: Round-trip test - Step 2: Import');
- $importStartTime = microtime(true);
-
- // Use the exported XML content for import
- $exportedXmlContent = $exportResult['xml_content'] ?? '';
- if (empty($exportedXmlContent)) {
- $testResults['message'] = 'Export did not return XML content';
- $testResults['details']['export_error'] = $exportResult;
- return $testResults;
- }
-
- $tempFilePath = tempnam(sys_get_temp_dir(), 'archimate_roundtrip_test_') . '.xml';
- file_put_contents($tempFilePath, $exportedXmlContent);
-
- $importResult = $this->importArchiMateFileFromPath([
- 'filePath' => $tempFilePath,
- 'fileName' => 'roundtrip_test.xml',
- 'fileSize' => strlen($exportedXmlContent),
- 'mimeType' => 'text/xml',
- 'updateExisting' => false,
- 'deleteOrphaned' => false,
- 'preserveIds' => true
- ]);
-
- $importEndTime = microtime(true);
- $testResults['statistics']['import_time'] = $importEndTime - $importStartTime;
-
- // Clean up temporary file
- if (file_exists($tempFilePath)) {
- unlink($tempFilePath);
- }
-
- if (!$importResult['success']) {
- $testResults['message'] = 'Import failed: ' . ($importResult['message'] ?? 'Unknown error');
- $testResults['details']['import_error'] = $importResult;
- return $testResults;
- }
-
- $this->logger->info('ArchiMate: Round-trip test - Import completed', [
- 'import_time' => $testResults['statistics']['import_time']
- ]);
-
- // Step 3: Validate results
- $this->logger->info('ArchiMate: Round-trip test - Step 3: Validation');
-
- $totalTime = microtime(true) - $startTime;
- $testResults['statistics']['total_time'] = $totalTime;
- $testResults['statistics']['elements_exported'] = $exportResult['statistics']['objects_exported'] ?? 0;
- $testResults['statistics']['elements_imported'] = $importResult['summary']['total_objects_created'] ?? 0;
- $testResults['statistics']['data_integrity_check'] = true; // Simplified for now
-
- // Test completed successfully
- $testResults['success'] = true;
- $testResults['message'] = 'Round-trip test completed successfully';
- $testResults['details'] = [
- 'export_result' => [
- 'success' => $exportResult['success'],
- 'message' => $exportResult['message'] ?? 'Export completed',
- 'file_size' => strlen($exportedXmlContent),
- 'elements_count' => $exportResult['statistics']['objects_exported'] ?? 0
- ],
- 'import_result' => [
- 'success' => $importResult['success'],
- 'message' => $importResult['message'] ?? 'Import completed',
- 'objects_created' => $importResult['summary']['total_objects_created'] ?? 0,
- 'validation_passed' => true
- ],
- 'performance' => [
- 'export_time_ms' => round($testResults['statistics']['export_time'] * 1000, 2),
- 'import_time_ms' => round($testResults['statistics']['import_time'] * 1000, 2),
- 'total_time_ms' => round($testResults['statistics']['total_time'] * 1000, 2)
- ]
- ];
-
- $this->logger->info('ArchiMate: Round-trip test completed successfully', [
- 'total_time' => $totalTime,
- 'elements_exported' => $testResults['statistics']['elements_exported'],
- 'elements_imported' => $testResults['statistics']['elements_imported']
- ]);
-
- return $testResults;
-
- } catch (\Exception $e) {
- $this->logger->error('ArchiMate: Round-trip test failed', [
- 'exception_class' => get_class($e),
- 'exception_message' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
-
- return [
- 'success' => false,
- 'message' => 'Round-trip test failed: ' . $e->getMessage(),
- 'details' => [
- 'error' => $e->getMessage(),
- 'exception_class' => get_class($e)
- ],
- 'statistics' => $testResults['statistics'] ?? []
- ];
- }
- }
-
- /**
- * Create test ArchiMate XML content for round-trip testing
- *
- * @return string Test XML content
- */
- private function createTestArchiMateXml(): string
- {
- return '
-
-
-
- Test application component for round-trip testing
-
-
- Test application service for round-trip testing
-
-
-
-
-
- ';
- }
-
- /**
- * Gets all AMEF element objects from the database
- *
- * This method retrieves all element objects from the AMEF register
- * using the same pattern as OrganizationSyncService for consistency.
- *
- * @param array $query Optional query criteria
- * @return array Array of element objects
- */
- public function getElementObjects(array $query = []): array
- {
- return $this->getObjectsWithPagination('element', $query);
- }
-
- /**
- * Get objects with pagination support for large datasets
- *
- * @param string $schemaType Type of schema (element, organization, view, relationship, etc.)
- * @param array $query Query parameters including pagination
- * @return array Array of objects
- */
- private function getObjectsWithPagination(string $schemaType, array $query = []): array
- {
- try {
- $objectService = $this->getObjectService();
- if (!$objectService) {
- $this->logger->error("ArchiMateService: ObjectService not available for {$schemaType} objects retrieval");
- return [];
- }
-
- $registerId = $this->getAmefRegisterId();
- $schemaId = $this->getAmefSchemaIdForType($schemaType);
-
- if (!$registerId || !$schemaId) {
- $this->logger->error("ArchiMateService: AMEF register or {$schemaType} schema not configured", [
- 'registerId' => $registerId,
- 'schemaId' => $schemaId
- ]);
- return [];
- }
-
- // Extract pagination parameters
- $limit = $query['limit'] ?? 1000; // Default limit for large datasets
- $offset = $query['offset'] ?? 0;
- $usePagination = $query['use_pagination'] ?? false;
-
- // Remove pagination parameters from query
- unset($query['limit'], $query['offset'], $query['use_pagination']);
-
- // Build base query for register and schema
- $baseQuery = [
- '@self' => [
- 'register' => (int) $registerId,
- 'schema' => (int) $schemaId
- ]
- ];
-
- // Merge with provided query
- $finalQuery = array_merge_recursive($baseQuery, $query);
-
- // Add pagination if requested
- if ($usePagination && $limit > 0) {
- $finalQuery['@pagination'] = [
- 'limit' => (int) $limit,
- 'offset' => (int) $offset
- ];
- }
-
- $this->logger->debug("ArchiMateService: Retrieving {$schemaType} objects", [
- 'register' => $registerId,
- 'schema' => $schemaId,
- 'query' => $finalQuery,
- 'pagination' => $usePagination ? ['limit' => $limit, 'offset' => $offset] : 'disabled'
- ]);
-
- // Use searchObjects method for filtering
- $objects = $objectService->searchObjects($finalQuery);
-
- $this->logger->debug("ArchiMateService: Retrieved {$schemaType} objects", [
- 'register' => $registerId,
- 'schema' => $schemaId,
- 'count' => count($objects),
- 'pagination' => $usePagination ? ['limit' => $limit, 'offset' => $offset] : 'disabled'
- ]);
-
- return $objects;
- } catch (\Exception $e) {
- $this->logger->error("ArchiMateService: Failed to retrieve {$schemaType} objects", [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
-
- return [];
- }
- }
-
- /**
- * Gets all AMEF organization objects from the database
- *
- * This method retrieves all organization objects from the AMEF register
- * using the same pattern as OrganizationSyncService for consistency.
- *
- * @param array $query Optional query criteria
- * @return array Array of organization objects
- */
- public function getOrganizationObjects(array $query = []): array
- {
- return $this->getObjectsWithPagination('organization', $query);
- }
-
- /**
- * Gets all AMEF view objects from the database
- *
- * This method retrieves all view objects from the AMEF register
- * using the same pattern as OrganizationSyncService for consistency.
- *
- * @param array $query Optional query criteria
- * @return array Array of view objects
- */
- public function getViewObjects(array $query = []): array
- {
- return $this->getObjectsWithPagination('view', $query);
- }
-
- /**
- * Gets all AMEF relationship objects from the database
- *
- * This method retrieves all relationship objects from the AMEF register
- * using the same pattern as OrganizationSyncService for consistency.
- *
- * @param array $query Optional query criteria
- * @return array Array of relationship objects
- */
- public function getRelationshipObjects(array $query = []): array
- {
- return $this->getObjectsWithPagination('relationship', $query);
- }
-
- /**
- * Set ArchiMate import status
- *
- * @param array $status The import status
- * @return void
- */
- public function setArchiMateImportStatus(array $status): void
- {
- $jsonStatus = json_encode($status, JSON_PRETTY_PRINT);
- $this->config->setValueString(self::APP_NAME, 'archimate_import_status', $jsonStatus);
- }
-
- /**
- * Set ArchiMate export status
- *
- * @param array $status The export status
- * @return void
- */
- public function setArchiMateExportStatus(array $status): void
- {
- $jsonStatus = json_encode($status, JSON_PRETTY_PRINT);
- $this->config->setValueString(self::APP_NAME, 'archimate_export_status', $jsonStatus);
- }
-
- /**
- * Clear ArchiMate import status
- * Optionally kills the running import process
- *
- * @param bool $killProcess Whether to attempt to kill the running import process
- * @return array Status of the clear operation
- */
- public function clearArchiMateImportStatus(bool $killProcess = false): array
- {
- $result = [
- 'cleared' => false,
- 'process_killed' => false,
- 'process_id' => null,
- 'was_running' => false,
- 'messages' => []
- ];
-
- // Get current status before clearing
- $importStatus = $this->config->getValueString(self::APP_NAME, 'archimate_import_status', '{}');
- $decoded = json_decode($importStatus, true);
-
- if (is_array($decoded) && isset($decoded['status'])) {
- $result['was_running'] = $decoded['status'] === 'running';
- $result['process_id'] = $decoded['process_id'] ?? null;
-
- // If requested and process is running, attempt to kill it
- if ($killProcess && $result['was_running'] && $result['process_id']) {
- $killResult = $this->killImportProcess($result['process_id']);
- $result['process_killed'] = $killResult['success'];
- $result['messages'] = array_merge($result['messages'], $killResult['messages']);
-
- $this->logger->info('Import process termination attempted', [
- 'process_id' => $result['process_id'],
- 'killed' => $result['process_killed'],
- 'messages' => $killResult['messages']
- ]);
- }
- }
-
- // Clear the configuration
- $this->config->deleteKey(self::APP_NAME, 'archimate_import_status');
- $result['cleared'] = true;
- $result['messages'][] = 'Import status configuration cleared';
-
- $this->logger->info('ArchiMate import status cleared', [
- 'was_running' => $result['was_running'],
- 'process_killed' => $result['process_killed'],
- 'process_id' => $result['process_id']
- ]);
-
- return $result;
- }
-
- /**
- * Safely update schema statistics to prevent race conditions in parallel processing
- *
- * @param string $schemaType The schema type being updated
- * @param array $stats The statistics to update (created, updated, skipped)
- * @return void
- */
- private function updateSchemaStatsSafely(string $schemaType, array $stats): void
- {
- // Use a retry mechanism with exponential backoff to handle race conditions
- $maxRetries = 5;
- $retryDelay = 10000; // Start with 10ms
-
- for ($attempt = 0; $attempt < $maxRetries; $attempt++) {
- try {
- // Get fresh status from storage to avoid stale data
- $currentStatusJson = $this->config->getValueString(self::APP_NAME, 'archimate_import_status', '{}');
- $currentStatus = json_decode($currentStatusJson, true);
-
- if (!is_array($currentStatus) || !isset($currentStatus['schema_progress'][$schemaType])) {
- $this->logger->warning('Schema progress not found for atomic update', [
- 'schema_type' => $schemaType,
- 'attempt' => $attempt + 1
- ]);
- return;
- }
-
- // Update only the specific schema stats
- $currentStatus['schema_progress'][$schemaType]['created'] = $stats['created'] ?? 0;
- $currentStatus['schema_progress'][$schemaType]['updated'] = $stats['updated'] ?? 0;
- $currentStatus['schema_progress'][$schemaType]['skipped'] = $stats['skipped'] ?? 0;
-
- // Calculate progress for this schema
- $found = $currentStatus['schema_progress'][$schemaType]['found'] ?? 0;
- $processed = ($stats['created'] ?? 0) + ($stats['updated'] ?? 0) + ($stats['skipped'] ?? 0);
- $schemaProgress = $found > 0 ? round(($processed / $found) * 100, 2) : 0;
- $currentStatus['schema_progress'][$schemaType]['progress'] = $schemaProgress;
-
- // Recalculate overall progress and main statistics
- $totalFound = 0;
- $totalProcessed = 0;
- $totalCreated = 0;
- $totalUpdated = 0;
- $totalSkipped = 0;
-
- foreach ($currentStatus['schema_progress'] as $schema => $data) {
- $totalFound += $data['found'] ?? 0;
- $schemaProcessed = ($data['created'] ?? 0) + ($data['updated'] ?? 0) + ($data['skipped'] ?? 0);
- $totalProcessed += $schemaProcessed;
- $totalCreated += $data['created'] ?? 0;
- $totalUpdated += $data['updated'] ?? 0;
- $totalSkipped += $data['skipped'] ?? 0;
- }
-
- $overallProgress = $totalFound > 0 ? round(($totalProcessed / $totalFound) * 100, 2) : 0;
- $currentStatus['progress'] = 35 + ($overallProgress * 0.6); // 35% to 95% range
-
- // Update main statistics
- $currentStatus['statistics']['objects_created'] = $totalCreated;
- $currentStatus['statistics']['objects_updated'] = $totalUpdated;
- $currentStatus['statistics']['objects_skipped'] = $totalSkipped;
-
- // Add timestamp and attempt info for debugging
- $currentStatus['last_stats_update'] = [
- 'timestamp' => microtime(true),
- 'schema_type' => $schemaType,
- 'attempt' => $attempt + 1,
- 'stats_applied' => $stats
- ];
-
- $this->logger->debug('Atomic stats update', [
- 'schema_type' => $schemaType,
- 'attempt' => $attempt + 1,
- 'schema_progress' => $schemaProgress,
- 'overall_progress' => $overallProgress,
- 'stats_applied' => $stats,
- 'totals' => [
- 'found' => $totalFound,
- 'processed' => $totalProcessed,
- 'created' => $totalCreated,
- 'updated' => $totalUpdated,
- 'skipped' => $totalSkipped
- ]
- ]);
-
- // Atomically save the updated status
- $this->setArchiMateImportStatus($currentStatus);
-
- // Success - exit retry loop
- return;
-
- } catch (\Exception $e) {
- $this->logger->warning('Stats update attempt failed, retrying', [
- 'schema_type' => $schemaType,
- 'attempt' => $attempt + 1,
- 'error' => $e->getMessage(),
- 'retry_delay_us' => $retryDelay
- ]);
-
- // Exponential backoff with jitter
- if ($attempt < $maxRetries - 1) {
- usleep($retryDelay + rand(0, $retryDelay / 2));
- $retryDelay *= 2;
- }
- }
- }
-
- $this->logger->error('Failed to update schema stats after all retries', [
- 'schema_type' => $schemaType,
- 'stats' => $stats,
- 'max_retries' => $maxRetries
- ]);
- }
-
- /**
- * Cancel a running ArchiMate import
- * This combines force clearing and process killing for a complete cancellation
- *
- * @return array Cancellation result with detailed status
- */
- public function cancelArchiMateImport(): array
- {
- $this->logger->info('ArchiMate import cancellation requested');
-
- // Get current status for detailed reporting
- $importStatus = $this->config->getValueString(self::APP_NAME, 'archimate_import_status', '{}');
- $decoded = json_decode($importStatus, true);
-
- $result = [
- 'cancelled' => false,
- 'was_running' => false,
- 'process_id' => null,
- 'process_killed' => false,
- 'status_cleared' => false,
- 'cancellation_time' => date('Y-m-d H:i:s'),
- 'messages' => []
- ];
-
- if (!is_array($decoded) || !isset($decoded['status'])) {
- $result['messages'][] = 'No import was running';
- $result['cancelled'] = true;
- $result['status_cleared'] = true;
-
- $this->logger->info('Import cancellation completed - no import was running');
- return $result;
- }
-
- $result['was_running'] = $decoded['status'] === 'running';
- $result['process_id'] = $decoded['process_id'] ?? null;
-
- if (!$result['was_running']) {
- // Clear any stale status
- $this->config->deleteKey(self::APP_NAME, 'archimate_import_status');
- $result['cancelled'] = true;
- $result['status_cleared'] = true;
- $result['messages'][] = 'Import was not running, cleared stale status';
-
- $this->logger->info('Import cancellation completed - import was not running');
- return $result;
- }
-
- // Import is running, attempt to kill the process
- if ($result['process_id']) {
- $this->logger->info('Attempting to kill running import process', [
- 'process_id' => $result['process_id'],
- 'import_status' => $decoded
- ]);
-
- $killResult = $this->killImportProcess($result['process_id']);
- $result['process_killed'] = $killResult['success'];
- $result['messages'] = array_merge($result['messages'], $killResult['messages']);
-
- if ($result['process_killed']) {
- $result['messages'][] = 'Import process successfully terminated';
- } else {
- $result['messages'][] = 'Import process could not be terminated, but status will be cleared';
- }
- } else {
- $result['messages'][] = 'No process ID found, clearing status only';
- }
-
- // Always clear the status after attempting to kill the process
- $this->config->deleteKey(self::APP_NAME, 'archimate_import_status');
- $result['status_cleared'] = true;
- $result['cancelled'] = true;
- $result['messages'][] = 'Import status cleared';
-
- $this->logger->info('ArchiMate import cancellation completed', [
- 'was_running' => $result['was_running'],
- 'process_id' => $result['process_id'],
- 'process_killed' => $result['process_killed'],
- 'status_cleared' => $result['status_cleared'],
- 'messages' => $result['messages']
- ]);
-
- return $result;
- }
-
- /**
- * Clear ArchiMate export status
- *
- * @return void
- */
- public function clearArchiMateExportStatus(): void
- {
- $this->config->deleteKey(self::APP_NAME, 'archimate_export_status');
- }
-
- /**
- * Get ArchiMate import/export status and AMEF object counts
- *
- * @return array The ArchiMate status with object counts
- */
- public function getArchiMateStatus(): array
- {
- $importStatus = $this->config->getValueString(self::APP_NAME, 'archimate_import_status', '{}');
- $exportStatus = $this->config->getValueString(self::APP_NAME, 'archimate_export_status', '{}');
-
- $importDecoded = json_decode($importStatus, true);
- $exportDecoded = json_decode($exportStatus, true);
-
- // Get AMEF object counts
- $elementObjects = $this->getElementObjects();
- $organizationObjects = $this->getOrganizationObjects();
- $viewObjects = $this->getViewObjects();
- $relationshipObjects = $this->getRelationshipObjects();
- $modelObjects = $this->getModelObjects();
- $propertyObjects = $this->getPropertyObjects();
-
- return [
- 'import' => is_array($importDecoded) ? $importDecoded : [],
- 'export' => is_array($exportDecoded) ? $exportDecoded : [],
- 'totalElementObjects' => count($elementObjects),
- 'totalOrganizationObjects' => count($organizationObjects),
- 'totalViewObjects' => count($viewObjects),
- 'totalRelationshipsObjects' => count($relationshipObjects),
- 'totalModelObjects' => count($modelObjects),
- 'totalPropertyObjects' => count($propertyObjects)
- ];
- }
-
- /**
- * Get AMEF configuration directly from IAppConfig
- *
- * @return array The AMEF configuration
- */
- private function getAmefConfig(): array
- {
- $config = $this->config->getValueString(self::APP_NAME, 'amef_config', '{}');
- $decoded = json_decode($config, true);
-
- return $decoded;
- }
-
- /**
- * Get Voorzieningen configuration directly from IAppConfig
- *
- * @return array The voorzieningen configuration
- */
- private function getVoorzieningenConfig(): array
- {
- $config = $this->config->getValueString(self::APP_NAME, 'voorzieningen_config', '{}');
- $decoded = json_decode($config, true);
-
- if (!is_array($decoded)) {
- // Fallback to individual config values for backward compatibility
- $decoded = [
- 'register' => $this->config->getValueString(self::APP_NAME, 'voorzieningen_register', ''),
- 'organisatie_schema' => $this->config->getValueString(self::APP_NAME, 'voorzieningen_organisatie_schema', ''),
- 'contactpersoon_schema' => $this->config->getValueString(self::APP_NAME, 'voorzieningen_contactpersoon_schema', ''),
- ];
- }
-
- return $decoded;
- }
-
- /**
- * Get model objects from the AMEF register
- *
- * @param array $query Optional query criteria
- * @return array Array of model objects
- */
- public function getModelObjects(array $query = []): array
- {
- return $this->getObjectsWithPagination('model', $query);
- }
-
- /**
- * Get property objects from the AMEF register
- *
- * @param array $query Optional query criteria
- * @return array Array of property objects
- */
- public function getPropertyObjects(array $query = []): array
- {
- return $this->getObjectsWithPagination('property', $query);
- }
-
- /**
- * Get property definition objects from the AMEF register
- *
- * @param array $query Optional query criteria
- * @return array Array of property definition objects
- */
- public function getPropertyDefinitionObjects(array $query = []): array
- {
- return $this->getObjectsWithPagination('property_definition', $query);
- }
-
- /**
- * Check if an ArchiMate import is currently in progress
- * Also handles stale lock detection and cleanup
- *
- * @return bool True if import is running, false otherwise
- */
- public function isImportInProgress(): bool
- {
- $importStatus = $this->config->getValueString(self::APP_NAME, 'archimate_import_status', '{}');
- $decoded = json_decode($importStatus, true);
-
- if (!is_array($decoded) || !isset($decoded['status']) || $decoded['status'] !== 'running') {
- return false;
- }
-
- // Check for stale locks (imports running for more than 1 hour)
- if (isset($decoded['lock_acquired_at'])) {
- $lockAge = microtime(true) - $decoded['lock_acquired_at'];
- $maxLockAge = 3600; // 1 hour in seconds
-
- if ($lockAge > $maxLockAge) {
- $this->logger->warning('Detected stale import lock, clearing it', [
- 'lock_age_seconds' => round($lockAge, 2),
- 'max_age_seconds' => $maxLockAge,
- 'stale_status' => $decoded
- ]);
-
- // Clear the stale lock
- $this->clearArchiMateImportStatus();
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * Check if an ArchiMate export is currently in progress
- *
- * @return bool True if export is running, false otherwise
- */
- public function isExportInProgress(): bool
- {
- $exportStatus = $this->config->getValueString(self::APP_NAME, 'archimate_export_status', '{}');
- $decoded = json_decode($exportStatus, true);
-
- return is_array($decoded) &&
- isset($decoded['status']) &&
- $decoded['status'] === 'running';
- }
-
- /**
- * Check if any ArchiMate operation is currently in progress
- *
- * @return bool True if any operation is running, false otherwise
- */
- public function isOperationInProgress(): bool
- {
- return $this->isImportInProgress() || $this->isExportInProgress();
- }
-
- /**
- * Atomically acquire an import lock to prevent concurrent imports
- *
- * @return bool True if lock acquired successfully, false if another operation is running
- */
- private function acquireImportLock(): bool
- {
- // Double-check pattern with atomic operation
- if ($this->isOperationInProgress()) {
- return false;
- }
-
- // Set a temporary lock status immediately
- $lockStatus = [
- 'status' => 'running',
- 'start_time' => date('Y-m-d H:i:s'),
- 'progress' => 0,
- 'current_step' => 'Acquiring import lock',
- 'lock_acquired_at' => microtime(true),
- 'process_id' => getmypid(),
- 'request_id' => uniqid('import_', true)
- ];
-
- $this->setArchiMateImportStatus($lockStatus);
-
- // Verify the lock was acquired (check for race conditions)
- usleep(10000); // 10ms delay to allow for race conditions
- $currentStatus = $this->config->getValueString(self::APP_NAME, 'archimate_import_status', '{}');
- $decoded = json_decode($currentStatus, true);
-
- if (!is_array($decoded) ||
- !isset($decoded['request_id']) ||
- $decoded['request_id'] !== $lockStatus['request_id']) {
-
- $this->logger->warning('Import lock acquisition failed due to race condition', [
- 'our_request_id' => $lockStatus['request_id'],
- 'current_request_id' => $decoded['request_id'] ?? 'unknown',
- 'current_status' => $decoded
- ]);
-
- return false;
- }
-
- $this->logger->info('Import lock acquired successfully', [
- 'request_id' => $lockStatus['request_id'],
- 'process_id' => $lockStatus['process_id']
- ]);
-
- return true;
- }
-
- /**
- * Release the import lock
- *
- * @return void
- */
- private function releaseImportLock(): void
- {
- $this->clearArchiMateImportStatus();
- $this->logger->info('Import lock released');
- }
-
- /**
- * Attempt to kill a running import process
- *
- * @param int $processId The process ID to kill
- * @return array Result of the kill operation
- */
- private function killImportProcess(int $processId): array
- {
- $result = [
- 'success' => false,
- 'messages' => []
- ];
-
- // First check if the process exists and is still running
- if (!$this->isProcessRunning($processId)) {
- $result['messages'][] = "Process {$processId} is not running";
- $result['success'] = true; // Consider it successful if already stopped
- return $result;
- }
-
- // Try graceful termination first (SIGTERM)
- if (function_exists('posix_kill')) {
- $this->logger->info("Attempting graceful termination of import process {$processId}");
-
- if (posix_kill($processId, SIGTERM)) {
- $result['messages'][] = "Sent SIGTERM to process {$processId}";
-
- // Wait a few seconds for graceful shutdown
- sleep(3);
-
- if (!$this->isProcessRunning($processId)) {
- $result['success'] = true;
- $result['messages'][] = "Process {$processId} terminated gracefully";
- return $result;
- }
-
- // If still running, try force kill (SIGKILL)
- $this->logger->warning("Process {$processId} didn't respond to SIGTERM, trying SIGKILL");
-
- if (posix_kill($processId, SIGKILL)) {
- $result['messages'][] = "Sent SIGKILL to process {$processId}";
-
- sleep(1);
-
- if (!$this->isProcessRunning($processId)) {
- $result['success'] = true;
- $result['messages'][] = "Process {$processId} force killed";
- } else {
- $result['messages'][] = "Process {$processId} could not be killed";
- }
- } else {
- $result['messages'][] = "Failed to send SIGKILL to process {$processId}";
- }
- } else {
- $result['messages'][] = "Failed to send SIGTERM to process {$processId}";
- }
- } else {
- // Fallback to system kill command if posix functions not available
- $this->logger->info("POSIX functions not available, using system kill command for process {$processId}");
-
- $killCommand = "kill -TERM {$processId} 2>&1";
- $output = [];
- $returnCode = 0;
-
- exec($killCommand, $output, $returnCode);
-
- if ($returnCode === 0) {
- $result['messages'][] = "Sent SIGTERM via system command to process {$processId}";
-
- sleep(3);
-
- if (!$this->isProcessRunning($processId)) {
- $result['success'] = true;
- $result['messages'][] = "Process {$processId} terminated via system command";
- } else {
- // Try force kill
- $forceKillCommand = "kill -KILL {$processId} 2>&1";
- exec($forceKillCommand, $output, $returnCode);
-
- if ($returnCode === 0) {
- $result['messages'][] = "Sent SIGKILL via system command to process {$processId}";
- sleep(1);
-
- if (!$this->isProcessRunning($processId)) {
- $result['success'] = true;
- $result['messages'][] = "Process {$processId} force killed via system command";
- }
- }
- }
- } else {
- $result['messages'][] = "System kill command failed for process {$processId}: " . implode(' ', $output);
- }
- }
-
- return $result;
- }
-
- /**
- * Check if a process is currently running
- *
- * @param int $processId The process ID to check
- * @return bool True if process is running, false otherwise
- */
- private function isProcessRunning(int $processId): bool
- {
- // Try using posix_getpgid first (most reliable)
- if (function_exists('posix_getpgid')) {
- return posix_getpgid($processId) !== false;
- }
-
- // Fallback to checking /proc filesystem (Linux)
- if (file_exists("/proc/{$processId}")) {
- return true;
- }
-
- // Fallback to ps command
- $psCommand = "ps -p {$processId} > /dev/null 2>&1";
- $returnCode = 0;
- exec($psCommand, $output, $returnCode);
-
- return $returnCode === 0;
- }
-
- /**
- * Force clear all ArchiMate operation statuses
- *
- * This method can be used to reset stuck operations
- *
- * @return void
- */
- public function forceClearAllStatuses(): void
- {
- $this->clearArchiMateImportStatus();
- $this->clearArchiMateExportStatus();
-
- $this->logger->info('ArchiMateService: Force cleared all operation statuses');
- }
-
- /**
- * Get detailed information about current operation status
- *
- * @return array Detailed status information
- */
- public function getDetailedOperationStatus(): array
- {
- $importStatus = $this->config->getValueString(self::APP_NAME, 'archimate_import_status', '{}');
- $exportStatus = $this->config->getValueString(self::APP_NAME, 'archimate_export_status', '{}');
-
- $importDecoded = json_decode($importStatus, true);
- $exportDecoded = json_decode($exportStatus, true);
-
- $status = [
- 'import_in_progress' => $this->isImportInProgress(),
- 'export_in_progress' => $this->isExportInProgress(),
- 'any_operation_in_progress' => $this->isOperationInProgress(),
- 'import_status' => is_array($importDecoded) ? $importDecoded : [],
- 'export_status' => is_array($exportDecoded) ? $exportDecoded : []
- ];
-
- // Add operation details if in progress
- if ($this->isImportInProgress() && is_array($importDecoded)) {
- $status['current_operation'] = 'import';
- $status['current_step'] = $importDecoded['current_step'] ?? 'Unknown';
- $status['progress'] = $importDecoded['progress'] ?? 0;
- $status['start_time'] = $importDecoded['start_time'] ?? 'Unknown';
- } elseif ($this->isExportInProgress() && is_array($exportDecoded)) {
- $status['current_operation'] = 'export';
- $status['current_step'] = $exportDecoded['current_step'] ?? 'Unknown';
- $status['progress'] = $exportDecoded['progress'] ?? 0;
- $status['start_time'] = $exportDecoded['start_time'] ?? 'Unknown';
- }
-
- return $status;
- }
-
- /**
- * Create or update a model object with metadata from imported XML
- *
- * @param array $modelMetadata The model metadata from the XML
- * @return array Result of the operation
- */
- private function createOrUpdateModelObject(array $modelMetadata): array
- {
- try {
- $objectService = $this->getObjectService();
- if (!$objectService) {
- $this->logger->error('ArchiMateService: ObjectService not available for model object creation');
- return ['success' => false, 'error' => 'ObjectService not available'];
- }
-
- $registerId = $this->getAmefRegisterId();
- $schemaId = $this->getAmefSchemaIdForType('model');
-
- if (!$registerId || !$schemaId) {
- $this->logger->error('ArchiMateService: AMEF register or model schema not configured', [
- 'registerId' => $registerId,
- 'schemaId' => $schemaId
- ]);
- return ['success' => false, 'error' => 'AMEF register or model schema not configured'];
- }
-
- $modelIdentifier = $modelMetadata['identifier'] ?? '';
- if (empty($modelIdentifier)) {
- $this->logger->warning('ArchiMateService: No model identifier found in metadata');
- return ['success' => false, 'error' => 'No model identifier found'];
- }
-
- // Check if model object already exists
- $existingModel = $this->findExistingObject($modelIdentifier, 'model');
-
- // Prepare model object data
- $modelData = [
- 'id' => $modelIdentifier, // Add the id field that saveObject expects
- 'archimate_id' => $modelIdentifier,
- 'name' => $modelMetadata['name'] ?? '',
- 'documentation' => $modelMetadata['documentation'] ?? '',
- 'properties' => $modelMetadata['properties'] ?? [],
- 'import_time' => date('Y-m-d H:i:s'),
- 'import_source' => 'archimate_xml_import'
- ];
-
- // Use OpenRegister's built-in duplicate detection via UUID
- // No need for custom existing model logic - OpenRegister handles it automatically
- $savedModelObject = $this->saveObject($modelData, 'model');
- $modelAction = $this->determineObjectAction($savedModelObject);
- $this->logger->info('ArchiMateService: Saved model object', [
- 'model_id' => $modelIdentifier,
- 'action' => $modelAction
- ]);
- return ['success' => true, 'action' => 'saved'];
-
- } catch (\Exception $e) {
- $this->logger->error('ArchiMateService: Failed to create/update model object', [
- 'error' => $e->getMessage(),
- 'model_metadata' => $modelMetadata
- ]);
- return ['success' => false, 'error' => $e->getMessage()];
- }
- }
-
- /**
- * Optimized method for processing large datasets with streaming and lazy loading
- * This method processes objects in smaller batches and uses lazy loading for existing objects
- *
- * @param array $archiMateData The ArchiMate data to process
- * @param array $options Processing options
- * @param callable|null $statusCallback Status update callback
- * @return array Processing results
- */
- private function convertToOpenRegisterObjectsOptimized(array $archiMateData, array $options, callable $statusCallback = null): array
- {
- $startTime = microtime(true);
-
- $this->logger->info('=== OPTIMIZED CONVERSION START ===', [
- 'elements_count' => count($archiMateData['elements'] ?? []),
- 'relationships_count' => count($archiMateData['relationships'] ?? []),
- 'organizations_count' => count($archiMateData['organizations'] ?? []),
- 'views_count' => count($archiMateData['views'] ?? []),
- 'property_definitions_count' => count($archiMateData['property_definitions'] ?? []),
- 'batch_size' => $options['batch_size'] ?? 100,
- 'streaming_batch_size' => $options['streaming_batch_size'] ?? 50,
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- // Initialize results structure
- $results = [
- 'objects_created' => 0,
- 'objects_updated' => 0,
- 'objects_deleted' => 0,
- 'objects_skipped' => 0,
- 'errors' => [],
- 'schema_statistics' => [
- 'elements' => ['found' => 0, 'created' => 0, 'updated' => 0, 'deleted' => 0, 'skipped' => 0, 'errors' => []],
- 'organizations' => ['found' => 0, 'created' => 0, 'updated' => 0, 'deleted' => 0, 'skipped' => 0, 'errors' => []],
- 'relationships' => ['found' => 0, 'created' => 0, 'updated' => 0, 'deleted' => 0, 'skipped' => 0, 'errors' => []],
- 'views' => ['found' => 0, 'created' => 0, 'updated' => 0, 'deleted' => 0, 'skipped' => 0, 'errors' => []],
- 'property_definitions' => ['found' => 0, 'created' => 0, 'updated' => 0, 'deleted' => 0, 'skipped' => 0, 'errors' => []]
- ]
- ];
-
- // Process schema types in parallel with optimized streaming
- $promises = [];
- $schemaTypes = ['elements', 'organizations', 'relationships', 'views', 'property_definitions'];
-
- // Create promises for all schema types to process them in parallel
- foreach ($schemaTypes as $schemaType) {
- if (!empty($archiMateData[$schemaType])) {
- $this->logger->info("Starting parallel processing of {$schemaType} with optimized streaming", [
- 'count' => count($archiMateData[$schemaType]),
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- // Create a promise for each schema type
- $promises[$schemaType] = $this->createSchemaProcessingPromise(
- $archiMateData[$schemaType],
- $schemaType,
- $options,
- $statusCallback
- );
-
- // Unset processed data to free memory immediately after creating promise
- unset($archiMateData[$schemaType]);
- $this->logger->info("{$schemaType} array unset from memory", [
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
- }
- }
-
- // Wait for all promises to complete and merge results
- foreach ($promises as $schemaType => $promise) {
- $schemaStart = microtime(true);
- $schemaResult = $this->waitForPromise($promise);
- $schemaTime = microtime(true) - $schemaStart;
-
- // Merge results
- $results['objects_created'] += $schemaResult['created'];
- $results['objects_updated'] += $schemaResult['updated'];
- $results['objects_deleted'] += $schemaResult['deleted'] ?? 0;
- $results['objects_skipped'] += $schemaResult['skipped'] ?? 0;
- $results['errors'] = array_merge($results['errors'], $schemaResult['errors']);
- $results['schema_statistics'][$schemaType] = $schemaResult;
-
- $this->logger->info("Completed parallel processing of {$schemaType}", [
- 'processing_time_seconds' => round($schemaTime, 3),
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
- }
-
- $totalTime = microtime(true) - $startTime;
-
- $this->logger->info('=== OPTIMIZED CONVERSION COMPLETED ===', [
- 'total_time_seconds' => round($totalTime, 3),
- 'objects_created' => $results['objects_created'],
- 'objects_updated' => $results['objects_updated'],
- 'objects_deleted' => $results['objects_deleted'],
- 'objects_skipped' => $results['objects_skipped'],
- 'total_errors' => count($results['errors']),
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- return $results;
- }
-
- /**
- * Create a ReactPHP promise for processing a schema type with optimized streaming
- *
- * @param array $items Items to process
- * @param string $schemaType Type of schema being processed
- * @param array $options Processing options
- * @param callable|null $statusCallback Status update callback
- * @return Promise Processing promise
- */
- private function createSchemaProcessingPromise(array $items, string $schemaType, array $options, callable $statusCallback = null): Promise
- {
- $deferred = new Deferred();
-
- try {
- $result = $this->processSchemaTypeOptimized($items, $schemaType, $options, $statusCallback);
- $deferred->resolve($result);
- } catch (\Exception $e) {
- $this->logger->error("Error processing {$schemaType} in parallel", [
- 'error' => $e->getMessage(),
- 'schema_type' => $schemaType
- ]);
- $deferred->reject($e);
- }
-
- return $deferred->promise();
- }
-
- /**
- * Process a specific schema type with optimized streaming and lazy loading
- *
- * @param array $items Items to process
- * @param string $schemaType Type of schema being processed
- * @param array $options Processing options
- * @param callable|null $statusCallback Status update callback
- * @return array Processing results
- */
- private function processSchemaTypeOptimized(array $items, string $schemaType, array $options, callable $statusCallback = null): array
- {
- $streamingBatchSize = $options['streaming_batch_size'] ?? 50;
- $totalItems = count($items);
- $processed = 0;
-
- $results = [
- 'found' => $totalItems,
- 'created' => 0,
- 'updated' => 0,
- 'deleted' => 0,
- 'skipped' => 0,
- 'errors' => []
- ];
-
- // Process items in streaming batches
- $batches = array_chunk($items, $streamingBatchSize, true);
-
- foreach ($batches as $batchIndex => $batch) {
- $this->logger->debug("Processing {$schemaType} batch {$batchIndex}", [
- 'batch_size' => count($batch),
- 'progress' => round(($processed / $totalItems) * 100, 2) . '%',
- 'memory_usage_mb' => round(memory_get_usage(true) / 1024 / 1024, 2)
- ]);
-
- // Process batch with lazy loading of existing objects
- $batchResult = $this->processBatchWithLazyLoading($batch, $schemaType, $options);
-
- // Merge batch results
- $results['created'] += $batchResult['created'];
- $results['updated'] += $batchResult['updated'];
- $results['deleted'] += $batchResult['deleted'] ?? 0;
- $results['skipped'] += $batchResult['skipped'] ?? 0;
- $results['errors'] = array_merge($results['errors'], $batchResult['errors']);
-
- $processed += count($batch);
-
- // Update status with cumulative results
- if ($statusCallback) {
- $progress = round(($processed / $totalItems) * 100, 2);
- $statusCallback($schemaType, $progress, $results);
- }
-
- // Force garbage collection every few batches
- if ($batchIndex % 3 === 0) {
- gc_collect_cycles();
- }
- }
-
- return $results;
- }
-
- /**
- * Process a batch of items with lazy loading of existing objects
- * This avoids loading all existing objects into memory at once
- *
- * @param array $batch Batch of items to process
- * @param string $schemaType Type of schema being processed
- * @param array $options Processing options
- * @return array Processing results
- */
- private function processBatchWithLazyLoading(array $batch, string $schemaType, array $options): array
- {
- $results = [
- 'created' => 0,
- 'updated' => 0,
- 'deleted' => 0,
- 'skipped' => 0,
- 'errors' => []
- ];
-
- // Extract ArchiMate IDs for this batch
- $archiMateIds = array_map(function($item) {
- return $item['id'] ?? null;
- }, $batch);
- $archiMateIds = array_filter($archiMateIds);
-
- // Lazy load only the existing objects for this batch
- $existingObjects = $this->getExistingObjectsForBatch($archiMateIds, $schemaType);
-
- foreach ($batch as $itemId => $item) {
- try {
- $archiMateId = $item['id'] ?? null;
- if (!$archiMateId) {
- $results['errors'][] = "Missing ID for {$schemaType} item";
- continue;
- }
-
- // Use OpenRegister's built-in duplicate detection via UUID
- // No need for custom existing object lookup - OpenRegister handles it automatically
- $modelIdentifier = $options['model_identifier'] ?? null;
- $savedObject = $this->saveObject($item, $schemaType, $modelIdentifier);
-
- // Determine if the object was created or updated based on timestamps
- $action = $this->determineObjectAction($savedObject);
- if ($action === 'created') {
- $results['created']++;
- } else {
- $results['updated']++;
- }
-
- } catch (\Exception $e) {
- $this->logger->error("Error processing {$schemaType} item", [
- 'item_id' => $item['id'] ?? 'unknown',
- 'error' => $e->getMessage()
- ]);
- $results['errors'][] = $e->getMessage();
- }
- }
-
- return $results;
- }
-
- /**
- * Get existing objects for a specific batch of ArchiMate IDs
- * This implements lazy loading to avoid loading all objects into memory
- *
- * @param array $archiMateIds Array of ArchiMate IDs to look up
- * @param string $schemaType Type of schema
- * @return array Array of existing objects indexed by ArchiMate ID
- */
- private function getExistingObjectsForBatch(array $archiMateIds, string $schemaType): array
- {
- if (empty($archiMateIds)) {
- return [];
- }
-
- try {
- $objectService = $this->getObjectService();
- if (!$objectService) {
- $this->logger->error('ObjectService not available for batch lookup');
- return [];
- }
-
- $registerId = $this->getAmefRegisterId();
- $schemaId = $this->getAmefSchemaIdForType($schemaType);
-
- if (!$registerId || !$schemaId) {
- $this->logger->error("AMEF register or {$schemaType} schema not configured");
- return [];
- }
-
- // Build query to find objects with specific ArchiMate IDs
- $query = [
- '@self' => [
- 'register' => (int) $registerId,
- 'schema' => (int) $schemaId
- ],
- 'archimate_id' => [
- 'in' => $archiMateIds
- ]
- ];
-
- $objects = $objectService->searchObjects($query);
-
- // Index by ArchiMate ID for fast lookup
- $indexedObjects = [];
- foreach ($objects as $object) {
- $objectArray = $object instanceof \OCA\OpenRegister\Db\ObjectEntity ? $object->jsonSerialize() : $object;
- if (isset($objectArray['archimate_id'])) {
- $indexedObjects[$objectArray['archimate_id']] = $objectArray;
- }
- }
-
- $this->logger->debug("Lazy loaded existing objects for {$schemaType}", [
- 'requested_ids' => count($archiMateIds),
- 'found_objects' => count($indexedObjects)
- ]);
-
- return $indexedObjects;
-
- } catch (\Exception $e) {
- $this->logger->error("Error lazy loading existing objects for {$schemaType}", [
- 'error' => $e->getMessage()
- ]);
- return [];
- }
- }
-
- /**
- * Optimized object comparison that avoids deep array comparisons for large objects
- * Uses hash-based comparison for better performance
- *
- * @param array $existingObject Existing object data
- * @param array $newObjectData New object data
- * @return bool True if objects are equal
- */
- private function areObjectsEqualOptimized(array $existingObject, array $newObjectData): bool
- {
- // For large objects, use hash-based comparison instead of deep array comparison
- $existingHash = $this->calculateObjectHash($existingObject);
- $newHash = $this->calculateObjectHash($newObjectData);
-
- return $existingHash === $newHash;
- }
-
- /**
- * Calculate a hash for object comparison
- * This is much faster than deep array comparison for large objects
- *
- * @param array $object Object data
- * @return string Hash of the object
- */
- private function calculateObjectHash(array $object): string
- {
- // Normalize object for consistent hashing
- $normalized = $this->normalizeObjectForComparison($object, ['id', 'created_at', 'updated_at']);
-
- // Sort recursively for consistent ordering
- $this->sortArrayRecursively($normalized);
-
- // Calculate hash
- return md5(serialize($normalized));
- }
-}
\ No newline at end of file
diff --git a/lib/Service/ContactpersoonService.php b/lib/Service/ContactpersoonService.php
index 37db8cea..7c0aa663 100644
--- a/lib/Service/ContactpersoonService.php
+++ b/lib/Service/ContactpersoonService.php
@@ -5,13 +5,13 @@
* This file contains the service class for handling contact person-specific operations
* in the SoftwareCatalog application.
*
- * @category Service
- * @package OCA\SoftwareCatalog\Service
- * @author Conduction b.v.
+ * @category Service
+ * @package OCA\SoftwareCatalog\Service
+ * @author Conduction b.v.
* @copyright 2024 Conduction B.V.
- * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
- * @link https://github.com/ConductionNL/SoftwareCatalog
+ * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
*/
declare(strict_types=1);
@@ -28,29 +28,30 @@
/**
* Service for handling contact person-specific operations
- *
+ *
* This service provides functionality for contact person processing,
* user account creation, and group management.
- *
+ *
* @category Service
* @package OCA\SoftwareCatalog\Service
* @author Conduction b.v.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
class ContactpersoonService
{
/**
- * ContactpersoonService constructor
+ * ContactpersoonService constructor.
*
- * @param ContactPersonHandler $contactPersonHandler Contact person handler
- * @param GroupHandler $groupHandler Group handler
- * @param HierarchyHandler $hierarchyHandler Hierarchy handler
- * @param LoggerInterface $logger Logger interface
- * @param ContainerInterface $container Container interface
- * @param IAppManager $appManager App manager
- * @param IAppConfig $config Configuration service
+ * @param ContactPersonHandler $contactPersonHandler Contact person handler.
+ * @param GroupHandler $groupHandler Group handler.
+ * @param HierarchyHandler $hierarchyHandler Hierarchy handler.
+ * @param LoggerInterface $logger Logger interface.
+ * @param ContainerInterface $container Container interface.
+ * @param IAppManager $appManager App manager.
+ * @param IAppConfig $config Configuration service.
+ * @param SettingsService $settingsService Settings service.
*/
public function __construct(
private readonly ContactPersonHandler $contactPersonHandler,
@@ -62,206 +63,356 @@ public function __construct(
private readonly IAppConfig $config,
private readonly SettingsService $settingsService
) {
- }
+
+ }//end __construct()
+
+ /**
+ * Tracks contact UUIDs currently being processed to prevent event recursion.
+ *
+ * When saveObject() is called to update the username, it triggers ObjectUpdatedEvent
+ * which re-enters this method — this guard breaks that loop.
+ *
+ * @var array
+ */
+ private static array $processingContacts = [];
/**
- * Processes a contactpersoon object to create a user account
+ * Processes a contactpersoon object to create a user account.
*
* If the contactpersoon object doesn't have a user or the user is missing,
* this method will create a user account with appropriate status.
*
- * @param object $contactpersoonObject The contactpersoon object to process
- * @param bool $isUpdate Whether this is an update operation
- *
- * @return bool True if processing was successful
- * @throws \Exception If processing fails
+ * @param object $contactpersoonObject The contactpersoon object to process.
+ * @param bool $isUpdate Whether this is an update operation.
+ *
+ * @return bool True if processing was successful.
+ *
+ * @throws \Exception If processing fails.
*/
- public function processContactpersoon(object $contactpersoonObject, bool $isUpdate = false): bool
+ public function processContactpersoon(object $contactpersoonObject, bool $isUpdate=false): bool
{
$startTime = microtime(true);
-
+
try {
$contactData = $contactpersoonObject->getObject();
- $contactId = $contactpersoonObject->getId();
-
- $this->logger->info('ContactpersoonService: Starting contactpersoon processing', [
- 'contactId' => $contactId,
- 'isUpdate' => $isUpdate,
- 'hasEmail' => !empty($contactData['email'] ?? $contactData['e-mailadres'] ?? ''),
- 'hasOrganisation' => !empty($contactData['organisation'])
- ]);
-
- // Check if contactpersoon has required data
- // Schema uses 'e-mailadres' but some data may use 'email'
- $email = $contactData['email'] ?? $contactData['e-mailadres'] ?? '';
- if (empty($email)) {
- $this->logger->warning('ContactpersoonService: Contactpersoon has no email, skipping processing', [
- 'contactId' => $contactId
- ]);
+ $contactId = $contactpersoonObject->getId();
+
+ // Recursion guard: saveObject triggers ObjectUpdatedEvent which re-enters here.
+ if (isset(self::$processingContacts[$contactId]) === true) {
+ return true;
+ }
+
+ self::$processingContacts[$contactId] = true;
+
+ $emailValue = ($contactData['email'] ?? $contactData['e-mailadres'] ?? '');
+ $hasEmail = empty($emailValue) === false;
+ $hasOrganisation = empty($contactData['organisation']) === false;
+ $this->logger->info(
+ 'ContactpersoonService: Starting contactpersoon processing',
+ [
+ 'contactId' => $contactId,
+ 'isUpdate' => $isUpdate,
+ 'hasEmail' => $hasEmail,
+ 'hasOrganisation' => $hasOrganisation,
+ ]
+ );
+
+ // Check if contactpersoon has required data.
+ // Schema uses 'e-mailadres' but some data may use 'email'.
+ $email = ($contactData['email'] ?? $contactData['e-mailadres'] ?? '');
+ if (empty($email) === true) {
+ $this->logger->warning(
+ 'ContactpersoonService: Contactpersoon has no email, skipping processing',
+ ['contactId' => $contactId]
+ );
+ return false;
+ }
+
+ // Validate email format before attempting user creation.
+ // Imported contacts may have invalid emails that would cause Nextcloud user creation to fail.
+ if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
+ $this->logger->warning(
+ 'ContactpersoonService: Contactpersoon has invalid email, skipping user creation',
+ [
+ 'contactId' => $contactId,
+ 'email' => $email,
+ ]
+ );
return false;
}
- // Use email as username
+ // Use email as username.
$username = $email;
-
- // Check if user already exists
+
+ // Check if user already exists.
$userManager = \OC::$server->get('OCP\IUserManager');
- $user = $userManager->get($username);
+ $user = $userManager->get($username);
+
+ if ($user === null) {
+ // Check if organization is active before creating user account.
+ $organizationUuid = ($contactData['organisation'] ?? $contactData['organisatie'] ?? '');
- if (!$user) {
- // Check if organization is active before creating user account
- $organizationUuid = $contactData['organisation'] ?? $contactData['organisatie'] ?? '';
-
- if (!empty($organizationUuid)) {
+ if (empty($organizationUuid) === false) {
+ // Look up organization entity, creating backup if missing.
+ $organisationEntity = null;
try {
$organisationMapper = \OC::$server->get('OCA\OpenRegister\Db\OrganisationMapper');
$organisationEntity = $organisationMapper->findByUuid($organizationUuid);
-
- if ($organisationEntity && $organisationEntity->getActive()) {
- // Determine if this is the first contact for the organization
- $isFirstContact = $this->contactPersonHandler->isFirstContactForOrganization($contactpersoonObject, $contactData);
-
- // Create user account - organization is active
- $this->logger->info('ContactpersoonService: Creating user account for contactpersoon (org is active)', [
- 'contactId' => $contactId,
- 'username' => $username,
+ } catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
+ // Org entity missing — try backup creation from org object.
+ $this->logger->info(
+ 'ContactpersoonService: Organisation entity missing, attempting backup creation',
+ [
+ 'contactId' => $contactId,
'organizationUuid' => $organizationUuid,
- 'organizationActive' => true,
- 'isFirstContact' => $isFirstContact
- ]);
+ ]
+ );
+ try {
+ $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
+ $settingsService = \OC::$server->get('OCA\SoftwareCatalog\Service\SettingsService');
+ $voorzieningenConfig = $settingsService->getVoorzieningenConfig();
+ $orgObject = $objectService->find(
+ id: $organizationUuid,
+ register: ($voorzieningenConfig['register'] ?? ''),
+ schema: ($voorzieningenConfig['organisatie_schema'] ?? ''),
+ _rbac: false,
+ _multitenancy: false
+ );
+ if ($orgObject !== null) {
+ $orgData = $orgObject->getObject();
+ $orgStatus = strtolower(($orgData['status'] ?? ''));
+ if (in_array(needle: $orgStatus, haystack: ['actief', 'active']) === true) {
+ $syncServiceClass = 'OCA\SoftwareCatalog\Service\OrganizationSyncService';
+ $organizationSyncService = \OC::$server->get($syncServiceClass);
+ $backupStats = [
+ 'entitiesCreated' => 0,
+ 'entitiesUpdated' => 0,
+ ];
+ $organisationEntity = $organizationSyncService->ensureOrganisationEntityPublic(
+ orgObject: $orgObject,
+ stats: $backupStats
+ );
+ $this->logger->info(
+ 'ContactpersoonService: Backup entity created',
+ [
+ 'contactId' => $contactId,
+ 'organizationUuid' => $organizationUuid,
+ 'entityCreated' => $organisationEntity !== null,
+ ]
+ );
+ }
+ }//end if
+ } catch (\Exception $backupEx) {
+ $this->logger->error(
+ 'ContactpersoonService: Backup entity creation failed',
+ [
+ 'contactId' => $contactId,
+ 'organizationUuid' => $organizationUuid,
+ 'error' => $backupEx->getMessage(),
+ ]
+ );
+ }//end try
+ }//end try
- $success = $this->contactPersonHandler->createUserAccount($contactpersoonObject, $isFirstContact);
- if (!$success) {
+ try {
+ if ($organisationEntity !== null && $organisationEntity->getActive() === true) {
+ // Determine if this is the first contact for the organization.
+ $isFirstContact = $this->contactPersonHandler->isFirstContactForOrganization(
+ contactpersoonObject: $contactpersoonObject,
+ contactData: $contactData
+ );
+
+ // Create user account - organization is active.
+ $this->logger->info(
+ 'ContactpersoonService: Creating user account for contactpersoon (org is active)',
+ [
+ 'contactId' => $contactId,
+ 'username' => $username,
+ 'organizationUuid' => $organizationUuid,
+ 'isFirstContact' => $isFirstContact,
+ ]
+ );
+
+ $success = $this->contactPersonHandler->createUserAccount(
+ contactpersoonObject: $contactpersoonObject,
+ isFirstContact: $isFirstContact
+ );
+ if ($success === false) {
throw new \Exception('Failed to create user account');
}
- // Link user to organization entity
- $this->contactPersonHandler->addUserToOrganizationEntity($contactpersoonObject, $username, $organizationUuid);
-
- // Update contactpersoon object owner to user UID
- $this->updateContactpersoonObjectOwner($contactpersoonObject, $username);
-
- $this->logger->info('ContactpersoonService: Successfully created user account', [
- 'contactId' => $contactId,
- 'username' => $username
- ]);
+ // Link user to organization entity.
+ $this->contactPersonHandler->addUserToOrganizationEntity(
+ contactpersoonObject: $contactpersoonObject,
+ username: $username,
+ organizationUuid: $organizationUuid
+ );
+
+ // Update contactpersoon object owner to user UID.
+ $this->updateContactpersoonObjectOwner(
+ contactObject: $contactpersoonObject,
+ userUID: $username
+ );
+
+ $this->logger->info(
+ 'ContactpersoonService: Successfully created user account',
+ [
+ 'contactId' => $contactId,
+ 'username' => $username,
+ ]
+ );
} else {
- $this->logger->info('ContactpersoonService: Skipping user creation - organization not active or not found', [
- 'contactId' => $contactId,
- 'organizationUuid' => $organizationUuid,
- 'organizationFound' => $organisationEntity !== null,
- 'organizationActive' => $organisationEntity ? $organisationEntity->getActive() : false
- ]);
+ if ($organisationEntity !== null) {
+ $orgActive = $organisationEntity->getActive();
+ } else {
+ $orgActive = false;
+ }
+
+ $this->logger->info(
+ // phpcs:ignore Generic.Files.LineLength.TooLong
+ 'ContactpersoonService: Skipping user creation - organization not active or entity not found',
+ [
+ 'contactId' => $contactId,
+ 'organizationUuid' => $organizationUuid,
+ 'organizationFound' => $organisationEntity !== null,
+ 'organizationActive' => $orgActive,
+ ]
+ );
return false;
- }
+ }//end if
} catch (\Exception $e) {
- $this->logger->info('ContactpersoonService: Skipping user creation - organization not found in entity table (not active)', [
- 'contactId' => $contactId,
- 'organizationUuid' => $organizationUuid,
- 'reason' => 'Organization not found in entity table'
- ]);
+ $this->logger->error(
+ 'ContactpersoonService: User creation failed',
+ [
+ 'contactId' => $contactId,
+ 'organizationUuid' => $organizationUuid,
+ 'error' => $e->getMessage(),
+ ]
+ );
return false;
- }
+ }//end try
} else {
- $this->logger->warning('ContactpersoonService: Contactpersoon has no organization reference, skipping user creation', [
- 'contactId' => $contactId
- ]);
+ $this->logger->warning(
+ 'ContactpersoonService: Contactpersoon has no organization reference, skipping user creation',
+ ['contactId' => $contactId]
+ );
return false;
- }
+ }//end if
} else {
- $this->logger->info('ContactpersoonService: User account already exists', [
- 'contactId' => $contactId,
- 'username' => $username
- ]);
- }
-
- // Update user groups based on contactpersoon data
- $this->updateUserGroups($contactpersoonObject, $username);
+ $this->logger->info(
+ 'ContactpersoonService: User account already exists',
+ [
+ 'contactId' => $contactId,
+ 'username' => $username,
+ ]
+ );
+ }//end if
+
+ // Update user groups based on contactpersoon data.
+ $this->updateUserGroups(
+ contactpersoonObject: $contactpersoonObject,
+ username: $username
+ );
- // Ensure organization has at least one beheerder
- $this->ensureOrganizationBeheerder($contactpersoonObject, $username);
+ // Ensure organization has at least one beheerder.
+ $this->ensureOrganizationBeheerder(
+ contactpersoonObject: $contactpersoonObject,
+ username: $username
+ );
- // Update the contactpersoon object with username if not set
- if (empty($contactData['username'])) {
- $this->updateContactpersoonUsername($contactpersoonObject, $username);
+ // Update the contactpersoon object with username if not set.
+ if (empty($contactData['username']) === true) {
+ $this->updateContactpersoonUsername(
+ contactpersoonObject: $contactpersoonObject,
+ username: $username
+ );
}
- $processingTime = round((microtime(true) - $startTime) * 1000, 2);
- $this->logger->info('ContactpersoonService: Successfully processed contactpersoon', [
- 'contactId' => $contactId,
- 'username' => $username,
- 'processingTime' => $processingTime . 'ms'
- ]);
+ $processingTime = round(((microtime(true) - $startTime) * 1000), 2);
+ $this->logger->info(
+ 'ContactpersoonService: Successfully processed contactpersoon',
+ [
+ 'contactId' => $contactId,
+ 'username' => $username,
+ 'processingTime' => $processingTime.'ms',
+ ]
+ );
return true;
-
} catch (\Exception $e) {
- $this->logger->error('ContactpersoonService: Failed to process contactpersoon object', [
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString(),
- 'objectId' => $contactpersoonObject->getId() ?? 'unknown',
- 'processingTime' => round((microtime(true) - $startTime) * 1000, 2) . 'ms'
- ]);
+ $this->logger->error(
+ 'ContactpersoonService: Failed to process contactpersoon object',
+ [
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
+ 'objectId' => ($contactpersoonObject->getId() ?? 'unknown'),
+ 'processingTime' => round(((microtime(true) - $startTime) * 1000), 2).'ms',
+ ]
+ );
throw $e;
- }
- }
+ } finally {
+ unset(self::$processingContacts[$contactId]);
+ }//end try
+
+ }//end processContactpersoon()
/**
* Updates user groups based on contactpersoon data
*
* @param object $contactpersoonObject The contactpersoon object
- * @param string $username The username to update groups for
- *
+ * @param string $username The username to update groups for
+ *
* @return void
*/
public function updateUserGroups(object $contactpersoonObject, string $username): void
{
- // Use the new organization type-based logic instead of old role-based logic
+ // Use the new organization type-based logic instead of old role-based logic.
$userManager = \OC::$server->get('OCP\IUserManager');
- $user = $userManager->get($username);
- if ($user) {
+ $user = $userManager->get($username);
+ if ($user !== null) {
$contactData = $contactpersoonObject->getObject();
- $this->contactPersonHandler->updateUserGroupsFromContactData($user, $contactData);
+ $this->contactPersonHandler->updateUserGroupsFromContactData(
+ user: $user,
+ contactData: $contactData
+ );
} else {
$this->logger->warning('User not found for group update', ['username' => $username]);
}
- }
+
+ }//end updateUserGroups()
/**
* Ensures organization has at least one beheerder and manages user hierarchy
*
* @param object $contactpersoonObject The contactpersoon object
- * @param string $username The username being processed
- *
+ * @param string $username The username being processed
+ *
* @return void
*/
public function ensureOrganizationBeheerder(object $contactpersoonObject, string $username): void
{
- $this->hierarchyHandler->ensureOrganizationBeheerder($contactpersoonObject, $username);
- }
+ $this->hierarchyHandler->ensureOrganizationBeheerder(
+ contactgegevensObject: $contactpersoonObject,
+ username: $username
+ );
+
+ }//end ensureOrganizationBeheerder()
/**
* Gets a user's manager
*
* @param string $username The username
- *
+ *
* @return string|null The manager's username or null if not set
*/
public function getUserManager(string $username): ?string
{
return $this->contactPersonHandler->getUserManager($username);
- }
- /**
- * Updates contactpersoon object with username
- *
- * @param object $contactpersoonObject The contactpersoon object
- * @param string $username The username to set
- *
- * @return void
- */
+ }//end getUserManager()
+
/**
* Normalize contact data types to match schema expectations.
* This ensures numeric strings are properly typed as strings.
@@ -272,96 +423,126 @@ public function getUserManager(string $username): ?string
*/
private function normalizeContactDataTypes(array $data): array
{
- // Fields that should always be strings according to the contactpersoon schema
- $stringFields = ['voornaam', 'tussenvoegsel', 'achternaam', 'functie', 'telefoonnummer', 'username'];
+ // Fields that should always be strings according to the contactpersoon schema.
+ $stringFields = [
+ 'voornaam',
+ 'tussenvoegsel',
+ 'achternaam',
+ 'functie',
+ 'telefoonnummer',
+ 'username',
+ ];
foreach ($stringFields as $field) {
- if (isset($data[$field]) && (is_int($data[$field]) || is_float($data[$field]))) {
+ if (isset($data[$field]) === true && (is_int($data[$field]) === true || is_float($data[$field]) === true)) {
$data[$field] = (string) $data[$field];
}
}
return $data;
- }
+ }//end normalizeContactDataTypes()
+
+ /**
+ * Updates contactpersoon object with username.
+ *
+ * @param object $contactpersoonObject The contactpersoon object.
+ * @param string $username The username to set.
+ *
+ * @return void
+ */
private function updateContactpersoonUsername(object $contactpersoonObject, string $username): void
{
try {
- $objectService = $this->getObjectService();
- if (!$objectService) {
- $this->logger->warning('ContactpersoonService: ObjectService not available for username update');
- return;
- }
-
$contactData = $contactpersoonObject->getObject();
- // Normalize data types to ensure schema validation passes
- $contactData = $this->normalizeContactDataTypes($contactData);
$contactData['username'] = $username;
-
- $updatedObject = $objectService->saveObject(
- $contactData, // object data (array)
- [], // extend (array)
- $contactpersoonObject->getRegister(), // register (int)
- $contactpersoonObject->getSchema(), // schema (int)
- $contactpersoonObject->getUuid() // uuid (string)
+ $contactpersoonObject->setObject($contactData);
+
+ // FIX #434: Use ObjectEntityMapper directly instead of ObjectService::saveObject().
+ // To avoid validation errors on the organisatie field (stored as UUID string but.
+ // Schema expects object type) and to avoid triggering ObjectUpdatedEvent cascades.
+ // That could interfere with the ongoing org activation process.
+ $objectMapper = \OC::$server->get('OCA\OpenRegister\Db\ObjectEntityMapper');
+ $objectMapper->update($contactpersoonObject);
+
+ $this->logger->info(
+ 'ContactpersoonService: Updated contactpersoon with username',
+ [
+ 'contactId' => $contactpersoonObject->getId(),
+ 'username' => $username,
+ ]
);
-
- $this->logger->info('ContactpersoonService: Updated contactpersoon with username', [
- 'contactId' => $contactpersoonObject->getId(),
- 'username' => $username
- ]);
-
} catch (\Exception $e) {
- $this->logger->error('ContactpersoonService: Failed to update contactpersoon username', [
- 'contactId' => $contactpersoonObject->getId(),
- 'username' => $username,
- 'error' => $e->getMessage()
- ]);
- }
- }
+ $this->logger->error(
+ 'ContactpersoonService: Failed to update contactpersoon username',
+ [
+ 'contactId' => $contactpersoonObject->getId(),
+ 'username' => $username,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+
+ }//end updateContactpersoonUsername()
/**
* Handles contactpersoon updates, particularly role changes
*
* @param object $contactpersoonObject The updated contactpersoon object
* @param object|null $oldContactpersoonObject The previous contactpersoon object
- *
+ *
* @return void
*/
- public function handleContactpersoonUpdate(object $contactpersoonObject, object $oldContactpersoonObject = null): void
+ public function handleContactpersoonUpdate(object $contactpersoonObject, object $oldContactpersoonObject=null): void
{
try {
$contactData = $contactpersoonObject->getObject();
- $contactId = $contactpersoonObject->getId();
-
- $this->logger->info('ContactpersoonService: Handling contactpersoon update', [
- 'contactId' => $contactId,
- 'hasOldObject' => $oldContactpersoonObject !== null
- ]);
+ $contactId = $contactpersoonObject->getId();
+
+ $this->logger->info(
+ 'ContactpersoonService: Handling contactpersoon update',
+ [
+ 'contactId' => $contactId,
+ 'hasOldObject' => $oldContactpersoonObject !== null,
+ ]
+ );
- // Process the contactpersoon (this will handle user creation/updates)
- $this->processContactpersoon($contactpersoonObject, true);
+ // Process the contactpersoon (this will handle user creation/updates).
+ $this->processContactpersoon(
+ contactpersoonObject: $contactpersoonObject,
+ isUpdate: true
+ );
- // If we have old object, check for role changes
- if ($oldContactpersoonObject) {
- $this->handleRoleChanges($contactpersoonObject, $oldContactpersoonObject);
+ // If we have old object, check for role changes.
+ if ($oldContactpersoonObject !== null) {
+ $this->handleRoleChanges(
+ newContactpersoonObject: $contactpersoonObject,
+ oldContactpersoonObject: $oldContactpersoonObject
+ );
}
// Sync name/functie fields back to the Nextcloud user when changed.
- $this->syncNameFieldsToUser($contactpersoonObject, $oldContactpersoonObject);
-
- $this->logger->info('ContactpersoonService: Successfully handled contactpersoon update', [
- 'contactId' => $contactId
- ]);
+ $this->syncNameFieldsToUser(
+ contactpersoonObject: $contactpersoonObject,
+ oldContactpersoonObject: $oldContactpersoonObject
+ );
+ $this->logger->info(
+ 'ContactpersoonService: Successfully handled contactpersoon update',
+ ['contactId' => $contactId]
+ );
} catch (\Exception $e) {
- $this->logger->error('ContactpersoonService: Failed to handle contactpersoon update', [
- 'contactId' => $contactpersoonObject->getId(),
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
- }
- }
+ $this->logger->error(
+ 'ContactpersoonService: Failed to handle contactpersoon update',
+ [
+ 'contactId' => $contactpersoonObject->getId(),
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+ }//end try
+
+ }//end handleContactpersoonUpdate()
/**
* Syncs name/functie fields from contactpersoon to the corresponding Nextcloud user.
@@ -374,10 +555,20 @@ public function handleContactpersoonUpdate(object $contactpersoonObject, object
private function syncNameFieldsToUser(object $contactpersoonObject, ?object $oldContactpersoonObject): void
{
$newData = $contactpersoonObject->getObject();
- $oldData = $oldContactpersoonObject !== null ? $oldContactpersoonObject->getObject() : [];
+ if ($oldContactpersoonObject !== null) {
+ $oldData = $oldContactpersoonObject->getObject();
+ } else {
+ $oldData = [];
+ }
// Check if any name/functie fields have changed.
- $nameFields = ['voornaam', 'tussenvoegsel', 'achternaam', 'functie', 'e-mailadres'];
+ $nameFields = [
+ 'voornaam',
+ 'tussenvoegsel',
+ 'achternaam',
+ 'functie',
+ 'e-mailadres',
+ ];
$hasNameChanges = false;
foreach ($nameFields as $field) {
@@ -392,65 +583,80 @@ private function syncNameFieldsToUser(object $contactpersoonObject, ?object $old
}
// Find the corresponding Nextcloud user.
- $username = $newData['username'] ?? '';
+ $username = ($newData['username'] ?? '');
if (empty($username) === true) {
return;
}
$userManager = \OC::$server->get('OCP\IUserManager');
- $user = $userManager->get($username);
+ $user = $userManager->get($username);
if ($user === null) {
- $this->logger->debug('ContactpersoonService: No Nextcloud user found for name sync', [
- 'username' => $username,
- ]);
+ $this->logger->debug(
+ 'ContactpersoonService: No Nextcloud user found for name sync',
+ ['username' => $username]
+ );
return;
}
- $this->logger->info('ContactpersoonService: Syncing contactpersoon name fields to user', [
- 'username' => $username,
- 'contactId' => $contactpersoonObject->getId(),
- 'changedData' => array_intersect_key(
- $newData,
- array_flip($nameFields)
- ),
- ]);
-
- $this->contactPersonHandler->storeContactNameFields($user, $newData);
- }
+ $this->logger->info(
+ 'ContactpersoonService: Syncing contactpersoon name fields to user',
+ [
+ 'username' => $username,
+ 'contactId' => $contactpersoonObject->getId(),
+ 'changedData' => array_intersect_key(
+ $newData,
+ array_flip($nameFields)
+ ),
+ ]
+ );
+
+ $this->contactPersonHandler->storeContactNameFields(
+ user: $user,
+ contactData: $newData
+ );
+
+ }//end syncNameFieldsToUser()
/**
* Handles role changes between old and new contactpersoon objects
*
* @param object $newContactpersoonObject The new contactpersoon object
* @param object $oldContactpersoonObject The old contactpersoon object
- *
+ *
* @return void
*/
private function handleRoleChanges(object $newContactpersoonObject, object $oldContactpersoonObject): void
{
$newData = $newContactpersoonObject->getObject();
$oldData = $oldContactpersoonObject->getObject();
-
- $newRoles = $newData['roles'] ?? [];
- $oldRoles = $oldData['roles'] ?? [];
-
- // Check if roles have changed
+
+ $newRoles = ($newData['roles'] ?? []);
+ $oldRoles = ($oldData['roles'] ?? []);
+
+ // Check if roles have changed.
if ($newRoles !== $oldRoles) {
- $username = $newData['email'] ?? $newData['e-mailadres'] ?? $newData['username'] ?? '';
- if ($username) {
- $this->logger->info('ContactpersoonService: Roles changed, updating user groups', [
- 'contactId' => $newContactpersoonObject->getId(),
- 'username' => $username,
- 'oldRoles' => $oldRoles,
- 'newRoles' => $newRoles
- ]);
-
- // Update user groups based on new roles
- $this->updateUserGroups($newContactpersoonObject, $username);
+ $username = ($newData['email'] ?? $newData['e-mailadres'] ?? $newData['username'] ?? '');
+ if (empty($username) === false) {
+ $this->logger->info(
+ 'ContactpersoonService: Roles changed, updating user groups',
+ [
+ 'contactId' => $newContactpersoonObject->getId(),
+ 'username' => $username,
+ 'oldRoles' => $oldRoles,
+ 'newRoles' => $newRoles,
+ ]
+ );
+
+ // Update user groups based on new roles.
+ $this->updateUserGroups(
+ contactpersoonObject: $newContactpersoonObject,
+ username: $username
+ );
}
}
- }
+
+ }//end handleRoleChanges()
/**
* Gets the ObjectService instance
@@ -459,117 +665,139 @@ private function handleRoleChanges(object $newContactpersoonObject, object $oldC
*/
private function getObjectService(): ?\OCA\OpenRegister\Service\ObjectService
{
- if (!$this->appManager->isEnabledForUser('openregister')) {
+ if ($this->appManager->isEnabledForUser('openregister') === false) {
return null;
}
try {
return $this->container->get('OCA\OpenRegister\Service\ObjectService');
} catch (\Exception $e) {
- $this->logger->error('ContactpersoonService: Failed to get ObjectService: ' . $e->getMessage());
+ $this->logger->error('ContactpersoonService: Failed to get ObjectService: '.$e->getMessage());
return null;
}
- }
+
+ }//end getObjectService()
/**
* Handles contact person deletion
*
* @param object $contactObject The contact object being deleted
- *
+ *
* @return void
*/
public function handleContactDeletion(object $contactObject): void
{
try {
$contactData = $contactObject->getObject();
- $username = $contactData['email'] ?? $contactData['e-mailadres'] ?? $contactData['username'] ?? '';
-
- if (!$username) {
- $this->logger->warning('ContactpersoonService: Contact deletion - no username found', [
- 'contactId' => $contactObject->getId()
- ]);
+ $username = ($contactData['email'] ?? $contactData['e-mailadres'] ?? $contactData['username'] ?? '');
+
+ if (empty($username) === true) {
+ $this->logger->warning(
+ 'ContactpersoonService: Contact deletion - no username found',
+ [
+ 'contactId' => $contactObject->getId(),
+ ]
+ );
return;
}
- $this->logger->info('ContactpersoonService: Handling contact deletion', [
- 'contactId' => $contactObject->getId(),
- 'username' => $username
- ]);
+ $this->logger->info(
+ 'ContactpersoonService: Handling contact deletion',
+ [
+ 'contactId' => $contactObject->getId(),
+ 'username' => $username,
+ ]
+ );
- // Get user manager to disable the user
+ // Get user manager to disable the user.
$userManager = \OC::$server->get('OCP\IUserManager');
- $user = $userManager->get($username);
-
- if ($user) {
- // Disable the user instead of deleting
+ $user = $userManager->get($username);
+
+ if ($user !== null) {
+ // Disable the user instead of deleting.
$user->setEnabled(false);
-
- $this->logger->info('ContactpersoonService: Disabled user for deleted contact', [
- 'contactId' => $contactObject->getId(),
- 'username' => $username
- ]);
+
+ $this->logger->info(
+ 'ContactpersoonService: Disabled user for deleted contact',
+ [
+ 'contactId' => $contactObject->getId(),
+ 'username' => $username,
+ ]
+ );
} else {
- $this->logger->warning('ContactpersoonService: User not found for deleted contact', [
- 'contactId' => $contactObject->getId(),
- 'username' => $username
- ]);
+ $this->logger->warning(
+ 'ContactpersoonService: User not found for deleted contact',
+ [
+ 'contactId' => $contactObject->getId(),
+ 'username' => $username,
+ ]
+ );
}
-
} catch (\Exception $e) {
- $this->logger->error('ContactpersoonService: Failed to handle contact deletion', [
- 'contactId' => $contactObject->getId(),
- 'error' => $e->getMessage()
- ]);
- }
- }
+ $this->logger->error(
+ 'ContactpersoonService: Failed to handle contact deletion',
+ [
+ 'contactId' => $contactObject->getId(),
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+
+ }//end handleContactDeletion()
/**
* Gets all contact persons for an organization
*
* @param string $organizationUuid The organization UUID
- *
+ *
* @return array Array of contact person objects
*/
public function getContactPersonsForOrganization(string $organizationUuid): array
{
try {
$objectService = $this->getObjectService();
- if (!$objectService) {
+ if ($objectService === null) {
return [];
}
- $voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $contactSchema = $voorzieningenConfig['contactpersoon_schema'] ?? null;
- $register = $voorzieningenConfig['register'] ?? null;
-
- // Skip if no proper configuration is available
- if (!$contactSchema || !$register) {
- $this->logger->warning('ContactpersoonService: Missing Voorzieningen configuration', [
- 'contactSchema' => $contactSchema,
- 'register' => $register
- ]);
- return [];
- }
-
- // Build query for searchObjects method
+ $voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
+ $contactSchema = ($voorzieningenConfig['contactpersoon_schema'] ?? null);
+ $register = ($voorzieningenConfig['register'] ?? null);
+
+ // Skip if no proper configuration is available.
+ if ($contactSchema === null || $register === null) {
+ $this->logger->warning(
+ 'ContactpersoonService: Missing Voorzieningen configuration',
+ [
+ 'contactSchema' => $contactSchema,
+ 'register' => $register,
+ ]
+ );
+ return [];
+ }
+
+ // Build query for searchObjects method.
$query = [
- '@self' => [
+ '@self' => [
'register' => (int) $register,
- 'schema' => (int) $contactSchema
+ 'schema' => (int) $contactSchema,
],
- 'organisation' => $organizationUuid
+ 'organisation' => $organizationUuid,
];
-
- return $objectService->searchObjects($query);
+ return $objectService->searchObjects($query);
} catch (\Exception $e) {
- $this->logger->error('ContactpersoonService: Failed to get contact persons for organization', [
- 'organizationUuid' => $organizationUuid,
- 'error' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'ContactpersoonService: Failed to get contact persons for organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'error' => $e->getMessage(),
+ ]
+ );
return [];
- }
- }
+ }//end try
+
+ }//end getContactPersonsForOrganization()
/**
* Gets all contact persons for an organization with user details spliced in
@@ -578,122 +806,149 @@ public function getContactPersonsForOrganization(string $organizationUuid): arra
* and enhances each contact person with their corresponding user details from Nextcloud.
*
* @param string $organizationUuid The organization UUID to get contact persons for
- *
+ *
* @return array Array of contact person objects with user details spliced in
- *
+ *
* @throws \Exception If contact person retrieval fails
*/
public function getContactPersonsWithUserDetailsForOrganization(string $organizationUuid): array
{
try {
- $this->logger->info('ContactpersoonService: Getting contact persons with user details for organization', [
- 'organizationUuid' => $organizationUuid
- ]);
-
- // Get contact persons for the organization
- $contactPersons = $this->getContactPersonsForOrganization($organizationUuid);
-
- if (empty($contactPersons)) {
- $this->logger->info('ContactpersoonService: No contact persons found for organization', [
- 'organizationUuid' => $organizationUuid
- ]);
+ $this->logger->info(
+ 'ContactpersoonService: Getting contact persons with user details for organization',
+ ['organizationUuid' => $organizationUuid]
+ );
+
+ // Get contact persons for the organization.
+ $contactPersons = $this->getContactPersonsForOrganization(organizationUuid: $organizationUuid);
+
+ if (empty($contactPersons) === true) {
+ $this->logger->info(
+ 'ContactpersoonService: No contact persons found for organization',
+ ['organizationUuid' => $organizationUuid]
+ );
return [];
}
- $this->logger->info('ContactpersoonService: Found contact persons, fetching user details', [
- 'organizationUuid' => $organizationUuid,
- 'contactPersonCount' => count($contactPersons)
- ]);
+ $this->logger->info(
+ 'ContactpersoonService: Found contact persons, fetching user details',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'contactPersonCount' => count($contactPersons),
+ ]
+ );
- // Get user manager to fetch user details
- $userManager = \OC::$server->get('OCP\IUserManager');
+ // Get user manager to fetch user details.
+ $userManager = \OC::$server->get('OCP\IUserManager');
$enhancedContactPersons = [];
- // Loop through each contact person and fetch user details
+ // Loop through each contact person and fetch user details.
foreach ($contactPersons as $contactPerson) {
try {
$contactData = $contactPerson->getObject();
- $username = $contactData['username'] ?? null;
-
- // Initialize user details as null
+ $username = ($contactData['username'] ?? null);
+
+ // Initialize user details as null.
$userDetails = null;
-
- // If username exists, fetch user details
- if ($username) {
+
+ // If username exists, fetch user details.
+ if ($username !== null) {
$user = $userManager->get($username);
- if ($user) {
+ if ($user !== null) {
$userDetails = [
- 'uid' => $user->getUID(),
- 'email' => $user->getEMailAddress(),
+ 'uid' => $user->getUID(),
+ 'email' => $user->getEMailAddress(),
'displayName' => $user->getDisplayName(),
- 'enabled' => $user->isEnabled(),
- 'lastLogin' => $user->getLastLogin(),
- 'backend' => $user->getBackendClassName(),
- 'home' => $user->getHome(),
+ 'enabled' => $user->isEnabled(),
+ 'lastLogin' => $user->getLastLogin(),
+ 'backend' => $user->getBackendClassName(),
+ 'home' => $user->getHome(),
'avatarImage' => $user->getAvatarImage(64)->data(),
- 'quota' => $user->getQuota(),
- 'freeQuota' => $user->getFreeQuota()
+ 'quota' => $user->getQuota(),
+ 'freeQuota' => $user->getFreeQuota(),
];
-
- $this->logger->debug('ContactpersoonService: Fetched user details', [
- 'contactPersonId' => $contactPerson->getId(),
- 'username' => $username,
- 'userEnabled' => $user->isEnabled()
- ]);
+
+ $this->logger->debug(
+ 'ContactpersoonService: Fetched user details',
+ [
+ 'contactPersonId' => $contactPerson->getId(),
+ 'username' => $username,
+ 'userEnabled' => $user->isEnabled(),
+ ]
+ );
} else {
- $this->logger->warning('ContactpersoonService: User not found for username', [
- 'contactPersonId' => $contactPerson->getId(),
- 'username' => $username
- ]);
- }
+ $this->logger->warning(
+ 'ContactpersoonService: User not found for username',
+ [
+ 'contactPersonId' => $contactPerson->getId(),
+ 'username' => $username,
+ ]
+ );
+ }//end if
} else {
- $this->logger->debug('ContactpersoonService: No username found for contact person', [
- 'contactPersonId' => $contactPerson->getId()
- ]);
- }
+ $this->logger->debug(
+ 'ContactpersoonService: No username found for contact person',
+ [
+ 'contactPersonId' => $contactPerson->getId(),
+ ]
+ );
+ }//end if
- // Create enhanced contact person object with user details spliced in
+ // Create enhanced contact person object with user details spliced in.
$enhancedContactData = $contactData;
$enhancedContactData['userDetails'] = $userDetails;
-
- // Create a new object with the enhanced data
+
+ // Create a new object with the enhanced data.
$enhancedContactPerson = clone $contactPerson;
$enhancedContactPerson->setObject($enhancedContactData);
-
+
$enhancedContactPersons[] = $enhancedContactPerson;
-
} catch (\Exception $e) {
- $this->logger->error('ContactpersoonService: Failed to process contact person', [
- 'contactPersonId' => $contactPerson->getId(),
- 'organizationUuid' => $organizationUuid,
- 'error' => $e->getMessage()
- ]);
-
- // Still add the contact person without user details
- $enhancedContactPersons[] = $contactPerson;
- }
- }
+ $this->logger->error(
+ 'ContactpersoonService: Failed to process contact person',
+ [
+ 'contactPersonId' => $contactPerson->getId(),
+ 'organizationUuid' => $organizationUuid,
+ 'error' => $e->getMessage(),
+ ]
+ );
- $this->logger->info('ContactpersoonService: Successfully enhanced contact persons with user details', [
- 'organizationUuid' => $organizationUuid,
- 'totalContactPersons' => count($enhancedContactPersons),
- 'contactPersonsWithUserDetails' => count(array_filter($enhancedContactPersons, function($cp) {
- $data = $cp->getObject();
- return $data['userDetails'] !== null;
- }))
- ]);
+ // Still add the contact person without user details.
+ $enhancedContactPersons[] = $contactPerson;
+ }//end try
+ }//end foreach
+
+ $this->logger->info(
+ 'ContactpersoonService: Successfully enhanced contact persons with user details',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'totalContactPersons' => count($enhancedContactPersons),
+ 'contactPersonsWithUserDetails' => count(
+ array_filter(
+ $enhancedContactPersons,
+ static function ($cp) {
+ $data = $cp->getObject();
+ return $data['userDetails'] !== null;
+ }
+ )
+ ),
+ ]
+ );
return $enhancedContactPersons;
-
} catch (\Exception $e) {
- $this->logger->error('ContactpersoonService: Failed to get contact persons with user details for organization', [
- 'organizationUuid' => $organizationUuid,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'ContactpersoonService: Failed to get contact persons with user details for organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
throw $e;
- }
- }
+ }//end try
+
+ }//end getContactPersonsWithUserDetailsForOrganization()
/**
* Gets bulk user information for multiple contact persons
@@ -702,48 +957,55 @@ public function getContactPersonsWithUserDetailsForOrganization(string $organiza
* which is more efficient than individual calls.
*
* @param array $contactpersoonIds Array of contact person IDs/UUIDs
- *
+ *
* @return array Array of user information keyed by contact person ID
- *
+ *
* @throws \Exception If bulk user info retrieval fails
*/
public function getBulkUserInfo(array $contactpersoonIds): array
{
try {
- $this->logger->info('ContactpersoonService: Getting bulk user info', [
- 'contactpersoonCount' => count($contactpersoonIds)
- ]);
+ $this->logger->info(
+ 'ContactpersoonService: Getting bulk user info',
+ [
+ 'contactpersoonCount' => count($contactpersoonIds),
+ ]
+ );
$bulkUserInfo = [];
- $userManager = \OC::$server->get('OCP\IUserManager');
-
- // Get contact person register and schema from settings
+ $userManager = \OC::$server->get('OCP\IUserManager');
+
+ // Get contact person register and schema from settings.
$contactRegister = null;
- $contactSchema = null;
+ $contactSchema = null;
try {
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $contactRegister = (int) ($voorzieningenConfig['register'] ?? 2);
- $contactSchema = (int) ($voorzieningenConfig['contactpersoon_schema'] ?? 25);
+ $contactRegister = (int) ($voorzieningenConfig['register'] ?? 2);
+ $contactSchema = (int) ($voorzieningenConfig['contactpersoon_schema'] ?? 25);
} catch (\Exception $e) {
- $this->logger->warning('Could not get contact person schema config, using defaults', [
- 'error' => $e->getMessage()
- ]);
+ $this->logger->warning(
+ 'Could not get contact person schema config, using defaults',
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
$contactRegister = 2;
- $contactSchema = 25;
+ $contactSchema = 25;
}
foreach ($contactpersoonIds as $contactpersoonId) {
try {
- // Get contactpersoon from OpenRegister
+ // Get contactpersoon from OpenRegister.
$objectService = $this->getObjectService();
- if (!$objectService) {
- $this->logger->warning('ContactpersoonService: ObjectService not available for bulk user info', [
- 'contactpersoonId' => $contactpersoonId
- ]);
+ if ($objectService === null) {
+ $this->logger->warning(
+ 'ContactpersoonService: ObjectService not available for bulk user info',
+ ['contactpersoonId' => $contactpersoonId]
+ );
continue;
}
- // Find the contactpersoon object with register and schema specified
+ // Find the contactpersoon object with register and schema specified.
$contactObject = $objectService->findSilent(
id: $contactpersoonId,
_extend: [],
@@ -753,188 +1015,232 @@ public function getBulkUserInfo(array $contactpersoonIds): array
_rbac: false,
_multitenancy: false
);
-
- if (!$contactObject) {
- $this->logger->warning('ContactpersoonService: Contactpersoon not found for bulk user info', [
- 'contactpersoonId' => $contactpersoonId
- ]);
+
+ if ($contactObject === null) {
+ $this->logger->warning(
+ 'ContactpersoonService: Contactpersoon not found for bulk user info',
+ ['contactpersoonId' => $contactpersoonId]
+ );
$bulkUserInfo[$contactpersoonId] = [
- 'hasUser' => false,
+ 'hasUser' => false,
'username' => null,
- 'groups' => []
+ 'groups' => [],
];
continue;
}
$contactData = $contactObject->getObject();
- $username = $contactData['username'] ?? null;
-
+ $username = $contactData['username'] ?? null;
+
$userInfo = [
- 'hasUser' => !empty($username),
+ 'hasUser' => empty($username) === false,
'username' => $username,
- 'groups' => []
+ 'groups' => [],
];
- // If user exists, get their current groups
- if ($username) {
+ // If user exists, get their current groups.
+ if (empty($username) === false) {
$user = $userManager->get($username);
- if ($user) {
- $groupManager = \OC::$server->get('OCP\IGroupManager');
- $userGroups = $groupManager->getUserGroups($user);
- $userInfo['groups'] = array_keys($userGroups);
+ if ($user !== null) {
+ $groupManager = \OC::$server->get('OCP\IGroupManager');
+ $userGroups = $groupManager->getUserGroups($user);
+ $userInfo['groups'] = array_keys($userGroups);
$userInfo['enabled'] = $user->isEnabled();
$userInfo['displayName'] = $user->getDisplayName();
- $userInfo['lastLogin'] = $user->getLastLogin();
+ $userInfo['lastLogin'] = $user->getLastLogin();
} else {
- $this->logger->warning('ContactpersoonService: User not found for bulk user info', [
- 'contactpersoonId' => $contactpersoonId,
- 'username' => $username
- ]);
+ $this->logger->warning(
+ 'ContactpersoonService: User not found for bulk user info',
+ [
+ 'contactpersoonId' => $contactpersoonId,
+ 'username' => $username,
+ ]
+ );
}
}
$bulkUserInfo[$contactpersoonId] = $userInfo;
-
} catch (\Exception $e) {
- $this->logger->error('ContactpersoonService: Failed to get user info for contactpersoon in bulk operation', [
- 'contactpersoonId' => $contactpersoonId,
- 'error' => $e->getMessage()
- ]);
-
- // Add error entry for this contactpersoon
+ $this->logger->error(
+ 'ContactpersoonService: Failed to get user info for contactpersoon in bulk operation',
+ [
+ 'contactpersoonId' => $contactpersoonId,
+ 'error' => $e->getMessage(),
+ ]
+ );
+
+ // Add error entry for this contactpersoon.
$bulkUserInfo[$contactpersoonId] = [
- 'hasUser' => false,
+ 'hasUser' => false,
'username' => null,
- 'groups' => [],
- 'error' => $e->getMessage()
+ 'groups' => [],
+ 'error' => $e->getMessage(),
];
- }
- }
-
- $this->logger->info('ContactpersoonService: Successfully retrieved bulk user info', [
- 'totalContactpersonen' => count($contactpersoonIds),
- 'successfulRetrievals' => count(array_filter($bulkUserInfo, function($info) {
- return !isset($info['error']);
- }))
- ]);
+ }//end try
+ }//end foreach
+
+ $this->logger->info(
+ 'ContactpersoonService: Successfully retrieved bulk user info',
+ [
+ 'totalContactpersonen' => count($contactpersoonIds),
+ 'successfulRetrievals' => count(
+ array_filter(
+ $bulkUserInfo,
+ function ($info) {
+ return isset($info['error']) === false;
+ }
+ )
+ ),
+ ]
+ );
return $bulkUserInfo;
-
} catch (\Exception $e) {
- $this->logger->error('ContactpersoonService: Failed to get bulk user info', [
- 'contactpersoonIds' => $contactpersoonIds,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'ContactpersoonService: Failed to get bulk user info',
+ [
+ 'contactpersoonIds' => $contactpersoonIds,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
throw $e;
- }
- }
+ }//end try
+
+ }//end getBulkUserInfo()
/**
- * Updates the contactpersoon object's @self metadata to set owner to the user UID
+ * Updates the contactpersoon object's @self metadata to set owner to the user UID.
+ *
+ * @param object $contactObject The contactpersoon object to update.
+ * @param string $userUID The user UID to set as owner.
*
- * @param object $contactObject The contactpersoon object to update
- * @param string $userUID The user UID to set as owner
* @return void
*/
private function updateContactpersoonObjectOwner(object $contactObject, string $userUID): void
{
try {
$contactId = $contactObject->getUuid();
-
- // Get configuration for register and schema
+
+ // Get configuration for register and schema.
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $register = $voorzieningenConfig['register'] ?? '';
- $contactSchema = $voorzieningenConfig['contactpersoon_schema'] ?? '';
-
- if (empty($register) || empty($contactSchema)) {
- $this->logger->warning('ContactpersoonService: Cannot update object owner - missing configuration', [
- 'contactId' => $contactId,
- 'register' => $register,
- 'contactSchema' => $contactSchema
- ]);
+ $register = ($voorzieningenConfig['register'] ?? '');
+ $contactSchema = ($voorzieningenConfig['contactpersoon_schema'] ?? '');
+
+ if (empty($register) === true || empty($contactSchema) === true) {
+ $this->logger->warning(
+ 'ContactpersoonService: Cannot update object owner - missing configuration',
+ [
+ 'contactId' => $contactId,
+ 'register' => $register,
+ 'contactSchema' => $contactSchema,
+ ]
+ );
return;
}
-
- $this->logger->info('ContactpersoonService: Updating contactpersoon object owner', [
- 'contactId' => $contactId,
- 'userUID' => $userUID,
- 'register' => $register,
- 'schema' => $contactSchema
- ]);
-
- // Get the current object data and normalize types
+
+ $this->logger->info(
+ 'ContactpersoonService: Updating contactpersoon object owner',
+ [
+ 'contactId' => $contactId,
+ 'userUID' => $userUID,
+ 'register' => $register,
+ 'schema' => $contactSchema,
+ ]
+ );
+
+ // Get the current object data and normalize types.
$currentObject = $contactObject->getObject();
- $currentObject = $this->normalizeContactDataTypes($currentObject);
+ $currentObject = $this->normalizeContactDataTypes(data: $currentObject);
- // Get current @self metadata or create new
- $selfMetadata = $currentObject['@self'] ?? [];
+ // Get current @self metadata or create new.
+ $selfMetadata = ($currentObject['@self'] ?? []);
- // Update the owner field to the user UID
+ // Update the owner field to the user UID.
$selfMetadata['owner'] = $userUID;
- // Set the organisation field in @self metadata to the organization UUID
- // This ensures the contact person is properly linked to their organization
- $organizationUuid = $currentObject['organisation'] ?? $currentObject['organisatie'] ?? '';
- if (!empty($organizationUuid)) {
+ // Set the organisation field in @self metadata to the organization UUID.
+ // This ensures the contact person is properly linked to their organization.
+ $organizationUuid = ($currentObject['organisation'] ?? $currentObject['organisatie'] ?? '');
+ if (empty($organizationUuid) === false) {
$selfMetadata['organisation'] = $organizationUuid;
- $this->logger->info('ContactpersoonService: Setting @self.organisation metadata', [
- 'contactId' => $contactId,
- 'organizationUuid' => $organizationUuid
- ]);
+ $this->logger->info(
+ 'ContactpersoonService: Setting @self.organisation metadata',
+ [
+ 'contactId' => $contactId,
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
} else {
- $this->logger->warning('ContactpersoonService: No organization UUID found for contact person', [
- 'contactId' => $contactId,
- 'contactData' => $currentObject
- ]);
+ $this->logger->warning(
+ 'ContactpersoonService: No organization UUID found for contact person',
+ [
+ 'contactId' => $contactId,
+ 'contactData' => $currentObject,
+ ]
+ );
}
- // Update the object with the new @self metadata
+ // Update the object with the new @self metadata.
$currentObject['@self'] = $selfMetadata;
$contactObject->setObject($currentObject);
-
- // Save the updated object using ObjectService
- $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
- $objectService->saveObject(
- object: $contactObject,
- register: $register,
- schema: $contactSchema,
- _rbac: false,
- _multitenancy: false
+
+ // Also update the entity's owner and organisation fields directly.
+ // These system fields control multi-tenancy filtering.
+ $contactObject->setOwner($userUID);
+ if (empty($organizationUuid) === false) {
+ $contactObject->setOrganisation($organizationUuid);
+ }
+
+ // FIX #434: Use ObjectEntityMapper directly instead of ObjectService::saveObject().
+ // To avoid validation errors on the organisatie field (stored as UUID string but.
+ // Schema expects object type) and to avoid triggering ObjectUpdatedEvent cascades.
+ $objectMapper = \OC::$server->get('OCA\OpenRegister\Db\ObjectEntityMapper');
+ $objectMapper->update($contactObject);
+
+ $this->logger->info(
+ 'ContactpersoonService: Successfully updated contactpersoon object owner and organisation',
+ [
+ 'contactId' => $contactId,
+ 'userUID' => $userUID,
+ 'ownerSet' => $selfMetadata['owner'],
+ 'organisationSet' => ($selfMetadata['organisation'] ?? 'not set'),
+ ]
);
-
- $this->logger->info('ContactpersoonService: Successfully updated contactpersoon object owner and organisation', [
- 'contactId' => $contactId,
- 'userUID' => $userUID,
- 'ownerSet' => $selfMetadata['owner'],
- 'organisationSet' => $selfMetadata['organisation'] ?? 'not set'
- ]);
-
} catch (\Exception $e) {
- $this->logger->error('ContactpersoonService: Failed to update contactpersoon object owner', [
- 'contactId' => $contactObject->getUuid(),
- 'userUID' => $userUID,
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine()
- ]);
- }
- }
+ $this->logger->error(
+ 'ContactpersoonService: Failed to update contactpersoon object owner',
+ [
+ 'contactId' => $contactObject->getUuid(),
+ 'userUID' => $userUID,
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]
+ );
+ }//end try
+
+ }//end updateContactpersoonObjectOwner()
/**
- * Enable user account for a contactpersoon
- * @param string $contactpersoonId
- * @throws \Exception
+ * Enable user account for a contactpersoon.
+ *
+ * @param string $contactpersoonId The UUID of the contactpersoon.
+ *
+ * @return void
+ *
+ * @throws \Exception If enabling fails.
*/
public function enableUserForContactpersoon(string $contactpersoonId): void
{
try {
- $this->logger->info('ContactpersoonService: Enabling user for contactpersoon', [
- 'contactpersoonId' => $contactpersoonId
- ]);
+ $this->logger->info(
+ 'ContactpersoonService: Enabling user for contactpersoon',
+ ['contactpersoonId' => $contactpersoonId]
+ );
$objectService = $this->getObjectService();
- if (!$objectService) {
+ if ($objectService === null) {
throw new \Exception('ObjectService not available');
}
@@ -945,56 +1251,67 @@ public function enableUserForContactpersoon(string $contactpersoonId): void
_rbac: false,
_multitenancy: false
);
- if (!$contactObject) {
+ if ($contactObject === null) {
throw new \Exception('Contactpersoon not found');
}
$contactData = $contactObject->getObject();
- $username = $contactData['username'] ?? null;
+ $username = ($contactData['username'] ?? null);
- if (!$username) {
+ if (empty($username) === true) {
throw new \Exception('No username found for contactpersoon');
}
$userManager = \OC::$server->get('OCP\IUserManager');
- $user = $userManager->get($username);
+ $user = $userManager->get($username);
- if (!$user) {
+ if ($user === null) {
throw new \Exception('User not found in Nextcloud');
}
- // Enable the user
+ // Enable the user.
$user->setEnabled(true);
- $this->logger->info('ContactpersoonService: User enabled successfully', [
- 'contactpersoonId' => $contactpersoonId,
- 'username' => $username
- ]);
-
+ $this->logger->info(
+ 'ContactpersoonService: User enabled successfully',
+ [
+ 'contactpersoonId' => $contactpersoonId,
+ 'username' => $username,
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('ContactpersoonService: Failed to enable user for contactpersoon', [
- 'contactpersoonId' => $contactpersoonId,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'ContactpersoonService: Failed to enable user for contactpersoon',
+ [
+ 'contactpersoonId' => $contactpersoonId,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
throw $e;
- }
- }
+ }//end try
+
+ }//end enableUserForContactpersoon()
/**
- * Disable user account for a contactpersoon
- * @param string $contactpersoonId
- * @throws \Exception
+ * Disable user account for a contactpersoon.
+ *
+ * @param string $contactpersoonId The UUID of the contactpersoon.
+ *
+ * @return void
+ *
+ * @throws \Exception If disabling fails.
*/
public function disableUserForContactpersoon(string $contactpersoonId): void
{
try {
- $this->logger->info('ContactpersoonService: Disabling user for contactpersoon', [
- 'contactpersoonId' => $contactpersoonId
- ]);
+ $this->logger->info(
+ 'ContactpersoonService: Disabling user for contactpersoon',
+ ['contactpersoonId' => $contactpersoonId]
+ );
$objectService = $this->getObjectService();
- if (!$objectService) {
+ if ($objectService === null) {
throw new \Exception('ObjectService not available');
}
@@ -1005,39 +1322,45 @@ public function disableUserForContactpersoon(string $contactpersoonId): void
_rbac: false,
_multitenancy: false
);
- if (!$contactObject) {
+ if ($contactObject === null) {
throw new \Exception('Contactpersoon not found');
}
$contactData = $contactObject->getObject();
- $username = $contactData['username'] ?? null;
+ $username = ($contactData['username'] ?? null);
- if (!$username) {
+ if (empty($username) === true) {
throw new \Exception('No username found for contactpersoon');
}
$userManager = \OC::$server->get('OCP\IUserManager');
- $user = $userManager->get($username);
+ $user = $userManager->get($username);
- if (!$user) {
+ if ($user === null) {
throw new \Exception('User not found in Nextcloud');
}
- // Disable the user
+ // Disable the user.
$user->setEnabled(false);
- $this->logger->info('ContactpersoonService: User disabled successfully', [
- 'contactpersoonId' => $contactpersoonId,
- 'username' => $username
- ]);
-
+ $this->logger->info(
+ 'ContactpersoonService: User disabled successfully',
+ [
+ 'contactpersoonId' => $contactpersoonId,
+ 'username' => $username,
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('ContactpersoonService: Failed to disable user for contactpersoon', [
- 'contactpersoonId' => $contactpersoonId,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'ContactpersoonService: Failed to disable user for contactpersoon',
+ [
+ 'contactpersoonId' => $contactpersoonId,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
throw $e;
- }
- }
-}
\ No newline at end of file
+ }//end try
+
+ }//end disableUserForContactpersoon()
+}//end class
diff --git a/lib/Service/GebruikService.php b/lib/Service/GebruikService.php
index 92ab31b0..c152dffa 100644
--- a/lib/Service/GebruikService.php
+++ b/lib/Service/GebruikService.php
@@ -1,4 +1,17 @@
+ * @copyright 2024 Conduction B.V.
+ * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
+ */
namespace OCA\SoftwareCatalog\Service;
@@ -11,97 +24,113 @@
class GebruikService
{
/**
- * @param SettingsService $settingsService
- * @param IAppManager $appManager
- * @param ContainerInterface $container
- * @param LoggerInterface $logger
+ * Constructor for GebruikService.
+ *
+ * @param SettingsService $settingsService The settings service instance
+ * @param IAppManager $appManager The application manager
+ * @param ContainerInterface $container The DI container
+ * @param LoggerInterface $logger The logger instance
*/
public function __construct(
private readonly SettingsService $settingsService,
private readonly IAppManager $appManager,
private readonly ContainerInterface $container,
private readonly LoggerInterface $logger,
- ) {}
+ ) {
+ }//end __construct()
/**
* Fetch relevant configuration for this service.
*
* @return array The resulting configuration parameters.
- * @throws Exception
+ *
+ * @throws Exception When configuration cannot be retrieved.
*/
private function getGebruiksConfiguration(): array
{
- // Try to get voorzieningen configuration from SettingsService
+ // Try to get voorzieningen configuration from SettingsService.
try {
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $this->logger->debug('Retrieved voorzieningen configuration', [
- 'config' => $voorzieningenConfig
- ]);
+ $this->logger->debug(
+ 'Retrieved voorzieningen configuration',
+ [
+ 'config' => $voorzieningenConfig,
+ ]
+ );
- $registerId = $voorzieningenConfig['register'] ?? null;
- $gebruikSchema = $voorzieningenConfig['gebruik_schema'] ?? null;
+ $registerId = $voorzieningenConfig['register'] ?? null;
+ $gebruikSchema = $voorzieningenConfig['gebruik_schema'] ?? null;
$applicatieSchema = $voorzieningenConfig['module_schema'] ?? null;
- // If configuration is available, use it
- if ($registerId && $gebruikSchema) {
+ // If configuration is available, use it.
+ if (empty($registerId) === false && empty($gebruikSchema) === false) {
return [
- 'registerId' => $registerId ?? 'null',
- 'gebruikSchema' => $gebruikSchema ?? 'null',
+ 'registerId' => $registerId ?? 'null',
+ 'gebruikSchema' => $gebruikSchema ?? 'null',
'applicatieSchema' => $applicatieSchema ?? 'null',
];
}
} catch (Exception $e) {
- $this->logger->warning('Failed to get voorzieningen configuration from SettingsService', [
- 'error' => $e->getMessage()
- ]);
- }
-
- // No hardcoded fallback - configuration must be properly set
- $this->logger->error('Failed to get voorzieningen configuration - no fallback provided', [
- 'registerId' => $registerId ?? 'null',
- 'gebruikSchema' => $gebruikSchema ?? 'null',
- 'voorzieningenConfig' => $voorzieningenConfig ?? 'null',
- 'applicatieSchema' => $applicatieSchema ?? 'null',
- ]);
+ $this->logger->warning(
+ 'Failed to get voorzieningen configuration from SettingsService',
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+
+ // No hardcoded fallback - configuration must be properly set.
+ $this->logger->error(
+ 'Failed to get voorzieningen configuration - no fallback provided',
+ [
+ 'registerId' => $registerId ?? 'null',
+ 'gebruikSchema' => $gebruikSchema ?? 'null',
+ 'voorzieningenConfig' => $voorzieningenConfig ?? 'null',
+ 'applicatieSchema' => $applicatieSchema ?? 'null',
+ ]
+ );
throw new Exception('Voorzieningen configuration not found. Please configure the schemas in the admin panel.');
- }
+ }//end getGebruiksConfiguration()
/**
- * Get ObjectService from OpenRegister app
+ * Get ObjectService from OpenRegister app.
+ *
+ * @return ObjectService The OpenRegister object service.
*
- * @return ObjectService The OpenRegister object service
- * @throws Exception When OpenRegister service is not available
+ * @throws Exception When OpenRegister service is not available.
*/
private function getObjectService(): ObjectService
{
- if (!in_array('openregister', $this->appManager->getInstalledApps())) {
+ if (in_array('openregister', $this->appManager->getInstalledApps()) === false) {
throw new Exception('OpenRegister app is not installed');
}
try {
return $this->container->get('OCA\OpenRegister\Service\ObjectService');
} catch (Exception $e) {
- throw new Exception('Failed to get OpenRegister service: ' . $e->getMessage());
+ throw new Exception('Failed to get OpenRegister service: '.$e->getMessage());
}
- }
+ }//end getObjectService()
/**
* Fetch gebruiken for given options.
*
* @param array $options The options to use while searching.
+ *
* @return array The result set of gebruiken.
- * @throws \OCP\DB\Exception
+ *
+ * @throws \OCP\DB\Exception When database query fails.
*/
public function getGebruiken(array $options): array
{
- $objectService = $this->getObjectService();
+ $objectService = $this->getObjectService();
$gebruiksConfig = $this->getGebruiksConfiguration();
$options['@self'] = [
'register' => $gebruiksConfig['registerId'],
- 'schema' => $gebruiksConfig['gebruikSchema'],
+ 'schema' => $gebruiksConfig['gebruikSchema'],
];
// Normalize _extend parameter to array format.
@@ -112,58 +141,68 @@ public function getGebruiken(array $options): array
} else if (is_array($extend) === false) {
$extend = [$extend];
}
+
$options['_extend'] = $extend;
unset($options['extend']);
$searchResult = $objectService->searchObjectsPaginated(query: $options, _rbac: false, _multitenancy: false);
- $searchResult['results'] = array_map(function($object) {
- if (is_array($object) === false) {
- $object = $object->getObject();
- }
+ $searchResult['results'] = array_map(
+ function ($object) {
+ if (is_array($object) === false) {
+ $object = $object->getObject();
+ }
- unset($object['interneAantekening']);
+ unset($object['interneAantekening']);
- return $object;
- }, $searchResult['results']);
+ return $object;
+ },
+ $searchResult['results']
+ );
return $searchResult;
- }
+ }//end getGebruiken()
/**
- * Get application ids for given options
+ * Get application ids for given options.
*
* @param array $options The options to use while searching.
- * @return array The resulting ids
- * @throws \OCP\DB\Exception
+ *
+ * @return array The resulting ids.
+ *
+ * @throws \OCP\DB\Exception When database query fails.
*/
public function getApplicationIds(array $options): array
{
- $objectService = $this->getObjectService();
+ $objectService = $this->getObjectService();
$gebruiksConfig = $this->getGebruiksConfiguration();
$options['@self'] = [
'register' => $gebruiksConfig['registerId'],
- 'schema' => $gebruiksConfig['applicatieSchema'],
+ 'schema' => $gebruiksConfig['applicatieSchema'],
];
$searchResult = $objectService->searchObjectsPaginated(query: $options, _rbac: false, _multitenancy: false);
- $searchResult = array_map(function($object) {
- // Handle both ObjectEntity and array results.
- if (is_array($object) === false) {
- // Use jsonSerialize to get full object with @self metadata.
- if (method_exists($object, 'jsonSerialize') === true) {
- $object = $object->jsonSerialize();
- } else if (method_exists($object, 'getId') === true) {
- return $object->getId();
- } else {
- $object = $object->getObject();
- }
- }
- return $object['@self']['id'] ?? $object['id'] ?? null;
- }, $searchResult['results']);
+ $searchResult = array_map(
+ function ($object) {
+ // Handle both ObjectEntity and array results.
+ if (is_array($object) === false) {
+ // Use jsonSerialize to get full object with @self metadata.
+ if (method_exists($object, 'jsonSerialize') === true) {
+ $object = $object->jsonSerialize();
+ } else if (method_exists($object, 'getId') === true) {
+ return $object->getId();
+ } else {
+ $object = $object->getObject();
+ }
+ }
+
+ return $object['@self']['id'] ?? $object['id'] ?? null;
+ },
+ $searchResult['results']
+ );
return $searchResult;
- }
-}
+ }//end getApplicationIds()
+}//end class
diff --git a/lib/Service/GebruikSyncService.php b/lib/Service/GebruikSyncService.php
index 238d8d6a..77edfc54 100644
--- a/lib/Service/GebruikSyncService.php
+++ b/lib/Service/GebruikSyncService.php
@@ -1,4 +1,21 @@
+ * @copyright 2024 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl
+ * @version GIT:
+ * @link https://github.com/conduction/nextcloud-software-catalog
+ */
declare(strict_types=1);
@@ -11,228 +28,274 @@
use DateTime;
/**
- * Service for synchronizing and processing Gebruik (Usage) objects
+ * Service for synchronizing and processing Gebruik (Usage) objects.
*
- * This service handles the processing of gebruik schema objects, including:
- * - Processing gebruiktVoorReferentiecomponenten to populate amefElements
- * - Auto-updating status based on date fields
- *
* @category Service
* @package OCA\SoftwareCatalog\Service
* @author Ruben van der Linde
* @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/conduction/nextcloud-software-catalog
*/
class GebruikSyncService
{
+
+ /**
+ * Logger for debugging and error reporting.
+ *
+ * @var LoggerInterface
+ */
private LoggerInterface $logger;
+
+ /**
+ * Service for retrieving configuration settings.
+ *
+ * @var SettingsService
+ */
private SettingsService $settingsService;
/**
- * Constructor for GebruikSyncService
+ * Constructor for GebruikSyncService.
*
- * @param LoggerInterface $logger Logger for debugging and error reporting
+ * @param LoggerInterface $logger Logger for debugging and error reporting
* @param SettingsService $settingsService Service for retrieving configuration settings
*/
public function __construct(
LoggerInterface $logger,
SettingsService $settingsService
) {
- $this->logger = $logger;
+ $this->logger = $logger;
$this->settingsService = $settingsService;
- }
+ }//end __construct()
/**
- * Process a specific gebruik object
- *
- * This method handles both AMEF elements processing and status auto-update
+ * Process a specific gebruik object.
+ *
+ * This method handles both AMEF elements processing and status auto-update.
*
* @param ObjectEntity $gebruikObject The gebruik object to process
- * @return array Processing statistics
+ *
+ * @return array Processing statistics.
*/
public function processSpecificGebruik(ObjectEntity $gebruikObject): array
{
$startTime = microtime(true);
- $stats = [
- 'startTime' => date('Y-m-d H:i:s'),
- 'gebruikId' => $gebruikObject->getUuid(),
+ $stats = [
+ 'startTime' => date('Y-m-d H:i:s'),
+ 'gebruikId' => $gebruikObject->getUuid(),
'amefElementsProcessed' => 0,
- 'statusUpdated' => false,
- 'errors' => [],
- 'duration' => 0
+ 'statusUpdated' => false,
+ 'errors' => [],
+ 'duration' => 0,
];
try {
$gebruikData = $gebruikObject->getObject();
$gebruikUuid = $gebruikObject->getUuid();
- $this->logger->debug('Processing gebruik object', [
- 'app' => 'softwarecatalog',
- 'gebruikId' => $gebruikUuid,
- 'currentStatus' => $gebruikData['status'] ?? 'Unknown',
- ]);
-
- // Step 1: Process gebruiktVoorReferentiecomponenten for AMEF elements
- $amefStats = $this->processAmefElements($gebruikObject);
+ $this->logger->debug(
+ 'Processing gebruik object',
+ [
+ 'app' => 'softwarecatalog',
+ 'gebruikId' => $gebruikUuid,
+ 'currentStatus' => $gebruikData['status'] ?? 'Unknown',
+ ]
+ );
+
+ // Step 1: Process gebruiktVoorReferentiecomponenten for AMEF elements.
+ $amefStats = $this->processAmefElements(gebruikObject: $gebruikObject);
$stats['amefElementsProcessed'] = $amefStats['amefElementsProcessed'];
$stats['errors'] = array_merge($stats['errors'], $amefStats['errors']);
- // Step 2: Auto-update status based on dates
- $statusStats = $this->updateStatusBasedOnDates($gebruikObject);
+ // Step 2: Auto-update status based on dates.
+ $statusStats = $this->updateStatusBasedOnDates(gebruikObject: $gebruikObject);
$stats['statusUpdated'] = $statusStats['statusUpdated'];
- $stats['errors'] = array_merge($stats['errors'], $statusStats['errors']);
+ $stats['errors'] = array_merge($stats['errors'], $statusStats['errors']);
- $stats['endTime'] = date('Y-m-d H:i:s');
+ $stats['endTime'] = date('Y-m-d H:i:s');
$stats['duration'] = round(microtime(true) - $startTime, 3);
- $this->logger->critical('✅ GEBRUIK PROCESSING COMPLETED', [
- 'app' => 'softwarecatalog',
- 'gebruikId' => $gebruikUuid,
- 'stats' => $stats,
- 'processingTime' => $stats['duration'] . 's'
- ]);
+ $this->logger->critical(
+ 'GEBRUIK PROCESSING COMPLETED',
+ [
+ 'app' => 'softwarecatalog',
+ 'gebruikId' => $gebruikUuid,
+ 'stats' => $stats,
+ 'processingTime' => $stats['duration'].'s',
+ ]
+ );
return $stats;
-
} catch (Exception $e) {
$stats['errors'][] = $e->getMessage();
$stats['duration'] = round(microtime(true) - $startTime, 3);
- $this->logger->error('💥 GEBRUIK PROCESSING ERROR', [
- 'app' => 'softwarecatalog',
- 'gebruikId' => $gebruikObject->getUuid(),
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'GEBRUIK PROCESSING ERROR',
+ [
+ 'app' => 'softwarecatalog',
+ 'gebruikId' => $gebruikObject->getUuid(),
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return $stats;
- }
- }
+ }//end try
+ }//end processSpecificGebruik()
/**
- * Process AMEF elements from gebruiktVoorReferentiecomponenten
- *
+ * Process AMEF elements from gebruiktVoorReferentiecomponenten.
+ *
* Searches for AMEF elements based on IDs in gebruiktVoorReferentiecomponenten
- * and adds their slugs to the amefElements array
+ * and adds their slugs to the amefElements array.
*
* @param ObjectEntity $gebruikObject The gebruik object to process
- * @return array Processing statistics
+ *
+ * @return array Processing statistics.
*/
private function processAmefElements(ObjectEntity $gebruikObject): array
{
$stats = [
'amefElementsProcessed' => 0,
- 'errors' => []
+ 'errors' => [],
];
try {
$gebruikData = $gebruikObject->getObject();
$gebruikUuid = $gebruikObject->getUuid();
- // Get the referentiecomponenten IDs
+ // Get the referentiecomponenten IDs.
$referentieComponenten = $gebruikData['gebruiktVoorReferentiecomponenten'] ?? [];
- if (empty($referentieComponenten)) {
- $this->logger->info('🔍 No referentiecomponenten found for gebruik object', [
- 'gebruikId' => $gebruikUuid
- ]);
+ if (empty($referentieComponenten) === true) {
+ $this->logger->info(
+ 'No referentiecomponenten found for gebruik object',
+ [
+ 'gebruikId' => $gebruikUuid,
+ ]
+ );
return $stats;
}
- $this->logger->debug('Processing referentiecomponenten', [
- 'app' => 'softwarecatalog',
- 'gebruikId' => $gebruikUuid,
- 'referentieComponentenCount' => count($referentieComponenten)
- ]);
+ $this->logger->debug(
+ 'Processing referentiecomponenten',
+ [
+ 'app' => 'softwarecatalog',
+ 'gebruikId' => $gebruikUuid,
+ 'referentieComponentenCount' => count($referentieComponenten),
+ ]
+ );
- // Extract IDs from referentiecomponenten
+ // Extract IDs from referentiecomponenten.
$referentieIds = [];
foreach ($referentieComponenten as $component) {
- if (isset($component['id'])) {
+ if (isset($component['id']) === true) {
$referentieIds[] = $component['id'];
}
}
- if (empty($referentieIds)) {
- $this->logger->warning('⚠️ No valid IDs found in referentiecomponenten', [
- 'gebruikId' => $gebruikUuid
- ]);
+ if (empty($referentieIds) === true) {
+ $this->logger->warning(
+ 'No valid IDs found in referentiecomponenten',
+ [
+ 'gebruikId' => $gebruikUuid,
+ ]
+ );
return $stats;
}
- // Get AMEF register configuration
+ // Get AMEF register configuration.
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $amefRegister = $voorzieningenConfig['amef_register'] ?? '';
- $elementSchema = $voorzieningenConfig['element_schema'] ?? '';
+ $amefRegister = $voorzieningenConfig['amef_register'] ?? '';
+ $elementSchema = $voorzieningenConfig['element_schema'] ?? '';
- if (empty($amefRegister) || empty($elementSchema)) {
+ if (empty($amefRegister) === true || empty($elementSchema) === true) {
$stats['errors'][] = 'AMEF register or element schema not configured';
- $this->logger->error('❌ AMEF configuration missing', [
- 'app' => 'softwarecatalog',
- 'amefRegister' => $amefRegister,
- 'elementSchema' => $elementSchema
- ]);
+ $this->logger->error(
+ 'AMEF configuration missing',
+ [
+ 'app' => 'softwarecatalog',
+ 'amefRegister' => $amefRegister,
+ 'elementSchema' => $elementSchema,
+ ]
+ );
return $stats;
}
- // Search for AMEF elements
- $amefElements = $this->searchAmefElementsByIds($referentieIds, $amefRegister, $elementSchema);
-
- // Extract slugs from found AMEF elements
+ // Search for AMEF elements.
+ $amefElements = $this->searchAmefElementsByIds(
+ ids: $referentieIds,
+ register: $amefRegister,
+ schema: $elementSchema
+ );
+
+ // Extract slugs from found AMEF elements.
$amefSlugs = [];
foreach ($amefElements as $amefElement) {
$amefData = $amefElement->getObject();
- if (isset($amefData['slug'])) {
+ if (isset($amefData['slug']) === true) {
$amefSlugs[] = $amefData['slug'];
$stats['amefElementsProcessed']++;
}
}
- // Update the gebruik object with AMEF slugs
- if (!empty($amefSlugs)) {
+ // Update the gebruik object with AMEF slugs.
+ if (empty($amefSlugs) === false) {
$gebruikData['amefElements'] = array_unique($amefSlugs);
- $this->updateGebruikObject($gebruikObject, $gebruikData);
-
- $this->logger->critical('✅ AMEF ELEMENTS UPDATED', [
- 'app' => 'softwarecatalog',
- 'gebruikId' => $gebruikUuid,
- 'amefSlugs' => $amefSlugs,
- 'amefElementsCount' => count($amefSlugs)
- ]);
+ $this->updateGebruikObject(
+ gebruikObject: $gebruikObject,
+ updatedData: $gebruikData
+ );
+
+ $this->logger->critical(
+ 'AMEF ELEMENTS UPDATED',
+ [
+ 'app' => 'softwarecatalog',
+ 'gebruikId' => $gebruikUuid,
+ 'amefSlugs' => $amefSlugs,
+ 'amefElementsCount' => count($amefSlugs),
+ ]
+ );
} else {
- $this->logger->info('ℹ️ No AMEF elements with slugs found', [
- 'gebruikId' => $gebruikUuid
- ]);
- }
+ $this->logger->info(
+ 'No AMEF elements with slugs found',
+ [
+ 'gebruikId' => $gebruikUuid,
+ ]
+ );
+ }//end if
return $stats;
-
} catch (Exception $e) {
- $stats['errors'][] = 'AMEF processing error: ' . $e->getMessage();
- $this->logger->error('💥 AMEF PROCESSING ERROR', [
- 'app' => 'softwarecatalog',
- 'gebruikId' => $gebruikObject->getUuid(),
- 'exception' => $e->getMessage()
- ]);
+ $stats['errors'][] = 'AMEF processing error: '.$e->getMessage();
+ $this->logger->error(
+ 'AMEF PROCESSING ERROR',
+ [
+ 'app' => 'softwarecatalog',
+ 'gebruikId' => $gebruikObject->getUuid(),
+ 'exception' => $e->getMessage(),
+ ]
+ );
return $stats;
- }
- }
+ }//end try
+ }//end processAmefElements()
/**
- * Search for AMEF elements by IDs
- *
+ * Search for AMEF elements by IDs.
+ *
* Uses searchObjects to find AMEF elements based on provided IDs.
* Since searchObjects may not support direct IDs array parameter,
* this method implements multiple individual searches.
*
- * @param array $ids Array of IDs to search for
+ * @param array $ids Array of IDs to search for
* @param string $register AMEF register ID
- * @param string $schema Element schema ID
- * @return array Array of found ObjectEntity objects
+ * @param string $schema Element schema ID
+ *
+ * @return array Array of found ObjectEntity objects.
*/
private function searchAmefElementsByIds(array $ids, string $register, string $schema): array
{
@@ -241,161 +304,192 @@ private function searchAmefElementsByIds(array $ids, string $register, string $s
foreach ($ids as $id) {
try {
- // Try to search by ID
+ // Try to search by ID.
$query = [
'@self' => [
'register' => (int) $register,
- 'schema' => (int) $schema
+ 'schema' => (int) $schema,
],
- 'id' => $id
+ 'id' => $id,
];
- $elements = $objectService->searchObjects($query);
+ $elements = $objectService->searchObjects($query);
$foundElements = array_merge($foundElements, $elements);
-
} catch (Exception $e) {
- $this->logger->warning('⚠️ Failed to search for AMEF element', [
- 'app' => 'softwarecatalog',
- 'id' => $id,
- 'error' => $e->getMessage()
- ]);
- }
- }
-
- $this->logger->info('🔍 AMEF elements search completed', [
- 'app' => 'softwarecatalog',
- 'searchedIds' => $ids,
- 'foundElementsCount' => count($foundElements)
- ]);
+ $this->logger->warning(
+ 'Failed to search for AMEF element',
+ [
+ 'app' => 'softwarecatalog',
+ 'id' => $id,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end foreach
+
+ $this->logger->info(
+ 'AMEF elements search completed',
+ [
+ 'app' => 'softwarecatalog',
+ 'searchedIds' => $ids,
+ 'foundElementsCount' => count($foundElements),
+ ]
+ );
return $foundElements;
- }
+ }//end searchAmefElementsByIds()
/**
- * Update status based on date fields
- *
+ * Update status based on date fields.
+ *
* Looks at all status date fields and updates the status to the one
- * with the highest date that is not in the future
+ * with the highest date that is not in the future.
*
* @param ObjectEntity $gebruikObject The gebruik object to process
- * @return array Processing statistics
+ *
+ * @return array Processing statistics.
*/
private function updateStatusBasedOnDates(ObjectEntity $gebruikObject): array
{
$stats = [
'statusUpdated' => false,
- 'errors' => []
+ 'errors' => [],
];
try {
- $gebruikData = $gebruikObject->getObject();
- $gebruikUuid = $gebruikObject->getUuid();
+ $gebruikData = $gebruikObject->getObject();
+ $gebruikUuid = $gebruikObject->getUuid();
$currentStatus = $gebruikData['status'] ?? '';
- // Define status dates mapping
+ // Define status dates mapping.
$statusDates = [
- 'Verwerving' => $gebruikData['startDatumVerwerving'] ?? null,
- 'Gepland' => $gebruikData['startDatumGepland'] ?? null,
- 'In productie' => $gebruikData['startDatumInProductie'] ?? null,
+ 'Verwerving' => $gebruikData['startDatumVerwerving'] ?? null,
+ 'Gepland' => $gebruikData['startDatumGepland'] ?? null,
+ 'In productie' => $gebruikData['startDatumInProductie'] ?? null,
'Uit te faseren' => $gebruikData['startDatumUitTeFaseren'] ?? null,
- 'Uitgefaseerd' => $gebruikData['startDatumUitGefaseerd'] ?? null
+ 'Uitgefaseerd' => $gebruikData['startDatumUitGefaseerd'] ?? null,
];
- $this->logger->info('🔍 CHECKING STATUS DATES', [
- 'app' => 'softwarecatalog',
- 'gebruikId' => $gebruikUuid,
- 'currentStatus' => $currentStatus,
- 'statusDates' => $statusDates
- ]);
-
- // Find the highest date that is not in the future
- $now = new DateTime();
+ $this->logger->info(
+ 'CHECKING STATUS DATES',
+ [
+ 'app' => 'softwarecatalog',
+ 'gebruikId' => $gebruikUuid,
+ 'currentStatus' => $currentStatus,
+ 'statusDates' => $statusDates,
+ ]
+ );
+
+ // Find the highest date that is not in the future.
+ $now = new DateTime();
$targetStatus = null;
- $targetDate = null;
+ $targetDate = null;
foreach ($statusDates as $status => $dateString) {
- if (!empty($dateString)) {
+ if (empty($dateString) === false) {
try {
$date = new DateTime($dateString);
-
- // Only consider dates that are not in the future
+
+ // Only consider dates that are not in the future.
if ($date <= $now) {
if ($targetDate === null || $date > $targetDate) {
- $targetDate = $date;
+ $targetDate = $date;
$targetStatus = $status;
}
}
} catch (Exception $e) {
- $this->logger->warning('⚠️ Invalid date format', [
- 'app' => 'softwarecatalog',
- 'gebruikId' => $gebruikUuid,
- 'status' => $status,
- 'dateString' => $dateString,
- 'error' => $e->getMessage()
- ]);
- }
- }
- }
-
- // Update status if we found a different one
- if ($targetStatus && $targetStatus !== $currentStatus) {
+ $this->logger->warning(
+ 'Invalid date format',
+ [
+ 'app' => 'softwarecatalog',
+ 'gebruikId' => $gebruikUuid,
+ 'status' => $status,
+ 'dateString' => $dateString,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end if
+ }//end foreach
+
+ // Update status if we found a different one.
+ if ($targetStatus !== null && $targetStatus !== $currentStatus) {
$gebruikData['status'] = $targetStatus;
- $this->updateGebruikObject($gebruikObject, $gebruikData);
+ $this->updateGebruikObject(
+ gebruikObject: $gebruikObject,
+ updatedData: $gebruikData
+ );
$stats['statusUpdated'] = true;
- $this->logger->critical('✅ STATUS AUTO-UPDATED', [
- 'app' => 'softwarecatalog',
- 'gebruikId' => $gebruikUuid,
- 'oldStatus' => $currentStatus,
- 'newStatus' => $targetStatus,
- 'basedOnDate' => $targetDate ? $targetDate->format('Y-m-d') : null
- ]);
+ if ($targetDate !== null) {
+ $basedOnDate = $targetDate->format('Y-m-d');
+ } else {
+ $basedOnDate = null;
+ }
+
+ $this->logger->critical(
+ 'STATUS AUTO-UPDATED',
+ [
+ 'app' => 'softwarecatalog',
+ 'gebruikId' => $gebruikUuid,
+ 'oldStatus' => $currentStatus,
+ 'newStatus' => $targetStatus,
+ 'basedOnDate' => $basedOnDate,
+ ]
+ );
} else {
- $this->logger->info('ℹ️ No status update needed', [
- 'app' => 'softwarecatalog',
- 'gebruikId' => $gebruikUuid,
- 'currentStatus' => $currentStatus,
- 'targetStatus' => $targetStatus
- ]);
- }
+ $this->logger->info(
+ 'No status update needed',
+ [
+ 'app' => 'softwarecatalog',
+ 'gebruikId' => $gebruikUuid,
+ 'currentStatus' => $currentStatus,
+ 'targetStatus' => $targetStatus,
+ ]
+ );
+ }//end if
return $stats;
-
} catch (Exception $e) {
- $stats['errors'][] = 'Status update error: ' . $e->getMessage();
- $this->logger->error('💥 STATUS UPDATE ERROR', [
- 'app' => 'softwarecatalog',
- 'gebruikId' => $gebruikObject->getUuid(),
- 'exception' => $e->getMessage()
- ]);
+ $stats['errors'][] = 'Status update error: '.$e->getMessage();
+ $this->logger->error(
+ 'STATUS UPDATE ERROR',
+ [
+ 'app' => 'softwarecatalog',
+ 'gebruikId' => $gebruikObject->getUuid(),
+ 'exception' => $e->getMessage(),
+ ]
+ );
return $stats;
- }
- }
+ }//end try
+ }//end updateStatusBasedOnDates()
/**
- * Update a gebruik object in OpenRegister
+ * Update a gebruik object in OpenRegister.
*
* @param ObjectEntity $gebruikObject The object to update
- * @param array $updatedData The updated data
+ * @param array $updatedData The updated data
+ *
* @return void
- * @throws Exception If the update fails
+ *
+ * @throws Exception If the update fails.
*/
private function updateGebruikObject(ObjectEntity $gebruikObject, array $updatedData): void
{
try {
$objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
-
- // Get voorzieningenConfig to find the correct register and schema
+
+ // Get voorzieningenConfig to find the correct register and schema.
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $register = $voorzieningenConfig['register'] ?? '';
- $gebruikSchema = $voorzieningenConfig['gebruik_schema'] ?? '';
+ $register = $voorzieningenConfig['register'] ?? '';
+ $gebruikSchema = $voorzieningenConfig['gebruik_schema'] ?? '';
- if (empty($register) || empty($gebruikSchema)) {
+ if (empty($register) === true || empty($gebruikSchema) === true) {
throw new Exception('Register or gebruik schema not configured');
}
- // Update the object
+ // Update the object.
$objectService->saveObject(
object: $updatedData,
register: (int) $register,
@@ -403,18 +497,23 @@ private function updateGebruikObject(ObjectEntity $gebruikObject, array $updated
id: $gebruikObject->getUuid()
);
- $this->logger->info('✅ Gebruik object updated successfully', [
- 'app' => 'softwarecatalog',
- 'gebruikId' => $gebruikObject->getUuid()
- ]);
-
+ $this->logger->info(
+ 'Gebruik object updated successfully',
+ [
+ 'app' => 'softwarecatalog',
+ 'gebruikId' => $gebruikObject->getUuid(),
+ ]
+ );
} catch (Exception $e) {
- $this->logger->error('❌ Failed to update gebruik object', [
- 'app' => 'softwarecatalog',
- 'gebruikId' => $gebruikObject->getUuid(),
- 'error' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to update gebruik object',
+ [
+ 'app' => 'softwarecatalog',
+ 'gebruikId' => $gebruikObject->getUuid(),
+ 'error' => $e->getMessage(),
+ ]
+ );
throw $e;
- }
- }
-}
+ }//end try
+ }//end updateGebruikObject()
+}//end class
diff --git a/lib/Service/ModuleComplianceService.php b/lib/Service/ModuleComplianceService.php
index f7af7620..1bf97b9e 100644
--- a/lib/Service/ModuleComplianceService.php
+++ b/lib/Service/ModuleComplianceService.php
@@ -10,7 +10,7 @@
* @author Conduction b.v.
* @copyright 2024 Conduction B.V.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
@@ -25,15 +25,15 @@
/**
* Service for handling module compliance logic.
- *
+ *
* This service handles the automatic synchronization of module 'standaarden'
* property based on linked compliance objects and their standaardversie references.
- *
+ *
* @category Service
* @package OCA\SoftwareCatalog\Service
* @author Conduction b.v.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
class ModuleComplianceService
@@ -41,16 +41,16 @@ class ModuleComplianceService
/**
* Constructor for ModuleComplianceService
*
- * @param ContainerInterface $container The container interface
+ * @param ContainerInterface $container The container interface
* @param SettingsService $settingsService The settings service
- * @param LoggerInterface $logger The logger instance
+ * @param LoggerInterface $logger The logger instance
*/
public function __construct(
private readonly ContainerInterface $container,
private readonly SettingsService $settingsService,
private readonly LoggerInterface $logger
) {
- }
+ }//end __construct()
/**
* Handle module compliance update
@@ -64,115 +64,150 @@ public function __construct(
public function handleModuleComplianceUpdate(object $moduleObject): void
{
$startTime = microtime(true);
- $moduleId = $moduleObject->getId();
-
- $this->logger->info('ModuleComplianceService: Starting module compliance update handling', [
- 'moduleId' => $moduleId,
- 'timestamp' => date('Y-m-d H:i:s')
- ]);
+ $moduleId = $moduleObject->getId();
+
+ $this->logger->info(
+ 'ModuleComplianceService: Starting module compliance update handling',
+ [
+ 'moduleId' => $moduleId,
+ 'timestamp' => date('Y-m-d H:i:s'),
+ ]
+ );
try {
- // Get the object service
+ // Get the object service.
$objectService = $this->getObjectService();
- if (!$objectService) {
+ if ($objectService === null) {
throw new \RuntimeException('ObjectService not available');
}
- // Get module UUID from entity
+ // Get module UUID from entity.
$moduleUuid = $moduleObject->getUuid();
$moduleData = $moduleObject->getObject();
- if (!$moduleUuid) {
- $this->logger->warning('ModuleComplianceService: Module object has no UUID', [
- 'moduleId' => $moduleId
- ]);
+ if ($moduleUuid === null) {
+ $this->logger->warning(
+ 'ModuleComplianceService: Module object has no UUID',
+ [
+ 'moduleId' => $moduleId,
+ ]
+ );
return;
}
- $this->logger->debug('ModuleComplianceService: Processing module', [
- 'moduleId' => $moduleId,
- 'moduleUuid' => $moduleUuid
- ]);
-
- // Get compliance objects linked to this module
- $complianceObjects = $this->getComplianceObjectsForModule($moduleUuid);
-
- $this->logger->debug('ModuleComplianceService: Found compliance objects', [
- 'moduleId' => $moduleId,
- 'moduleUuid' => $moduleUuid,
- 'complianceCount' => count($complianceObjects)
- ]);
-
- // Extract standaardversie UUIDs from compliance objects
- $standaardversieUuids = $this->extractStandaardversieUuids($complianceObjects);
-
- $this->logger->debug('ModuleComplianceService: Extracted standaardversie UUIDs', [
- 'moduleId' => $moduleId,
- 'moduleUuid' => $moduleUuid,
- 'standaardversieUuids' => $standaardversieUuids,
- 'count' => count($standaardversieUuids)
- ]);
-
- // Get current standaarden from module
+ $this->logger->debug(
+ 'ModuleComplianceService: Processing module',
+ [
+ 'moduleId' => $moduleId,
+ 'moduleUuid' => $moduleUuid,
+ ]
+ );
+
+ // Get compliance objects linked to this module.
+ $complianceObjects = $this->getComplianceObjectsForModule(moduleUuid: $moduleUuid);
+
+ $this->logger->debug(
+ 'ModuleComplianceService: Found compliance objects',
+ [
+ 'moduleId' => $moduleId,
+ 'moduleUuid' => $moduleUuid,
+ 'complianceCount' => count($complianceObjects),
+ ]
+ );
+
+ // Extract standaardversie UUIDs from compliance objects.
+ $standaardversieUuids = $this->extractStandaardversieUuids(complianceObjects: $complianceObjects);
+
+ $this->logger->debug(
+ 'ModuleComplianceService: Extracted standaardversie UUIDs',
+ [
+ 'moduleId' => $moduleId,
+ 'moduleUuid' => $moduleUuid,
+ 'standaardversieUuids' => $standaardversieUuids,
+ 'count' => count($standaardversieUuids),
+ ]
+ );
+
+ // Get current standaarden from module.
$currentStandaarden = $moduleData['standaardVersies'] ?? [];
-
- // Ensure currentStandaarden is an array
- if (!is_array($currentStandaarden)) {
+
+ // Ensure currentStandaarden is an array.
+ if (is_array($currentStandaarden) === false) {
$currentStandaarden = [];
}
- $this->logger->debug('ModuleComplianceService: Current standaarden', [
- 'moduleId' => $moduleId,
- 'moduleUuid' => $moduleUuid,
- 'currentStandaarden' => $currentStandaarden,
- 'count' => count($currentStandaarden)
- ]);
-
- // Compare and update if different
- if ($this->arraysAreDifferent($currentStandaarden, $standaardversieUuids)) {
- $this->logger->info('ModuleComplianceService: Standaarden differ, updating module', [
- 'moduleId' => $moduleId,
- 'moduleUuid' => $moduleUuid,
- 'oldStandaarden' => $currentStandaarden,
- 'newStandaarden' => $standaardversieUuids
- ]);
-
- // Update the module with new standaarden
- $this->updateModuleStandaarden($moduleObject, $standaardversieUuids);
-
- $this->logger->info('ModuleComplianceService: Successfully updated module standaarden', [
- 'moduleId' => $moduleId,
- 'moduleUuid' => $moduleUuid,
- 'standaarden' => $standaardversieUuids
- ]);
+ $this->logger->debug(
+ 'ModuleComplianceService: Current standaarden',
+ [
+ 'moduleId' => $moduleId,
+ 'moduleUuid' => $moduleUuid,
+ 'currentStandaarden' => $currentStandaarden,
+ 'count' => count($currentStandaarden),
+ ]
+ );
+
+ // Compare and update if different.
+ if ($this->arraysAreDifferent(array1: $currentStandaarden, array2: $standaardversieUuids) === true) {
+ $this->logger->info(
+ 'ModuleComplianceService: Standaarden differ, updating module',
+ [
+ 'moduleId' => $moduleId,
+ 'moduleUuid' => $moduleUuid,
+ 'oldStandaarden' => $currentStandaarden,
+ 'newStandaarden' => $standaardversieUuids,
+ ]
+ );
+
+ // Update the module with new standaarden.
+ $this->updateModuleStandaarden(
+ moduleObject: $moduleObject,
+ standaardversieUuids: $standaardversieUuids
+ );
+
+ $this->logger->info(
+ 'ModuleComplianceService: Successfully updated module standaarden',
+ [
+ 'moduleId' => $moduleId,
+ 'moduleUuid' => $moduleUuid,
+ 'standaarden' => $standaardversieUuids,
+ ]
+ );
} else {
- $this->logger->debug('ModuleComplianceService: Standaarden are already up to date', [
- 'moduleId' => $moduleId,
- 'moduleUuid' => $moduleUuid
- ]);
- }
+ $this->logger->debug(
+ 'ModuleComplianceService: Standaarden are already up to date',
+ [
+ 'moduleId' => $moduleId,
+ 'moduleUuid' => $moduleUuid,
+ ]
+ );
+ }//end if
- $endTime = microtime(true);
+ $endTime = microtime(true);
$executionTime = round(($endTime - $startTime) * 1000, 2);
-
- $this->logger->info('ModuleComplianceService: Completed module compliance update handling', [
- 'moduleId' => $moduleId,
- 'moduleUuid' => $moduleUuid,
- 'executionTimeMs' => $executionTime,
- 'timestamp' => date('Y-m-d H:i:s')
- ]);
+ $this->logger->info(
+ 'ModuleComplianceService: Completed module compliance update handling',
+ [
+ 'moduleId' => $moduleId,
+ 'moduleUuid' => $moduleUuid,
+ 'executionTimeMs' => $executionTime,
+ 'timestamp' => date('Y-m-d H:i:s'),
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('ModuleComplianceService: Failed to handle module compliance update', [
- 'moduleId' => $moduleId,
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'ModuleComplianceService: Failed to handle module compliance update',
+ [
+ 'moduleId' => $moduleId,
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
throw $e;
- }
- }
+ }//end try
+ }//end handleModuleComplianceUpdate()
/**
* Get compliance objects linked to a module
@@ -186,61 +221,72 @@ public function handleModuleComplianceUpdate(object $moduleObject): void
private function getComplianceObjectsForModule(string $moduleUuid): array
{
try {
- // Get compliance schema ID from configuration
+ // Get compliance schema ID from configuration.
$complianceSchemaId = $this->settingsService->getSchemaIdForObjectType('compliancy');
- if (!$complianceSchemaId) {
- $this->logger->warning('ModuleComplianceService: Compliance schema not configured', [
- 'moduleUuid' => $moduleUuid
- ]);
+ if ($complianceSchemaId === null) {
+ $this->logger->warning(
+ 'ModuleComplianceService: Compliance schema not configured',
+ [
+ 'moduleUuid' => $moduleUuid,
+ ]
+ );
return [];
}
- // Get register ID from voorzieningen config
+ // Get register ID from voorzieningen config.
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $registerId = $voorzieningenConfig['register'] ?? null;
+ $registerId = $voorzieningenConfig['register'] ?? null;
- if (!$registerId) {
- $this->logger->warning('ModuleComplianceService: Voorzieningen register not configured', [
- 'moduleUuid' => $moduleUuid
- ]);
+ if ($registerId === null) {
+ $this->logger->warning(
+ 'ModuleComplianceService: Voorzieningen register not configured',
+ [
+ 'moduleUuid' => $moduleUuid,
+ ]
+ );
return [];
}
- // Get object service
+ // Get object service.
$objectService = $this->getObjectService();
- if (!$objectService) {
+ if ($objectService === null) {
throw new \RuntimeException('ObjectService not available');
}
- // Query compliance objects where module matches the module UUID
+ // Query compliance objects where module matches the module UUID.
$query = [
- '@self' => [
- 'schema' => (int) $complianceSchemaId,
+ '@self' => [
+ 'schema' => (int) $complianceSchemaId,
'register' => (int) $registerId,
],
'module' => $moduleUuid,
];
$complianceObjects = $objectService->searchObjects($query);
- $this->logger->debug('ModuleComplianceService: Retrieved compliance objects', [
- 'moduleUuid' => $moduleUuid,
- 'complianceSchemaId' => $complianceSchemaId,
- 'count' => count($complianceObjects)
- ]);
+ $this->logger->debug(
+ 'ModuleComplianceService: Retrieved compliance objects',
+ [
+ 'moduleUuid' => $moduleUuid,
+ 'complianceSchemaId' => $complianceSchemaId,
+ 'count' => count($complianceObjects),
+ ]
+ );
return $complianceObjects;
-
} catch (\Exception $e) {
- $this->logger->error('ModuleComplianceService: Failed to get compliance objects for module', [
- 'moduleUuid' => $moduleUuid,
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine()
- ]);
+ $this->logger->error(
+ 'ModuleComplianceService: Failed to get compliance objects for module',
+ [
+ 'moduleUuid' => $moduleUuid,
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]
+ );
throw $e;
- }
- }
+ }//end try
+ }//end getComplianceObjectsForModule()
/**
* Extract standaardversie UUIDs from compliance objects
@@ -253,72 +299,96 @@ private function extractStandaardversieUuids(array $complianceObjects): array
{
$standaardversieUuids = [];
$tracking = [
- 'withStandaardversie' => 0,
+ 'withStandaardversie' => 0,
'withoutStandaardversie' => 0,
- 'stringType' => 0,
- 'arrayType' => 0,
- 'objectType' => 0,
- 'invalidType' => 0,
+ 'stringType' => 0,
+ 'arrayType' => 0,
+ 'objectType' => 0,
+ 'invalidType' => 0,
];
foreach ($complianceObjects as $complianceObject) {
- $complianceData = $complianceObject->getObject();
+ $complianceData = $complianceObject->getObject();
$standaardversie = $complianceData['standaardversie'] ?? null;
-
- if ($standaardversie) {
+
+ if ($standaardversie !== null) {
$tracking['withStandaardversie']++;
-
- // Handle both string UUID and object with UUID property
- if (is_string($standaardversie)) {
+
+ // Handle both string UUID and object with UUID property.
+ if (is_string($standaardversie) === true) {
$tracking['stringType']++;
$standaardversieUuids[] = $standaardversie;
- $this->logger->debug('ModuleComplianceService: Found string standaardversie', [
- 'complianceId' => $complianceObject->getId(),
- 'standaardversie' => $standaardversie
- ]);
- } elseif (is_array($standaardversie) && isset($standaardversie['uuid'])) {
+ $this->logger->debug(
+ 'ModuleComplianceService: Found string standaardversie',
+ [
+ 'complianceId' => $complianceObject->getId(),
+ 'standaardversie' => $standaardversie,
+ ]
+ );
+ } else if (is_array($standaardversie) === true && isset($standaardversie['uuid']) === true) {
$tracking['arrayType']++;
$standaardversieUuids[] = $standaardversie['uuid'];
- $this->logger->debug('ModuleComplianceService: Found array standaardversie', [
- 'complianceId' => $complianceObject->getId(),
- 'standaardversie' => $standaardversie['uuid']
- ]);
- } elseif (is_object($standaardversie) && isset($standaardversie->uuid)) {
+ $this->logger->debug(
+ 'ModuleComplianceService: Found array standaardversie',
+ [
+ 'complianceId' => $complianceObject->getId(),
+ 'standaardversie' => $standaardversie['uuid'],
+ ]
+ );
+ } else if (is_object($standaardversie) === true && isset($standaardversie->uuid) === true) {
$tracking['objectType']++;
$standaardversieUuids[] = $standaardversie->uuid;
- $this->logger->debug('ModuleComplianceService: Found object standaardversie', [
- 'complianceId' => $complianceObject->getId(),
- 'standaardversie' => $standaardversie->uuid
- ]);
+ $this->logger->debug(
+ 'ModuleComplianceService: Found object standaardversie',
+ [
+ 'complianceId' => $complianceObject->getId(),
+ 'standaardversie' => $standaardversie->uuid,
+ ]
+ );
} else {
$tracking['invalidType']++;
- $this->logger->warning('ModuleComplianceService: Invalid standaardversie type', [
- 'complianceId' => $complianceObject->getId(),
- 'type' => gettype($standaardversie),
- 'value' => is_array($standaardversie) ? json_encode($standaardversie) : (string)$standaardversie
- ]);
- }
+ if (is_array($standaardversie) === true) {
+ $standaardversieValue = json_encode($standaardversie);
+ } else {
+ $standaardversieValue = (string) $standaardversie;
+ }
+
+ $this->logger->warning(
+ 'ModuleComplianceService: Invalid standaardversie type',
+ [
+ 'complianceId' => $complianceObject->getId(),
+ 'type' => gettype($standaardversie),
+ 'value' => $standaardversieValue,
+ ]
+ );
+ }//end if
} else {
$tracking['withoutStandaardversie']++;
- $this->logger->debug('ModuleComplianceService: Compliance object missing standaardversie', [
- 'complianceId' => $complianceObject->getId(),
- 'complianceUuid' => $complianceData['uuid'] ?? 'unknown'
- ]);
- }
- }
-
- // Remove duplicates and empty values
+ $this->logger->debug(
+ 'ModuleComplianceService: Compliance object missing standaardversie',
+ [
+ 'complianceId' => $complianceObject->getId(),
+ 'complianceUuid' => $complianceData['uuid'] ?? 'unknown',
+ ]
+ );
+ }//end if
+ }//end foreach
+
+ // Remove duplicates and empty values.
$standaardversieUuids = array_unique(array_filter($standaardversieUuids));
- $this->logger->info('ModuleComplianceService: Extracted standaardversie UUIDs', [
- 'complianceCount' => count($complianceObjects),
- 'tracking' => $tracking,
- 'uniqueStandaardversieUuids' => count($standaardversieUuids),
- 'standaardversieUuids' => $standaardversieUuids
- ]);
+ $this->logger->info(
+ 'ModuleComplianceService: Extracted standaardversie UUIDs',
+ [
+ 'complianceCount' => count($complianceObjects),
+ 'tracking' => $tracking,
+ 'uniqueStandaardversieUuids' => count($standaardversieUuids),
+ 'standaardversieUuids' => $standaardversieUuids,
+ ]
+ );
return $standaardversieUuids;
- }
+ }//end extractStandaardversieUuids()
/**
* Check if two arrays are different (ignoring order)
@@ -330,17 +400,17 @@ private function extractStandaardversieUuids(array $complianceObjects): array
*/
private function arraysAreDifferent(array $array1, array $array2): bool
{
- // Sort both arrays to ignore order
+ // Sort both arrays to ignore order.
sort($array1);
sort($array2);
-
+
return $array1 !== $array2;
- }
+ }//end arraysAreDifferent()
/**
* Update module standaarden property
*
- * @param object $moduleObject The module object to update
+ * @param object $moduleObject The module object to update
* @param array $standaardversieUuids Array of standaardversie UUIDs
*
* @return void
@@ -350,22 +420,22 @@ private function arraysAreDifferent(array $array1, array $array2): bool
private function updateModuleStandaarden(object $moduleObject, array $standaardversieUuids): void
{
try {
- // Get object service
+ // Get object service.
$objectService = $this->getObjectService();
- if (!$objectService) {
+ if ($objectService === null) {
throw new \RuntimeException('ObjectService not available');
}
- // Get current module data
+ // Get current module data.
$moduleData = $moduleObject->getObject();
-
- // Update standaarden property
+
+ // Update standaarden property.
$moduleData['standaardVersies'] = $standaardversieUuids;
-
- // Get register ID from module object
+
+ // Get register ID from module object.
$registerId = $moduleObject->getRegister();
-
- // Save the updated module
+
+ // Save the updated module.
$savedObject = $objectService->saveObject(
object: $moduleData,
extend: [],
@@ -374,23 +444,28 @@ private function updateModuleStandaarden(object $moduleObject, array $standaardv
uuid: $moduleObject->getUuid()
);
- $this->logger->info('ModuleComplianceService: Updated module standaarden', [
- 'moduleId' => $moduleObject->getId(),
- 'moduleUuid' => $moduleData['uuid'] ?? null,
- 'standaarden' => $standaardversieUuids
- ]);
-
+ $this->logger->info(
+ 'ModuleComplianceService: Updated module standaarden',
+ [
+ 'moduleId' => $moduleObject->getId(),
+ 'moduleUuid' => $moduleData['uuid'] ?? null,
+ 'standaarden' => $standaardversieUuids,
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('ModuleComplianceService: Failed to update module standaarden', [
- 'moduleId' => $moduleObject->getId(),
- 'standaardversieUuids' => $standaardversieUuids,
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine()
- ]);
+ $this->logger->error(
+ 'ModuleComplianceService: Failed to update module standaarden',
+ [
+ 'moduleId' => $moduleObject->getId(),
+ 'standaardversieUuids' => $standaardversieUuids,
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]
+ );
throw $e;
- }
- }
+ }//end try
+ }//end updateModuleStandaarden()
/**
* Perform bulk sync of module standards from all compliance objects
@@ -402,290 +477,312 @@ private function updateModuleStandaarden(object $moduleObject, array $standaardv
public function bulkSyncModuleStandards(): array
{
$startTime = microtime(true);
-
+
$this->logger->info('ModuleComplianceService: Starting bulk sync of module standards');
-
+
$results = [
- 'totalProcessed' => 0,
- 'complianceMissingModule' => 0,
+ 'totalProcessed' => 0,
+ 'complianceMissingModule' => 0,
'complianceMissingStandaardversie' => 0,
- 'modulesFound' => 0,
- 'modulesNotFound' => 0,
- 'modulesWithNoStandards' => 0,
- 'modulesAlreadyUpToDate' => 0,
- 'modulesUpdated' => 0,
- 'standardsAdded' => 0,
- 'errors' => [],
- 'samples' => [
- 'complianceWithStandaardversie' => [],
+ 'modulesFound' => 0,
+ 'modulesNotFound' => 0,
+ 'modulesWithNoStandards' => 0,
+ 'modulesAlreadyUpToDate' => 0,
+ 'modulesUpdated' => 0,
+ 'standardsAdded' => 0,
+ 'errors' => [],
+ 'samples' => [
+ 'complianceWithStandaardversie' => [],
'complianceWithoutStandaardversie' => [],
- 'modulesUpdated' => [],
- 'modulesSkipped' => [],
+ 'modulesUpdated' => [],
+ 'modulesSkipped' => [],
],
- 'modules' => [], // Full list of all processed modules
+ // Full list of all processed modules.
+ 'modules' => [],
];
try {
- // Get compliance schema ID from configuration
+ // Get compliance schema ID from configuration.
$complianceSchemaId = $this->settingsService->getSchemaIdForObjectType('compliancy');
- if (!$complianceSchemaId) {
+ if ($complianceSchemaId === null) {
throw new \RuntimeException('Compliance schema not configured');
}
- // Get register ID from voorzieningen config
+ // Get register ID from voorzieningen config.
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $registerId = $voorzieningenConfig['register'] ?? null;
+ $registerId = $voorzieningenConfig['register'] ?? null;
- if (!$registerId) {
+ if ($registerId === null) {
throw new \RuntimeException('Voorzieningen register not configured');
}
- // Get module schema ID from configuration
+ // Get module schema ID from configuration.
$moduleSchemaId = $this->settingsService->getSchemaIdForObjectType('module');
- if (!$moduleSchemaId) {
+ if ($moduleSchemaId === null) {
throw new \RuntimeException('Module schema not configured');
}
- // Get object service
+ // Get object service.
$objectService = $this->getObjectService();
- if (!$objectService) {
+ if ($objectService === null) {
throw new \RuntimeException('ObjectService not available');
}
- // Get all compliance objects (both schema AND register are required)
+ // Get all compliance objects (both schema AND register are required).
$query = [
'@self' => [
- 'schema' => (int) $complianceSchemaId,
+ 'schema' => (int) $complianceSchemaId,
'register' => (int) $registerId,
],
];
$complianceObjects = $objectService->searchObjects($query);
-
- $this->logger->info('ModuleComplianceService: Found compliance objects for bulk sync', [
- 'count' => count($complianceObjects)
- ]);
+
+ $this->logger->info(
+ 'ModuleComplianceService: Found compliance objects for bulk sync',
+ [
+ 'count' => count($complianceObjects),
+ ]
+ );
$results['totalProcessed'] = count($complianceObjects);
- // Group compliance objects by module UUID and track samples
+ // Group compliance objects by module UUID and track samples.
$complianceByModule = [];
- $sampleCount = 0;
-
+ $sampleCount = 0;
+
foreach ($complianceObjects as $complianceObject) {
- $complianceData = $complianceObject->getObject();
- $moduleUuid = $complianceData['module'] ?? null;
+ $complianceData = $complianceObject->getObject();
+ $moduleUuid = $complianceData['module'] ?? null;
$standaardversie = $complianceData['standaardversie'] ?? null;
-
- // Track compliance objects with/without standaardversie (first 5 samples)
+
+ // Track compliance objects with/without standaardversie (first 5 samples).
if ($sampleCount < 5) {
- if ($standaardversie) {
+ if ($standaardversie !== null) {
$results['samples']['complianceWithStandaardversie'][] = [
- 'id' => $complianceObject->getId(),
- 'uuid' => $complianceData['uuid'] ?? 'unknown',
- 'module' => $moduleUuid,
+ 'id' => $complianceObject->getId(),
+ 'uuid' => $complianceData['uuid'] ?? 'unknown',
+ 'module' => $moduleUuid,
'standaardversie' => $standaardversie,
];
} else {
$results['samples']['complianceWithoutStandaardversie'][] = [
- 'id' => $complianceObject->getId(),
- 'uuid' => $complianceData['uuid'] ?? 'unknown',
+ 'id' => $complianceObject->getId(),
+ 'uuid' => $complianceData['uuid'] ?? 'unknown',
'module' => $moduleUuid,
];
}
+
$sampleCount++;
}
-
- if (!$moduleUuid) {
+
+ if ($moduleUuid === null) {
$results['complianceMissingModule']++;
- $results['errors'][] = 'Compliance object has no module reference: ' . $complianceObject->getId();
+ $results['errors'][] = 'Compliance object has no module reference: '.$complianceObject->getId();
continue;
}
- // Handle both string UUID and object with UUID property
- if (is_string($moduleUuid)) {
+ // Handle both string UUID and object with UUID property.
+ if (is_string($moduleUuid) === true) {
$moduleUuidValue = $moduleUuid;
- } elseif (is_array($moduleUuid) && isset($moduleUuid['uuid'])) {
+ } else if (is_array($moduleUuid) === true && isset($moduleUuid['uuid']) === true) {
$moduleUuidValue = $moduleUuid['uuid'];
- } elseif (is_object($moduleUuid) && isset($moduleUuid->uuid)) {
+ } else if (is_object($moduleUuid) === true && isset($moduleUuid->uuid) === true) {
$moduleUuidValue = $moduleUuid->uuid;
} else {
- $results['errors'][] = 'Invalid module reference in compliance object: ' . $complianceObject->getId();
+ $results['errors'][] = 'Invalid module reference in compliance object: '.$complianceObject->getId();
continue;
}
- if (!isset($complianceByModule[$moduleUuidValue])) {
+ if (isset($complianceByModule[$moduleUuidValue]) === false) {
$complianceByModule[$moduleUuidValue] = [];
}
+
$complianceByModule[$moduleUuidValue][] = $complianceObject;
- }
+ }//end foreach
$results['modulesFound'] = count($complianceByModule);
- // Process each module
+ // Process each module.
foreach ($complianceByModule as $moduleUuid => $moduleComplianceObjects) {
try {
- // Find the module object (must specify register and schema for magic table lookup)
+ // Find the module object (must specify register and schema for magic table lookup).
$moduleObject = $objectService->find(
id: $moduleUuid,
register: (int) $registerId,
schema: (int) $moduleSchemaId
);
- if (!$moduleObject) {
+ if ($moduleObject === null) {
$results['modulesNotFound']++;
- $results['errors'][] = 'Module not found for UUID: ' . $moduleUuid;
-
- // Add to full modules list
+ $results['errors'][] = 'Module not found for UUID: '.$moduleUuid;
+
+ // Add to full modules list.
$results['modules'][] = [
- 'uuid' => $moduleUuid,
- 'name' => 'Not Found',
- 'status' => 'error',
- 'reason' => 'Module not found in database',
- 'complianceCount' => count($moduleComplianceObjects),
+ 'uuid' => $moduleUuid,
+ 'name' => 'Not Found',
+ 'status' => 'error',
+ 'reason' => 'Module not found in database',
+ 'complianceCount' => count($moduleComplianceObjects),
'currentStandaarden' => [],
- 'newStandaarden' => [],
- 'standardsCount' => 0,
+ 'newStandaarden' => [],
+ 'standardsCount' => 0,
];
continue;
}
- // Get module data for tracking
+ // Get module data for tracking.
$moduleData = $moduleObject->getObject();
$moduleName = $moduleData['name'] ?? $moduleData['title'] ?? 'Unknown';
- // Extract standaardversie UUIDs from compliance objects
- $standaardversieUuids = $this->extractStandaardversieUuids($moduleComplianceObjects);
-
- if (empty($standaardversieUuids)) {
+ // Extract standaardversie UUIDs from compliance objects.
+ $standaardversieUuids = $this->extractStandaardversieUuids(complianceObjects: $moduleComplianceObjects);
+
+ if (empty($standaardversieUuids) === true) {
$results['modulesWithNoStandards']++;
$results['complianceMissingStandaardversie'] += count($moduleComplianceObjects);
-
- // Add to full modules list
+
+ // Add to full modules list.
$results['modules'][] = [
- 'uuid' => $moduleUuid,
- 'name' => $moduleName,
- 'status' => 'skipped',
- 'reason' => 'No standaardversie found',
- 'complianceCount' => count($moduleComplianceObjects),
+ 'uuid' => $moduleUuid,
+ 'name' => $moduleName,
+ 'status' => 'skipped',
+ 'reason' => 'No standaardversie found',
+ 'complianceCount' => count($moduleComplianceObjects),
'currentStandaarden' => [],
- 'newStandaarden' => [],
- 'standardsCount' => 0,
+ 'newStandaarden' => [],
+ 'standardsCount' => 0,
];
-
- // Add to samples (first 5)
+
+ // Add to samples (first 5).
if (count($results['samples']['modulesSkipped']) < 5) {
+ $complianceCount = count($moduleComplianceObjects);
+ $reason = 'No standaardversie found in '.$complianceCount.' compliance object(s)';
$results['samples']['modulesSkipped'][] = [
- 'uuid' => $moduleUuid,
- 'name' => $moduleName,
- 'reason' => 'No standaardversie found in ' . count($moduleComplianceObjects) . ' compliance object(s)',
- 'complianceCount' => count($moduleComplianceObjects),
+ 'uuid' => $moduleUuid,
+ 'name' => $moduleName,
+ 'reason' => $reason,
+ 'complianceCount' => $complianceCount,
];
}
+
continue;
- }
+ }//end if
- // Get current standaarden from module
+ // Get current standaarden from module.
$currentStandaarden = $moduleData['standaardVersies'] ?? [];
-
- // Ensure currentStandaarden is an array
- if (!is_array($currentStandaarden)) {
+
+ // Ensure currentStandaarden is an array.
+ if (is_array($currentStandaarden) === false) {
$currentStandaarden = [];
}
- // Compare and update if different
- if ($this->arraysAreDifferent($currentStandaarden, $standaardversieUuids)) {
- // Update the module with new standaarden
- $this->updateModuleStandaarden($moduleObject, $standaardversieUuids);
-
+ // Compare and update if different.
+ if ($this->arraysAreDifferent(array1: $currentStandaarden, array2: $standaardversieUuids) === true) {
+ // Update the module with new standaarden.
+ $this->updateModuleStandaarden(
+ moduleObject: $moduleObject,
+ standaardversieUuids: $standaardversieUuids
+ );
+
$results['modulesUpdated']++;
$results['standardsAdded'] += count($standaardversieUuids);
-
- // Add to full modules list
+
+ // Add to full modules list.
$results['modules'][] = [
- 'uuid' => $moduleUuid,
- 'name' => $moduleName,
- 'status' => 'updated',
- 'reason' => 'Standards updated',
- 'complianceCount' => count($moduleComplianceObjects),
+ 'uuid' => $moduleUuid,
+ 'name' => $moduleName,
+ 'status' => 'updated',
+ 'reason' => 'Standards updated',
+ 'complianceCount' => count($moduleComplianceObjects),
'currentStandaarden' => $currentStandaarden,
- 'newStandaarden' => $standaardversieUuids,
- 'standardsCount' => count($standaardversieUuids),
+ 'newStandaarden' => $standaardversieUuids,
+ 'standardsCount' => count($standaardversieUuids),
];
-
- // Add to samples (first 5)
+
+ // Add to samples (first 5).
if (count($results['samples']['modulesUpdated']) < 5) {
$results['samples']['modulesUpdated'][] = [
- 'uuid' => $moduleUuid,
- 'name' => $moduleName,
- 'oldStandaarden' => $currentStandaarden,
- 'newStandaarden' => $standaardversieUuids,
+ 'uuid' => $moduleUuid,
+ 'name' => $moduleName,
+ 'oldStandaarden' => $currentStandaarden,
+ 'newStandaarden' => $standaardversieUuids,
'complianceCount' => count($moduleComplianceObjects),
];
}
-
- $this->logger->info('ModuleComplianceService: Updated module in bulk sync', [
- 'moduleUuid' => $moduleUuid,
- 'moduleName' => $moduleName,
- 'standaarden' => $standaardversieUuids,
- 'count' => count($standaardversieUuids)
- ]);
+
+ $this->logger->info(
+ 'ModuleComplianceService: Updated module in bulk sync',
+ [
+ 'moduleUuid' => $moduleUuid,
+ 'moduleName' => $moduleName,
+ 'standaarden' => $standaardversieUuids,
+ 'count' => count($standaardversieUuids),
+ ]
+ );
} else {
$results['modulesAlreadyUpToDate']++;
-
- // Add to full modules list
+
+ // Add to full modules list.
$results['modules'][] = [
- 'uuid' => $moduleUuid,
- 'name' => $moduleName,
- 'status' => 'up-to-date',
- 'reason' => 'Already up-to-date',
- 'complianceCount' => count($moduleComplianceObjects),
+ 'uuid' => $moduleUuid,
+ 'name' => $moduleName,
+ 'status' => 'up-to-date',
+ 'reason' => 'Already up-to-date',
+ 'complianceCount' => count($moduleComplianceObjects),
'currentStandaarden' => $currentStandaarden,
- 'newStandaarden' => $standaardversieUuids,
- 'standardsCount' => count($currentStandaarden),
+ 'newStandaarden' => $standaardversieUuids,
+ 'standardsCount' => count($currentStandaarden),
];
-
- // Add to samples (first 5)
+
+ // Add to samples (first 5).
if (count($results['samples']['modulesSkipped']) < 5) {
$results['samples']['modulesSkipped'][] = [
- 'uuid' => $moduleUuid,
- 'name' => $moduleName,
- 'reason' => 'Already up-to-date',
- 'currentStandaarden' => $currentStandaarden,
+ 'uuid' => $moduleUuid,
+ 'name' => $moduleName,
+ 'reason' => 'Already up-to-date',
+ 'currentStandaarden' => $currentStandaarden,
'extractedStandaarden' => $standaardversieUuids,
- 'complianceCount' => count($moduleComplianceObjects),
+ 'complianceCount' => count($moduleComplianceObjects),
];
}
- }
-
+ }//end if
} catch (\Exception $e) {
- $results['errors'][] = 'Failed to process module ' . $moduleUuid . ': ' . $e->getMessage();
- $this->logger->error('ModuleComplianceService: Error processing module in bulk sync', [
- 'moduleUuid' => $moduleUuid,
- 'exception' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
- }
- }
-
- $endTime = microtime(true);
+ $results['errors'][] = 'Failed to process module '.$moduleUuid.': '.$e->getMessage();
+ $this->logger->error(
+ 'ModuleComplianceService: Error processing module in bulk sync',
+ [
+ 'moduleUuid' => $moduleUuid,
+ 'exception' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+ }//end try
+ }//end foreach
+
+ $endTime = microtime(true);
$executionTime = round(($endTime - $startTime) * 1000, 2);
-
- $this->logger->info('ModuleComplianceService: Completed bulk sync of module standards', [
- 'results' => $results,
- 'executionTimeMs' => $executionTime
- ]);
- return $results;
+ $this->logger->info(
+ 'ModuleComplianceService: Completed bulk sync of module standards',
+ [
+ 'results' => $results,
+ 'executionTimeMs' => $executionTime,
+ ]
+ );
+ return $results;
} catch (\Exception $e) {
- $this->logger->error('ModuleComplianceService: Bulk sync failed', [
- 'exception' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'ModuleComplianceService: Bulk sync failed',
+ [
+ 'exception' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
throw $e;
- }
- }
+ }//end try
+ }//end bulkSyncModuleStandards()
/**
* Get the object service
@@ -697,10 +794,13 @@ private function getObjectService(): ?ObjectService
try {
return $this->container->get('OCA\OpenRegister\Service\ObjectService');
} catch (\Exception $e) {
- $this->logger->error('ModuleComplianceService: Failed to get ObjectService', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'ModuleComplianceService: Failed to get ObjectService',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return null;
}
- }
-}
+ }//end getObjectService()
+}//end class
diff --git a/lib/Service/ModuleRegistrationService.php b/lib/Service/ModuleRegistrationService.php
index c2fd633f..d0ba13db 100644
--- a/lib/Service/ModuleRegistrationService.php
+++ b/lib/Service/ModuleRegistrationService.php
@@ -1,4 +1,18 @@
+ * @copyright 2024 Conduction B.V.
+ * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
+ */
declare(strict_types=1);
@@ -11,6 +25,9 @@
/**
* Service for auto-setting geregistreerdDoor on module objects
* based on the owning organisation's type.
+ *
+ * @category Service
+ * @package OCA\SoftwareCatalog\Service
*/
class ModuleRegistrationService
{
@@ -24,33 +41,50 @@ class ModuleRegistrationService
'Community' => 'Community',
];
+ /**
+ * Constructor for ModuleRegistrationService.
+ *
+ * @param ContainerInterface $container The DI container
+ * @param SettingsService $settingsService The settings service
+ * @param LoggerInterface $logger The logger instance
+ */
public function __construct(
private readonly ContainerInterface $container,
private readonly SettingsService $settingsService,
private readonly LoggerInterface $logger
) {
- }
+ }//end __construct()
/**
* Handle a module create/update event: look up the owning organisatie's type
* and set geregistreerdDoor accordingly.
+ *
+ * @param object $moduleObject The module object to process
+ *
+ * @return void
*/
public function handleModuleRegistration(object $moduleObject): void
{
- $moduleId = $moduleObject->getId();
+ $moduleId = $moduleObject->getId();
$organisationUuid = $moduleObject->getOrganisation();
- if (empty($organisationUuid)) {
- $this->logger->debug('ModuleRegistrationService: Module has no organisation, skipping', [
- 'moduleId' => $moduleId,
- ]);
+ if (empty($organisationUuid) === true) {
+ $this->logger->debug(
+ 'ModuleRegistrationService: Module has no organisation, skipping',
+ [
+ 'moduleId' => $moduleId,
+ ]
+ );
return;
}
- $this->logger->info('ModuleRegistrationService: Processing module for geregistreerdDoor', [
- 'moduleId' => $moduleId,
- 'organisationUuid' => $organisationUuid,
- ]);
+ $this->logger->info(
+ 'ModuleRegistrationService: Processing module for geregistreerdDoor',
+ [
+ 'moduleId' => $moduleId,
+ 'organisationUuid' => $organisationUuid,
+ ]
+ );
try {
$objectService = $this->getObjectService();
@@ -62,13 +96,16 @@ public function handleModuleRegistration(object $moduleObject): void
// Look up the organisatie object whose UUID matches the module's _organisation.
$organisatieSchemaId = $this->settingsService->getSchemaIdForObjectType('organisatie');
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $registerId = $voorzieningenConfig['register'] ?? null;
+ $registerId = $voorzieningenConfig['register'] ?? null;
if ($organisatieSchemaId === null || $registerId === null) {
- $this->logger->warning('ModuleRegistrationService: Organisatie schema or register not configured', [
- 'organisatieSchemaId' => $organisatieSchemaId,
- 'registerId' => $registerId,
- ]);
+ $this->logger->warning(
+ 'ModuleRegistrationService: Organisatie schema or register not configured',
+ [
+ 'organisatieSchemaId' => $organisatieSchemaId,
+ 'registerId' => $registerId,
+ ]
+ );
return;
}
@@ -81,29 +118,38 @@ public function handleModuleRegistration(object $moduleObject): void
schema: (int) $organisatieSchemaId
);
} catch (\Exception $e) {
- $this->logger->debug('ModuleRegistrationService: Organisatie not found for organisation UUID', [
- 'moduleId' => $moduleId,
- 'organisationUuid' => $organisationUuid,
- ]);
+ $this->logger->debug(
+ 'ModuleRegistrationService: Organisatie not found for organisation UUID',
+ [
+ 'moduleId' => $moduleId,
+ 'organisationUuid' => $organisationUuid,
+ ]
+ );
return;
}
if ($organisatieObject === null) {
- $this->logger->debug('ModuleRegistrationService: Organisatie not found for organisation UUID', [
- 'moduleId' => $moduleId,
- 'organisationUuid' => $organisationUuid,
- ]);
+ $this->logger->debug(
+ 'ModuleRegistrationService: Organisatie not found for organisation UUID',
+ [
+ 'moduleId' => $moduleId,
+ 'organisationUuid' => $organisationUuid,
+ ]
+ );
return;
}
$organisatieData = $organisatieObject->getObject();
- $orgType = $organisatieData['type'] ?? null;
-
- if (empty($orgType)) {
- $this->logger->debug('ModuleRegistrationService: Organisatie has no type, skipping', [
- 'moduleId' => $moduleId,
- 'organisationUuid' => $organisationUuid,
- ]);
+ $orgType = $organisatieData['type'] ?? null;
+
+ if (empty($orgType) === true) {
+ $this->logger->debug(
+ 'ModuleRegistrationService: Organisatie has no type, skipping',
+ [
+ 'moduleId' => $moduleId,
+ 'organisationUuid' => $organisationUuid,
+ ]
+ );
return;
}
@@ -111,22 +157,28 @@ public function handleModuleRegistration(object $moduleObject): void
$geregistreerdDoor = self::TYPE_MAP[$orgType] ?? null;
if ($geregistreerdDoor === null) {
- $this->logger->warning('ModuleRegistrationService: Unknown org type, cannot map geregistreerdDoor', [
- 'moduleId' => $moduleId,
- 'orgType' => $orgType,
- ]);
+ $this->logger->warning(
+ 'ModuleRegistrationService: Unknown org type, cannot map geregistreerdDoor',
+ [
+ 'moduleId' => $moduleId,
+ 'orgType' => $orgType,
+ ]
+ );
return;
}
// Check if already set correctly.
- $moduleData = $moduleObject->getObject();
+ $moduleData = $moduleObject->getObject();
$currentValue = $moduleData['geregistreerdDoor'] ?? null;
if ($currentValue === $geregistreerdDoor) {
- $this->logger->debug('ModuleRegistrationService: geregistreerdDoor already correct', [
- 'moduleId' => $moduleId,
- 'geregistreerdDoor' => $geregistreerdDoor,
- ]);
+ $this->logger->debug(
+ 'ModuleRegistrationService: geregistreerdDoor already correct',
+ [
+ 'moduleId' => $moduleId,
+ 'geregistreerdDoor' => $geregistreerdDoor,
+ ]
+ );
return;
}
@@ -142,30 +194,44 @@ public function handleModuleRegistration(object $moduleObject): void
_multitenancy: false
);
- $this->logger->info('ModuleRegistrationService: Set geregistreerdDoor on module', [
- 'moduleId' => $moduleId,
- 'orgType' => $orgType,
- 'geregistreerdDoor' => $geregistreerdDoor,
- ]);
+ $this->logger->info(
+ 'ModuleRegistrationService: Set geregistreerdDoor on module',
+ [
+ 'moduleId' => $moduleId,
+ 'orgType' => $orgType,
+ 'geregistreerdDoor' => $geregistreerdDoor,
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('ModuleRegistrationService: Failed to set geregistreerdDoor', [
- 'moduleId' => $moduleId,
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- ]);
- }
- }
+ $this->logger->error(
+ 'ModuleRegistrationService: Failed to set geregistreerdDoor',
+ [
+ 'moduleId' => $moduleId,
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]
+ );
+ }//end try
+ }//end handleModuleRegistration()
+ /**
+ * Get the object service from the DI container.
+ *
+ * @return ObjectService|null The object service or null if not available
+ */
private function getObjectService(): ?ObjectService
{
try {
return $this->container->get('OCA\OpenRegister\Service\ObjectService');
} catch (\Exception $e) {
- $this->logger->error('ModuleRegistrationService: Failed to get ObjectService', [
- 'exception' => $e->getMessage(),
- ]);
+ $this->logger->error(
+ 'ModuleRegistrationService: Failed to get ObjectService',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return null;
}
- }
-}
+ }//end getObjectService()
+}//end class
diff --git a/lib/Service/ModuleVersionService.php b/lib/Service/ModuleVersionService.php
index 2998b61e..52f26bda 100644
--- a/lib/Service/ModuleVersionService.php
+++ b/lib/Service/ModuleVersionService.php
@@ -11,7 +11,7 @@
* @author Conduction b.v.
* @copyright 2024 Conduction B.V.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT: 1.0.0
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
@@ -34,16 +34,16 @@ class ModuleVersionService
/**
* Constructor for ModuleVersionService
*
- * @param ContainerInterface $container The DI container
- * @param SettingsService $settingsService The settings service
- * @param LoggerInterface $logger The logger instance
+ * @param ContainerInterface $container The DI container
+ * @param SettingsService $settingsService The settings service
+ * @param LoggerInterface $logger The logger instance
*/
public function __construct(
private readonly ContainerInterface $container,
private readonly SettingsService $settingsService,
private readonly LoggerInterface $logger
) {
- }
+ }//end __construct()
/**
* Ensures a module has at least one version.
@@ -60,9 +60,12 @@ public function ensureDefaultVersion(object $moduleObject): void
{
$moduleUuid = $moduleObject->getUuid();
- $this->logger->info('ModuleVersionService: Checking if module has versions', [
- 'moduleUuid' => $moduleUuid,
- ]);
+ $this->logger->info(
+ 'ModuleVersionService: Checking if module has versions',
+ [
+ 'moduleUuid' => $moduleUuid,
+ ]
+ );
try {
$objectService = $this->getObjectService();
@@ -73,21 +76,24 @@ public function ensureDefaultVersion(object $moduleObject): void
// Get schema and register IDs.
$moduleVersieSchemaId = $this->settingsService->getSchemaIdForObjectType('moduleVersie');
- $voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $registerId = $voorzieningenConfig['register'] ?? null;
+ $voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
+ $registerId = $voorzieningenConfig['register'] ?? null;
if ($moduleVersieSchemaId === null || $registerId === null) {
- $this->logger->warning('ModuleVersionService: moduleVersie schema or register not configured', [
- 'moduleVersieSchemaId' => $moduleVersieSchemaId,
- 'registerId' => $registerId,
- ]);
+ $this->logger->warning(
+ 'ModuleVersionService: moduleVersie schema or register not configured',
+ [
+ 'moduleVersieSchemaId' => $moduleVersieSchemaId,
+ 'registerId' => $registerId,
+ ]
+ );
return;
}
- // Query moduleVersie objects where module == this module's UUID.
- $query = [
- '@self' => [
- 'schema' => (int) $moduleVersieSchemaId,
+ // Query moduleVersie objects where module === this module's UUID.
+ $query = [
+ '@self' => [
+ 'schema' => (int) $moduleVersieSchemaId,
'register' => (int) $registerId,
],
'module' => $moduleUuid,
@@ -98,27 +104,34 @@ public function ensureDefaultVersion(object $moduleObject): void
_multitenancy: false
);
- $versionCount = is_array($existingVersions) ? count($existingVersions) : 0;
+ if (is_array($existingVersions) === true) {
+ $versionCount = count($existingVersions);
+ } else {
+ $versionCount = 0;
+ }
if ($versionCount > 0) {
- $this->logger->info('ModuleVersionService: Module already has versions, skipping', [
- 'moduleUuid' => $moduleUuid,
- 'versionCount' => $versionCount,
- ]);
+ $this->logger->info(
+ 'ModuleVersionService: Module already has versions, skipping',
+ [
+ 'moduleUuid' => $moduleUuid,
+ 'versionCount' => $versionCount,
+ ]
+ );
return;
}
// No versions exist — create a default 1.0.0 version.
- $moduleData = $moduleObject->getObject();
- $moduleName = $moduleData['voorkeurnaam'] ?? $moduleData['naam'] ?? 'Onbekende applicatie';
+ $moduleData = $moduleObject->getObject();
+ $moduleName = $moduleData['voorkeurnaam'] ?? $moduleData['naam'] ?? 'Onbekende applicatie';
$moduleDescription = $moduleData['beschrijvingKort'] ?? '';
$versionData = [
- 'module' => $moduleUuid,
- 'versie' => '1.0.0',
+ 'module' => $moduleUuid,
+ 'versie' => '1.0.0',
'beschrijvingKort' => $moduleDescription,
'beschrijvingLang' => '',
- 'status' => 'in gebruik',
+ 'status' => 'in gebruik',
];
$savedVersion = $objectService->saveObject(
@@ -129,20 +142,26 @@ public function ensureDefaultVersion(object $moduleObject): void
_multitenancy: false
);
- $this->logger->info('ModuleVersionService: Created default version 1.0.0', [
- 'moduleUuid' => $moduleUuid,
- 'moduleName' => $moduleName,
- 'versionUuid' => $savedVersion->getUuid(),
- ]);
+ $this->logger->info(
+ 'ModuleVersionService: Created default version 1.0.0',
+ [
+ 'moduleUuid' => $moduleUuid,
+ 'moduleName' => $moduleName,
+ 'versionUuid' => $savedVersion->getUuid(),
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('ModuleVersionService: Failed to ensure default version', [
- 'moduleUuid' => $moduleUuid,
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- ]);
- }
- }
+ $this->logger->error(
+ 'ModuleVersionService: Failed to ensure default version',
+ [
+ 'moduleUuid' => $moduleUuid,
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]
+ );
+ }//end try
+ }//end ensureDefaultVersion()
/**
* Get the object service from the DI container.
@@ -154,10 +173,13 @@ private function getObjectService(): ?ObjectService
try {
return $this->container->get('OCA\OpenRegister\Service\ObjectService');
} catch (\Exception $e) {
- $this->logger->error('ModuleVersionService: Failed to get ObjectService', [
- 'exception' => $e->getMessage(),
- ]);
+ $this->logger->error(
+ 'ModuleVersionService: Failed to get ObjectService',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return null;
}
- }
-}
+ }//end getObjectService()
+}//end class
diff --git a/lib/Service/OrganisatieService.php b/lib/Service/OrganisatieService.php
index 0d4615b4..d8127569 100644
--- a/lib/Service/OrganisatieService.php
+++ b/lib/Service/OrganisatieService.php
@@ -1,17 +1,17 @@
+ * @category Service
+ * @package OCA\SoftwareCatalog\Service
+ * @author Conduction b.v.
* @copyright 2024 Conduction B.V.
- * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
- * @link https://github.com/ConductionNL/SoftwareCatalog
+ * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
*/
declare(strict_types=1);
@@ -26,7 +26,7 @@
use OCP\IAppConfig;
/**
- * Service for handling organization-specific operations
+ * Service for handling organization-specific operations.
*
* This service provides functionality for organization entity creation,
* status management, and integration with OpenRegister.
@@ -35,19 +35,21 @@
* @package OCA\SoftwareCatalog\Service
* @author Conduction b.v.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
class OrganisatieService
{
/**
- * OrganisatieService constructor
+ * OrganisatieService constructor.
*
- * @param OrganizationHandler $organizationHandler Organization handler
- * @param LoggerInterface $logger Logger interface
- * @param ContainerInterface $container Container interface
- * @param IAppManager $appManager App manager
- * @param IAppConfig $config Configuration service
+ * @param OrganizationHandler $organizationHandler Organization handler
+ * @param LoggerInterface $logger Logger interface
+ * @param ContainerInterface $container Container interface
+ * @param IAppManager $appManager App manager
+ * @param IAppConfig $config Configuration service
+ * @param IUserManager $userManager User manager service
+ * @param SymfonyEmailService $emailService Email service
*/
public function __construct(
private readonly OrganizationHandler $organizationHandler,
@@ -58,10 +60,10 @@ public function __construct(
private readonly IUserManager $userManager,
private readonly SymfonyEmailService $emailService,
) {
- }
+ }//end __construct()
/**
- * Creates an organization entity in OpenRegister
+ * Creates an organization entity in OpenRegister.
*
* @param array $objectData The organization object data
*
@@ -71,50 +73,61 @@ public function createOrganisationInOpenRegister(array $objectData): ?object
{
try {
$organizationUuid = $objectData['id'] ?? null;
- if (!$organizationUuid) {
+ if (empty($organizationUuid) === true) {
$this->logger->error('OrganisatieService: No organization UUID provided for creation');
return null;
}
- $this->logger->info('OrganisatieService: Creating organization entity in OpenRegister', [
- 'organizationUuid' => $organizationUuid,
- 'naam' => $objectData['naam'] ?? 'Unknown'
- ]);
+ $this->logger->info(
+ 'OrganisatieService: Creating organization entity in OpenRegister',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'naam' => $objectData['naam'] ?? 'Unknown',
+ ]
+ );
- // Map the data for OpenRegister
- $mappedData = $this->mapOrganizationDataForOpenRegister($objectData);
+ // Map the data for OpenRegister.
+ $mappedData = $this->mapOrganizationDataForOpenRegister(objectData: $objectData);
- // Get organisation service
+ // Get organisation service.
$organisationService = $this->getOrganisationService();
- if (!$organisationService) {
+ if ($organisationService === null) {
$this->logger->error('OrganisatieService: OrganisationService not available');
return null;
}
- // Create the organization entity
- $organisationEntity = $this->createOrganisationEntityInternal($organisationService, $mappedData, $organizationUuid);
-
- if ($organisationEntity) {
- $this->logger->info('OrganisatieService: Successfully created organization entity', [
- 'organizationUuid' => $organizationUuid,
- 'entityId' => $organisationEntity->getId()
- ]);
+ // Create the organization entity.
+ $organisationEntity = $this->createOrganisationEntityInternal(
+ organisationService: $organisationService,
+ mappedData: $mappedData,
+ organizationUuid: $organizationUuid
+ );
+
+ if ($organisationEntity !== null) {
+ $this->logger->info(
+ 'OrganisatieService: Successfully created organization entity',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'entityId' => $organisationEntity->getId(),
+ ]
+ );
}
return $organisationEntity;
-
} catch (\Exception $e) {
- $this->logger->error('OrganisatieService: Error creating organization entity', [
- 'error' => $e->getMessage(),
- 'objectData' => $objectData,
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'OrganisatieService: Error creating organization entity',
+ [
+ 'error' => $e->getMessage(),
+ 'organizationUuid' => $objectData['id'] ?? 'unknown',
+ ]
+ );
return null;
- }
- }
+ }//end try
+ }//end createOrganisationInOpenRegister()
/**
- * Updates organization entity status based on object data
+ * Updates organization entity status based on object data.
*
* @param string $organizationUuid The organization UUID
* @param array $objectData The organization object data
@@ -124,64 +137,65 @@ public function createOrganisationInOpenRegister(array $objectData): ?object
public function updateOrganizationStatus(string $organizationUuid, array $objectData): bool
{
try {
- $this->logger->info('OrganisatieService: Updating organization status', [
- 'organizationUuid' => $organizationUuid,
- 'beoordeling' => $objectData['beoordeling'] ?? 'unknown'
- ]);
-
- // Get the organization entity
+ $this->logger->info(
+ 'OrganisatieService: Updating organization status',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'beoordeling' => $objectData['beoordeling'] ?? 'unknown',
+ ]
+ );
+
+ // Get the organization entity.
$organisationMapper = $this->container->get('OCA\OpenRegister\Db\OrganisationMapper');
$organisationEntity = $organisationMapper->findByUuid($organizationUuid);
- // Map status from SoftwareCatalog to OpenRegister
- $active = $this->mapStatus($objectData['beoordeling'] ?? 'actief');
+ // Map status from SoftwareCatalog to OpenRegister.
+ $active = $this->mapStatus(status: $objectData['beoordeling'] ?? 'actief');
- // Update the entity
+ // Update the entity.
$organisationEntity->setActive($active);
$organisationMapper->save($organisationEntity);
- $this->logger->info('OrganisatieService: Successfully updated organization status', [
- 'organizationUuid' => $organizationUuid,
- 'active' => $active
- ]);
+ $this->logger->info(
+ 'OrganisatieService: Successfully updated organization status',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'active' => $active,
+ ]
+ );
return true;
-
} catch (\Exception $e) {
- $this->logger->error('OrganisatieService: Failed to update organization status', [
- 'organizationUuid' => $organizationUuid,
- 'error' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'OrganisatieService: Failed to update organization status',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'error' => $e->getMessage(),
+ ]
+ );
return false;
- }
- }
+ }//end try
+ }//end updateOrganizationStatus()
/**
- * Gets the OrganisationService instance
+ * Gets the OrganisationService instance.
*
- * @return \OCA\OpenRegister\Service\OrganisationService|null
+ * @return \OCA\OpenRegister\Service\OrganisationService|null The service instance or null if unavailable
*/
private function getOrganisationService(): ?\OCA\OpenRegister\Service\OrganisationService
{
- if (!$this->appManager->isEnabledForUser('openregister')) {
+ if ($this->appManager->isEnabledForUser('openregister') === false) {
return null;
}
try {
return $this->container->get('OCA\OpenRegister\Service\OrganisationService');
} catch (\Exception $e) {
- $this->logger->error('OrganisatieService: Failed to get OrganisationService: ' . $e->getMessage());
+ $this->logger->error('OrganisatieService: Failed to get OrganisationService: '.$e->getMessage());
return null;
}
- }
+ }//end getOrganisationService()
- /**
- * Maps organization data for OpenRegister format
- *
- * @param array $objectData The organization object data
- *
- * @return array The mapped data for OpenRegister
- */
/**
* Maps organization data from Software Catalog object to OpenRegister format.
*
@@ -191,27 +205,27 @@ private function getOrganisationService(): ?\OCA\OpenRegister\Service\Organisati
*/
private function mapOrganizationDataForOpenRegister(array $objectData): array
{
- // Get the organization name - try 'naam' first, then 'name', then use UUID as fallback
+ // Get the organization name - try 'naam' first, then 'name', then use UUID as fallback.
$naam = $objectData['naam'] ?? $objectData['name'] ?? null;
-
- // If still no name, create a unique one using the ID to avoid slug conflicts
- if (empty($naam) || $naam === 'Unknown') {
- $orgId = $objectData['id'] ?? uniqid('org-');
- $naam = 'Organisation ' . substr($orgId, 0, 8);
+
+ // If still no name, create a unique one using the ID to avoid slug conflicts.
+ if (empty($naam) === true || $naam === 'Unknown') {
+ $orgId = $objectData['id'] ?? uniqid(prefix: 'org-');
+ $naam = 'Organisation '.substr($orgId, 0, 8);
}
-
+
return [
- 'naam' => $naam,
- 'type' => $objectData['type'] ?? '',
- 'website' => $objectData['website'] ?? '',
- 'active' => $this->mapStatus($objectData['status'] ?? $objectData['beoordeling'] ?? 'actief'),
+ 'naam' => $naam,
+ 'type' => $objectData['type'] ?? '',
+ 'website' => $objectData['website'] ?? '',
+ 'active' => $this->mapStatus(status: $objectData['status'] ?? $objectData['beoordeling'] ?? 'actief'),
'contactpersonen' => $objectData['contactpersonen'] ?? [],
- 'deelnemers' => $objectData['deelnemers'] ?? []
+ 'deelnemers' => $objectData['deelnemers'] ?? [],
];
- }
+ }//end mapOrganizationDataForOpenRegister()
/**
- * Maps status from Software Catalog to OpenRegister format
+ * Maps status from Software Catalog to OpenRegister format.
*
* @param string $status The status from Software Catalog
*
@@ -224,12 +238,13 @@ private function mapStatus(string $status): bool
return match ($normalizedStatus) {
'actief', 'active' => true,
'inactief', 'inactive', 'deactief' => false,
- default => true // Default to active for unknown statuses
+ // Default to active for unknown statuses.
+ default => true
};
- }
+ }//end mapStatus()
/**
- * Internal method to create organization entity
+ * Internal method to create organization entity.
*
* HOTFIX: Parent organisation setting has been disabled due to RBAC issues.
* Previously, new organisations were automatically set as children of the active organisation,
@@ -247,38 +262,42 @@ private function createOrganisationEntityInternal(
array $mappedData,
string $organizationUuid
): \OCA\OpenRegister\Db\Organisation {
-
// HOTFIX: Commented out automatic parent organisation setting due to RBAC issues.
- // When child organisations are created, the parent relationship causes permission problems
- // where users cannot access the newly created organisations due to hierarchical RBAC filtering.
+ // When child organisations are created, the parent relationship causes permission problems.
+ // Where users cannot access the newly created organisations due to hierarchical RBAC filtering.
// TODO: Investigate and fix RBAC logic to properly handle parent-child organisation relationships.
- // $parentOrganisationUuid = $this->getActiveOrganisationUuid($organisationService);
-
- $this->logger->info('OrganisatieService: Creating organisation entity', [
- 'uuid' => $organizationUuid,
- 'name' => $mappedData['naam'],
- 'active' => $mappedData['active'],
- // 'parentOrganisation' => $parentOrganisationUuid // HOTFIX: Commented out
- ]);
+ // Disabled: $parentOrganisationUuid = $this->getActiveOrganisationUuid(organisationService: $organisationService).
+ $this->logger->info(
+ 'OrganisatieService: Creating organisation entity',
+ [
+ 'uuid' => $organizationUuid,
+ 'name' => $mappedData['naam'],
+ 'active' => $mappedData['active'],
+ // 'parentOrganisation' => $parentOrganisationUuid // HOTFIX: Commented out.
+ ]
+ );
// Use OrganisationService to create the entity.
// NOTE: Don't call save() afterwards as it causes UUID/ID issues in the mapper.
$organisationEntity = $organisationService->createOrganisation(
- $mappedData['naam'], // name (string)
- $mappedData['type'] ?? '', // description (string)
- false, // addCurrentUser (bool) - don't auto-add current user
- $organizationUuid // uuid (string)
+ name: (string) $mappedData['naam'],
+ description: (string) ($mappedData['type'] ?? ''),
+ addCurrentUser: false,
+ uuid: $organizationUuid
);
- $this->logger->info('OrganisatieService: Organisation entity created successfully', [
- 'uuid' => $organizationUuid,
- 'entityId' => $organisationEntity->getId(),
- 'active' => $organisationEntity->getActive(),
- 'parent' => $organisationEntity->getParent()
- ]);
+ $this->logger->info(
+ 'OrganisatieService: Organisation entity created successfully',
+ [
+ 'uuid' => $organizationUuid,
+ 'entityId' => $organisationEntity->getId(),
+ 'active' => $organisationEntity->getActive(),
+ 'parent' => $organisationEntity->getParent(),
+ ]
+ );
return $organisationEntity;
- }
+ }//end createOrganisationEntityInternal()
/**
* Get the currently active organisation UUID from the user session.
@@ -297,16 +316,19 @@ private function getActiveOrganisationUuid(
return $activeOrganisation->getUuid();
}
} catch (\Exception $e) {
- $this->logger->debug('OrganisatieService: Could not get active organisation', [
- 'error' => $e->getMessage()
- ]);
+ $this->logger->debug(
+ 'OrganisatieService: Could not get active organisation',
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
}
return null;
- }
+ }//end getActiveOrganisationUuid()
/**
- * Adds users to organization entity
+ * Adds users to organization entity.
*
* @param string $organizationUuid The organization UUID
* @param array $usernames Array of usernames to add
@@ -316,59 +338,73 @@ private function getActiveOrganisationUuid(
public function addUsersToOrganization(string $organizationUuid, array $usernames): bool
{
try {
- $this->logger->info('OrganisatieService: Adding users to organization', [
- 'organizationUuid' => $organizationUuid,
- 'userCount' => count($usernames)
- ]);
-
- // Get the organization entity
+ $this->logger->info(
+ 'OrganisatieService: Adding users to organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'userCount' => count($usernames),
+ ]
+ );
+
+ // Get the organization entity.
$organisationMapper = $this->container->get('OCA\OpenRegister\Db\OrganisationMapper');
$organisationEntity = $organisationMapper->findByUuid($organizationUuid);
- // Get current users and merge with new ones
+ // Get current users and merge with new ones.
$currentUsers = $organisationEntity->getUsers() ?? [];
- $allUsers = array_unique(array_merge($currentUsers, $usernames));
+ $allUsers = array_unique(array_merge($currentUsers, $usernames));
- foreach($usernames as $username) {
+ foreach ($usernames as $username) {
$user = $this->userManager->get($username);
$userData = [
'username' => $user->getUID(),
- 'email' => $user->getEMailAddress(),
- 'name' => $user->getDisplayName(),
+ 'email' => $user->getEMailAddress(),
+ 'name' => $user->getDisplayName(),
];
- $this->emailService->sendUserUpdateEmail($userData, $organisationEntity->jsonSerialize());
+ $this->emailService->sendUserUpdateEmail(
+ user: $userData,
+ organization: $organisationEntity->jsonSerialize()
+ );
}
- // Update the entity
+ // Update the entity.
$organisationEntity->setUsers($allUsers);
$organisationMapper->save($organisationEntity);
- $this->logger->info('OrganisatieService: Successfully added users to organization', [
- 'organizationUuid' => $organizationUuid,
- 'totalUsers' => count($allUsers),
- 'addedUsers' => array_diff($allUsers, $currentUsers)
- ]);
+ $this->logger->info(
+ 'OrganisatieService: Successfully added users to organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'totalUsers' => count($allUsers),
+ 'addedUsers' => array_diff($allUsers, $currentUsers),
+ ]
+ );
return true;
-
} catch (\Exception $e) {
- $this->logger->error('OrganisatieService: Failed to add users to organization', [
- 'organizationUuid' => $organizationUuid,
- 'error' => $e->getMessage()
- ]);
- // Log detailed error information using PSR-3 logger
- $this->logger->error('OrganisatieService: Exception details', [
- 'message' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'OrganisatieService: Failed to add users to organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ // Log detailed error information using PSR-3 logger.
+ $this->logger->error(
+ 'OrganisatieService: Exception details',
+ [
+ 'message' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return false;
- }
- }
+ }//end try
+ }//end addUsersToOrganization()
/**
- * Gets admin group usernames
+ * Gets admin group usernames.
*
* @return array Array of admin usernames
*/
@@ -376,23 +412,27 @@ public function getAdminGroupUsernames(): array
{
try {
$groupManager = $this->container->get('OCP\IGroupManager');
- $adminGroup = $groupManager->get('admin');
+ $adminGroup = $groupManager->get('admin');
- if ($adminGroup) {
- $adminUsers = $adminGroup->getUsers();
+ if ($adminGroup !== null) {
+ $adminUsers = $adminGroup->getUsers();
$adminUsernames = [];
foreach ($adminUsers as $user) {
$adminUsernames[] = $user->getUID();
}
+
return $adminUsernames;
}
return [];
} catch (\Exception $e) {
- $this->logger->error('OrganisatieService: Failed to get admin users', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'OrganisatieService: Failed to get admin users',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return [];
- }
- }
-}
+ }//end try
+ }//end getAdminGroupUsernames()
+}//end class
diff --git a/lib/Service/OrganizationSyncService.php b/lib/Service/OrganizationSyncService.php
index 1fefadae..a82cd002 100644
--- a/lib/Service/OrganizationSyncService.php
+++ b/lib/Service/OrganizationSyncService.php
@@ -5,13 +5,12 @@
* This file contains the service class for synchronizing organizations and contact persons
* between SoftwareCatalog objects and OpenRegister entities.
*
- * @category Service
- * @package OCA\SoftwareCatalog\Service
- * @author Conduction b.v.
+ * @category Service
+ * @package OCA\SoftwareCatalog\Service
+ * @author Conduction b.v.
* @copyright 2024 Conduction B.V.
- * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
- * @link https://github.com/ConductionNL/SoftwareCatalog
+ * @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
+ * @link https://github.com/ConductionNL/SoftwareCatalog
*/
declare(strict_types=1);
@@ -30,7 +29,7 @@
use Psr\Log\LoggerInterface;
/**
- * Service for synchronizing organizations and contact persons
+ * Service for synchronizing organizations and contact persons.
*
* This service provides comprehensive synchronization between SoftwareCatalog objects
* and OpenRegister entities, ensuring data consistency and proper user management.
@@ -39,11 +38,11 @@
* @package OCA\SoftwareCatalog\Service
* @author Conduction b.v.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
class OrganizationSyncService
{
+
/**
* OrganisatieService instance
*
@@ -89,11 +88,14 @@ class OrganizationSyncService
/**
* Constructor for OrganizationSyncService
*
- * @param OrganisatieService $organisatieService The organization service
- * @param ContactpersoonService $contactpersoonService The contact person service
- * @param SymfonyEmailService $emailService The email service
- * @param IAppConfig $config The configuration service
- * @param LoggerInterface $logger The logger instance
+ * @param OrganisatieService $organisatieService The organization service.
+ * @param ContactpersoonService $contactpersoonService The contact person service.
+ * @param SymfonyEmailService $emailService The email service.
+ * @param IAppConfig $config The configuration service.
+ * @param LoggerInterface $logger The logger instance.
+ * @param SettingsService $settingsService The settings service.
+ * @param IDBConnection $db The database connection.
+ * @param ContactPersonHandler $contactpersonHandler The contact person handler.
*/
public function __construct(
OrganisatieService $organisatieService,
@@ -105,13 +107,14 @@ public function __construct(
private IDBConnection $db,
private readonly ContactPersonHandler $contactpersonHandler,
) {
- $this->organisatieService = $organisatieService;
+ $this->organisatieService = $organisatieService;
$this->contactpersoonService = $contactpersoonService;
- $this->emailService = $emailService;
- $this->config = $config;
- $this->logger = $logger;
+ $this->emailService = $emailService;
+ $this->config = $config;
+ $this->logger = $logger;
$this->settingsService = $settingsService;
- }
+
+ }//end __construct()
/**
* Create a database-agnostic JSON extraction expression
@@ -126,22 +129,28 @@ public function __construct(
*/
private function jsonExtract(string $column, string $path): string
{
- $platform = $this->db->getDatabasePlatform();
+ $platform = $this->db->getDatabasePlatform();
$isPostgres = $platform->getName() === 'postgresql';
- // Normalize path - remove '$.' prefix if present for PostgreSQL
+ // Normalize path - remove '$.' prefix if present for PostgreSQL.
$cleanPath = ltrim($path, '$.');
- if ($isPostgres) {
- // PostgreSQL: Use ->> operator for text extraction
- // Cast to json first if needed, then extract
+ if (empty($isPostgres) === false) {
+ // PostgreSQL: Use ->> operator for text extraction.
+ // Cast to json first if needed, then extract.
return "({$column}::json->>'{$cleanPath}')";
}
- // MySQL/MariaDB: Use json_unquote(json_extract())
- $jsonPath = str_starts_with($path, '$.') ? $path : '$.'. $path;
+ // MySQL/MariaDB: Use json_unquote(json_extract()).
+ if (str_starts_with($path, '$.') === true) {
+ $jsonPath = $path;
+ } else {
+ $jsonPath = '$.'.$path;
+ }
+
return "json_unquote(json_extract({$column}, '{$jsonPath}'))";
- }
+
+ }//end jsonExtract()
/**
* Create a database-agnostic JSON contains expression
@@ -155,237 +164,344 @@ private function jsonExtract(string $column, string $path): string
*/
private function jsonContains(string $column, string $value): string
{
- $platform = $this->db->getDatabasePlatform();
+ $platform = $this->db->getDatabasePlatform();
$isPostgres = $platform->getName() === 'postgresql';
- if ($isPostgres) {
- // PostgreSQL: Use @> operator with jsonb
+ if (empty($isPostgres) === false) {
+ // PostgreSQL: Use @> operator with jsonb.
return "({$column}::jsonb @> '\"{$value}\"'::jsonb)";
}
- // MySQL/MariaDB: Use JSON_CONTAINS
+ // MySQL/MariaDB: Use JSON_CONTAINS.
return "json_contains({$column}, '\"{$value}\"')";
- }
- public function performOrganizationsSync(int $batchSize = 50, int $maxExecutionSeconds = 45): array
+ }//end jsonContains()
+
+ /**
+ * Perform synchronization of organizations.
+ *
+ * @param int $batchSize The batch size for processing.
+ * @param int $maxExecutionSeconds The maximum execution time in seconds.
+ *
+ * @return array The sync statistics.
+ */
+ public function performOrganizationsSync(int $batchSize=50, int $maxExecutionSeconds=45): array
{
- // Check configuration
+ // Check configuration.
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $register = $voorzieningenConfig['register'] ?? '';
- $organizationSchema = $voorzieningenConfig['organisatie_schema'] ?? '';
-// $contactSchema = $voorzieningenConfig['contactpersoon_schema'] ?? '';
+ $register = ($voorzieningenConfig['register'] ?? '');
+ $organizationSchema = ($voorzieningenConfig['organisatie_schema'] ?? '');
$startTime = time();
- $stats = [
+ $stats = [
'organizationsProcessed' => 0,
- 'entitiesCreated' => 0,
- 'entitiesUpdated' => 0,
- 'contactPersonsProcessed' => 0,
- 'usersCreated' => 0,
- 'usersUpdated' => 0,
- 'errors' => [],
- 'batchSize' => $batchSize,
- 'maxExecutionSeconds' => $maxExecutionSeconds,
- 'timeoutReached' => false,
- 'startTime' => date('Y-m-d H:i:s'),
- 'endTime' => null,
- 'duration' => null
+ 'entitiesCreated' => 0,
+ 'entitiesUpdated' => 0,
+ 'errors' => [],
+ 'batchSize' => $batchSize,
+ 'maxExecutionSeconds' => $maxExecutionSeconds,
+ 'timeoutReached' => false,
+ 'totalRemaining' => 0,
+ 'startTime' => date('Y-m-d H:i:s'),
+ 'endTime' => null,
+ 'duration' => null,
];
+ if (empty($register) === true || empty($organizationSchema) === true) {
+ $this->logger->warning('OrganizationSync: voorzieningen config missing register or organisatie_schema');
+ return $stats;
+ }
+
+ // Build table name dynamically from config (environment-agnostic).
+ // Objects live in per-schema MagicMapper tables, NOT in openregister_objects.
+ $magicTableName = 'openregister_table_'.$register.'_'.$organizationSchema;
+
+ // Count total remaining orgs without entities for progress logging.
+ $countQb = $this->db->getQueryBuilder();
+ $totalRemaining = (int) $countQb->select($countQb->createFunction('COUNT(*) as cnt'))
+ ->from($magicTableName, 'o')
+ ->leftJoin('o', 'openregister_organisations', 'org', 'o._uuid = org.uuid')
+ ->where('org.uuid IS NULL')
+ ->andWhere(
+ $countQb->createFunction(
+ 'LOWER(o.status) = '.$countQb->createNamedParameter('actief')
+ )
+ )
+ ->execute()->fetchOne();
+
+ $stats['totalRemaining'] = $totalRemaining;
+
+ if ($totalRemaining === 0) {
+ $this->logger->debug('OrganizationSync: all active orgs have entities, nothing to do');
+ return $stats;
+ }
+
+ $this->logger->info('OrganizationSync: '.$totalRemaining.' active orgs without entity, processing batch of '.$batchSize);
+
+ // Query active orgs that don't have an OpenRegister organisation entity yet.
$qb = $this->db->getQueryBuilder();
+ $qb->select('o._uuid as uuid', 'o.status')
+ ->from($magicTableName, 'o')
+ ->leftJoin('o', 'openregister_organisations', 'org', 'o._uuid = org.uuid')
+ ->where('org.uuid IS NULL')
+ ->andWhere(
+ $qb->createFunction(
+ 'LOWER(o.status) = '.$qb->createNamedParameter('actief')
+ )
+ )
+ ->orderBy('o._updated', 'ASC')
+ ->setMaxResults($batchSize);
+
+ $rows = $qb->execute()->fetchAll();
+
+ $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
+ if ($objectService instanceof ObjectService === false) {
+ $this->logger->error('OrganizationSync: could not resolve ObjectService');
+ return $stats;
+ }
- // Query to find organizations that need syncing.
- // Note: The 'active' column was removed as it doesn't exist in the openregister_organisations table.
- // Now syncs all non-Concept organizations that either don't have an organisation entity yet,
- // or need to be re-synced based on their updated timestamp.
- $statusExtract = $this->jsonExtract('o.object', '$.status');
- $qb->select('o.uuid', $qb->createFunction("{$statusExtract} as status"), 'o2.uuid as oreg_uuid')
- ->from(from: 'openregister_objects', alias: 'o')
- ->leftJoin(fromAlias:'o', join: 'openregister_organisations', alias: 'o2', condition: 'o.uuid = o2.uuid')
- ->where($qb->expr()->eq('o.schema', $qb->createNamedParameter($organizationSchema)))
- ->andWhere($qb->expr()->eq('o.register', $qb->createNamedParameter($register)))
- ->andWhere($qb->expr()->neq($qb->createFunction("LOWER({$statusExtract})"), $qb->createNamedParameter('concept')))
- ->orderBy('o.updated', 'ASC') // Process oldest first for consistency.
- ->setMaxResults($batchSize); // Limit batch size.
-
- $sql = $qb->getSQL();
- $objects = $qb->execute()->fetchAll();
- $orgs = [];
-
-
- foreach($objects as $object) {
- // Check if we're approaching the time limit
- if (time() - $startTime >= $maxExecutionSeconds) {
+ foreach ($rows as $row) {
+ if ((time() - $startTime) >= $maxExecutionSeconds) {
$stats['timeoutReached'] = true;
- $this->logger->info('OrganizationSyncService: Execution time limit reached', [
- 'processedCount' => $stats['organizationsProcessed'],
- 'executionTime' => time() - $startTime,
- 'maxExecutionSeconds' => $maxExecutionSeconds,
- 'trigger' => 'batch_processing'
- ]);
+ $this->logger->info(
+ 'OrganizationSync: time limit reached',
+ [
+ 'processed' => $stats['organizationsProcessed'],
+ 'elapsed' => (time() - $startTime),
+ ]
+ );
break;
}
- $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
- if($objectService instanceOf ObjectService === false) {
- return [];
- }
-
- $object = $objectService->find(id: $object['uuid'], register: $register, schema: $organizationSchema);
+ try {
+ $object = $objectService->find(
+ id: $row['uuid'],
+ register: $register,
+ schema: $organizationSchema,
+ _rbac: false,
+ _multitenancy: false
+ );
- $org = $this->ensureOrganisationEntity($object,$stats);
+ $this->ensureOrganisationEntity(organisatieObject: $object, stats: $stats, sendEmails: false);
+ $stats['organizationsProcessed']++;
+ } catch (\Exception $e) {
+ $stats['errors'][] = $row['uuid'].': '.$e->getMessage();
+ $this->logger->error(
+ 'OrganizationSync: failed to process org '.$row['uuid'],
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }
+ }//end foreach
+ $stats['endTime'] = date('Y-m-d H:i:s');
+ $stats['duration'] = (time() - $startTime);
- }
+ $this->logger->info(
+ 'OrganizationSync: batch complete',
+ [
+ 'processed' => $stats['organizationsProcessed'],
+ 'created' => $stats['entitiesCreated'],
+ 'remaining' => ($totalRemaining - $stats['organizationsProcessed']),
+ 'duration' => $stats['duration'].'s',
+ ]
+ );
return $stats;
- }
- public function performContactSync(int $batchSize = 100, int $maxExecutionSeconds = 30) :array
+ }//end performOrganizationsSync()
+
+ /**
+ * Perform synchronization of contact persons.
+ *
+ * @param int $batchSize The batch size for processing.
+ * @param int $maxExecutionSeconds The maximum execution time in seconds.
+ *
+ * @return array The sync statistics.
+ */
+ public function performContactSync(int $batchSize=100, int $maxExecutionSeconds=30): array
{
- // Check configuration
+ // Check configuration.
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $register = $voorzieningenConfig['register'] ?? '';
-// $organizationSchema = $voorzieningenConfig['organisatie_schema'] ?? '';
- $contactSchema = $voorzieningenConfig['contactpersoon_schema'] ?? '';
+ $register = ($voorzieningenConfig['register'] ?? '');
+ $contactSchema = ($voorzieningenConfig['contactpersoon_schema'] ?? '');
$startTime = time();
- $stats = [
- 'organizationsProcessed' => 0,
- 'entitiesCreated' => 0,
- 'entitiesUpdated' => 0,
+ $stats = [
'contactPersonsProcessed' => 0,
- 'usersCreated' => 0,
- 'usersUpdated' => 0,
- 'errors' => [],
- 'batchSize' => $batchSize,
- 'maxExecutionSeconds' => $maxExecutionSeconds,
- 'timeoutReached' => false,
- 'startTime' => date('Y-m-d H:i:s'),
- 'endTime' => null,
- 'duration' => null
+ 'errors' => [],
+ 'batchSize' => $batchSize,
+ 'maxExecutionSeconds' => $maxExecutionSeconds,
+ 'timeoutReached' => false,
+ 'totalRemaining' => 0,
+ 'startTime' => date('Y-m-d H:i:s'),
+ 'endTime' => null,
+ 'duration' => null,
];
- $qb = $this->db->getQueryBuilder();
+ if (empty($register) === true || empty($contactSchema) === true) {
+ $this->logger->warning('ContactSync: voorzieningen config missing register or contactpersoon_schema');
+ return $stats;
+ }
+
+ // Query per-schema magic table directly (NOT the empty openregister_objects blob table).
+ $contactTableName = 'openregister_table_'.$register.'_'.$contactSchema;
- $emailExtract = $this->jsonExtract('o.object', '$.e-mailadres');
- $usernameExtract = $this->jsonExtract('o.object', '$.username');
- $organisatieExtract = $this->jsonExtract('o.object', '$.organisatie');
-
- $qb->select(
- 'o.uuid',
- 'a.uid',
- $qb->createFunction("{$emailExtract} as email"),
- $qb->createFunction("{$usernameExtract} as username"),
- 'oo.uuid as organisation'
- )
- ->from('openregister_objects', 'o')
- ->leftJoin(
- fromAlias: 'o',
- join: 'accounts_data',
- alias: 'a',
- condition: "{$emailExtract} = a.value")
- ->leftJoin(
- fromAlias: 'o',
- join: 'openregister_organisations',
- alias: 'oo',
- condition: "oo.uuid = {$organisatieExtract}"
+ // Find contacts without a username that DO have a matching Nextcloud account (by email).
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('o._uuid as uuid', 'o.e_mailadres as email', 'o.organisatie', 'a.uid')
+ ->from($contactTableName, 'o')
+ ->innerJoin('o', 'accounts_data', 'a', 'o.e_mailadres = a.value')
+ ->where(
+ $qb->createFunction(
+ '(o.username IS NULL OR o.username = '.$qb->createNamedParameter('').')'
+ )
)
- ->where($qb->expr()->eq('o.register', $qb->createNamedParameter($register)))
- ->andWhere($qb->expr()->eq('o.schema', $qb->createNamedParameter($contactSchema)))
- ->andWhere($qb->expr()->isNull($qb->createFunction($usernameExtract)))
- ->orderBy('o.updated', 'ASC') // Process oldest first
- ->setMaxResults($batchSize); // Limit batch size
+ ->andWhere($qb->createFunction('o.organisatie IS NOT NULL'))
+ ->orderBy('o._updated', 'ASC')
+ ->setMaxResults($batchSize);
$contacts = $qb->execute()->fetchAll();
+ $stats['totalRemaining'] = count($contacts);
+
+ if (empty($contacts) === true) {
+ $this->logger->debug('ContactSync: no contacts to sync');
+ return $stats;
+ }
+
+ $this->logger->info('ContactSync: processing '.count($contacts).' contacts with existing NC accounts');
+
+ $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
+ if ($objectService instanceof ObjectService === false) {
+ $this->logger->error('ContactSync: could not resolve ObjectService');
+ return $stats;
+ }
foreach ($contacts as $contact) {
- // Check if we're approaching the time limit
- if (time() - $startTime >= $maxExecutionSeconds) {
+ if ((time() - $startTime) >= $maxExecutionSeconds) {
$stats['timeoutReached'] = true;
- $this->logger->info('OrganizationSyncService: Contact sync time limit reached', [
- 'contactsProcessed' => $stats['contactPersonsProcessed'],
- 'executionTime' => time() - $startTime,
- 'maxExecutionSeconds' => $maxExecutionSeconds,
- 'trigger' => 'batch_processing'
- ]);
+ $this->logger->info(
+ 'ContactSync: time limit reached',
+ [
+ 'processed' => $stats['contactPersonsProcessed'],
+ ]
+ );
break;
}
- $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
- $contactEntity = $objectService->find(id: $contact['uuid'], register: $register, schema: $contactSchema);
- $contactEntityObject = $contactEntity->getObject();
-
- if ($contact['organisation'] === null) {
- continue;
- }
-
- $contactEntityObject['username'] = $contact['uid'];
+ try {
+ $contactEntity = $objectService->find(
+ id: $contact['uuid'],
+ register: $register,
+ schema: $contactSchema,
+ _rbac: false
+ );
+ $contactEntityObject = $contactEntity->getObject();
+ $contactEntityObject['username'] = $contact['uid'];
- if ($contact['uid'] === null) {
- // Skip user creation in cron sync — users should be created manually
- // via the convert-to-user endpoint or automatically via event listeners.
- $this->logger->debug('Skipping user creation in cron sync — use convert-to-user endpoint or event listener', [
- 'app' => 'softwarecatalog',
- 'contactId' => $contactEntity->getId()
- ]);
- continue;
- }
+ // Temporarily remove organisatie field to avoid validation error.
+ // (schema expects object type but field stores a UUID string).
+ $savedOrganisatie = $contactEntityObject['organisatie'] ?? null;
+ unset($contactEntityObject['organisatie']);
- // Remove organisatie field to avoid validation error
- // (it's stored as UUID string but schema expects object type)
- unset($contactEntityObject['organisatie']);
+ $contactEntity->setObject($contactEntityObject);
+ $objectService->saveObject(
+ object: $contactEntity,
+ register: $register,
+ schema: $contactSchema,
+ _rbac: false,
+ _multitenancy: false
+ );
- $contactEntity->setObject($contactEntityObject);
- $objectService->saveObject(object: $contactEntity, register: $register, schema: $contactSchema, _rbac: false, _multitenancy: false);
+ // Restore the organisatie field so the link is preserved.
+ if ($savedOrganisatie !== null) {
+ $restoredData = $contactEntity->getObject();
+ $restoredData['organisatie'] = $savedOrganisatie;
+ $contactEntity->setObject($restoredData);
+ $objectMapper = \OC::$server->get('OCA\OpenRegister\Db\ObjectEntityMapper');
+ $objectMapper->update($contactEntity);
+ }
- $stats['contactPersonsProcessed']++;
- }
+ $stats['contactPersonsProcessed']++;
+ } catch (\Exception $e) {
+ $stats['errors'][] = $contact['uuid'].': '.$e->getMessage();
+ $this->logger->error(
+ 'ContactSync: failed to sync contact '.$contact['uuid'],
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end foreach
+ $stats['endTime'] = date('Y-m-d H:i:s');
+ $stats['duration'] = (time() - $startTime);
return $stats;
- }
+ }//end performContactSync()
+ /**
+ * Perform synchronization of users.
+ *
+ * @return array The sync statistics.
+ */
public function performUserSync(): array
{
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $register = $voorzieningenConfig['register'] ?? '';
-// $organizationSchema = $voorzieningenConfig['organisatie_schema'] ?? '';
- $contactSchema = $voorzieningenConfig['contactpersoon_schema'] ?? '';
+ $register = ($voorzieningenConfig['register'] ?? '');
+ $contactSchema = ($voorzieningenConfig['contactpersoon_schema'] ?? '');
+ if (empty($register) === true || empty($contactSchema) === true) {
+ $this->logger->warning('UserSync: voorzieningen config missing register or contactpersoon_schema');
+ return [];
+ }
- $qb = $this->db->getQueryBuilder();
- $usernameExtract2 = $this->jsonExtract('o.object', '$.username');
- $organisatieExtract2 = $this->jsonExtract('o.object', '$.organisatie');
+ // Query per-schema magic table directly (NOT the empty openregister_objects blob table).
+ $contactTableName = 'openregister_table_'.$register.'_'.$contactSchema;
- // Build JSON contains check - this is complex and platform-specific
- $platform = $this->db->getDatabasePlatform();
+ // Build JSON contains check - platform-specific.
+ $platform = $this->db->getDatabasePlatform();
$isPostgres = $platform->getName() === 'postgresql';
- if ($isPostgres) {
- // PostgreSQL: Check if username is NOT in the users array
- $jsonContainsCheck = "NOT (oo.users::jsonb @> to_jsonb({$usernameExtract2}))";
+ if (empty($isPostgres) === false) {
+ $jsonContainsCheck = "NOT (oo.users::jsonb @> to_jsonb(o.username::text))";
} else {
- // MySQL: Use JSON_CONTAINS
- $jsonContainsCheck = "json_contains(oo.users, json_extract(o.object, '$.username')) = 0";
+ $jsonContainsCheck = "JSON_CONTAINS(oo.users, CONCAT('\"', o.username, '\"')) = 0";
}
- $qb->select('o.uuid', $qb->createFunction("{$usernameExtract2} as username"), $qb->createFunction("{$organisatieExtract2} as organisation"), 'oo.users')
- ->from(from:'openregister_objects', alias: 'o')
- ->leftJoin(fromAlias: 'o', join: 'openregister_organisations', alias: 'oo', condition: "oo.uuid = {$organisatieExtract2}")
- ->where($qb->expr()->eq('o.register', $qb->createNamedParameter($register)))
- ->andWhere($qb->expr()->eq('o.schema', $qb->createNamedParameter($contactSchema)))
+ // Find contacts with a username whose username is NOT in their org's users array.
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('o._uuid as uuid', 'o.username', 'o.organisatie', 'oo.users')
+ ->from($contactTableName, 'o')
+ ->leftJoin('o', 'openregister_organisations', 'oo', 'oo.uuid = o.organisatie')
+ ->where($qb->createFunction('o.username IS NOT NULL'))
+ ->andWhere($qb->createFunction('o.username !== '.$qb->createNamedParameter('')))
+ ->andWhere($qb->createFunction('o.organisatie IS NOT NULL'))
->andWhere($qb->createFunction($jsonContainsCheck));
- $sql = $qb->getSQL();
$users = $qb->execute()->fetchAll();
- foreach($users as $user) {
- $this->organisatieService->addUsersToOrganization($user['organisation'], [$user['username']]);
+
+ if (empty($users) === false) {
+ $this->logger->info('UserSync: adding '.count($users).' users to their org entities');
+ }
+
+ foreach ($users as $user) {
+ try {
+ $this->organisatieService->addUsersToOrganization($user['organisatie'], [$user['username']]);
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'UserSync: failed to add user '.$user['username'].' to org '.$user['organisatie'],
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }
}
return [];
- }
+ }//end performUserSync()
/**
* Performs comprehensive organization and contact person synchronization
@@ -397,89 +513,120 @@ public function performUserSync(): array
*
* @return array Synchronization results and statistics
*/
- public function performFullSync(int $minutesBack = 10): array
+ public function performFullSync(int $minutesBack=10): array
{
- $this->logger->info('OrganizationSyncService: Starting comprehensive organization synchronization', [
- 'minutesBack' => $minutesBack,
- 'syncMode' => $minutesBack === 0 ? 'full' : 'incremental'
- ]);
+ if ($minutesBack === 0) {
+ $syncModeValue = 'full';
+ } else {
+ $syncModeValue = 'incremental';
+ }
+
+ $this->logger->info(
+ 'OrganizationSyncService: Starting comprehensive organization synchronization',
+ [
+ 'minutesBack' => $minutesBack,
+ 'syncMode' => $syncModeValue,
+ ]
+ );
$stats = [
- 'organizationsProcessed' => 0,
- 'entitiesCreated' => 0,
- 'entitiesUpdated' => 0,
+ 'organizationsProcessed' => 0,
+ 'entitiesCreated' => 0,
+ 'entitiesUpdated' => 0,
'contactPersonsProcessed' => 0,
- 'usersCreated' => 0,
- 'usersUpdated' => 0,
- 'errors' => [],
- 'startTime' => date('Y-m-d H:i:s'),
- 'endTime' => null,
- 'duration' => null
+ 'usersCreated' => 0,
+ 'usersUpdated' => 0,
+ 'errors' => [],
+ 'startTime' => date('Y-m-d H:i:s'),
+ 'endTime' => null,
+ 'duration' => null,
];
try {
- // Check configuration
+ // Check configuration.
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $register = $voorzieningenConfig['register'] ?? '';
- $organizationSchema = $voorzieningenConfig['organisatie_schema'] ?? '';
- $contactSchema = $voorzieningenConfig['contactpersoon_schema'] ?? '';
+ $register = ($voorzieningenConfig['register'] ?? '');
+ $organizationSchema = ($voorzieningenConfig['organisatie_schema'] ?? '');
+ $contactSchema = ($voorzieningenConfig['contactpersoon_schema'] ?? '');
- if (empty($register) || empty($organizationSchema)) {
+ if (empty($register) === true || empty($organizationSchema) === true) {
$error = 'Missing configuration: register or organization schema not configured';
- $this->logger->error('OrganizationSyncService: ' . $error);
+ $this->logger->error('OrganizationSyncService: '.$error);
$stats['errors'][] = $error;
return $stats;
}
- // Get organisatie objects based on time window
- $organisatieObjects = $this->getOrganisatieObjectsByTimeWindow($register, $organizationSchema, $minutesBack);
- $this->logger->info('OrganizationSyncService: Found organisatie objects', [
- 'count' => count($organisatieObjects),
- 'minutesBack' => $minutesBack,
- 'syncMode' => $minutesBack === 0 ? 'full' : 'incremental'
- ]);
+ // Get organisatie objects based on time window.
+ $organisatieObjects = $this->getOrganisatieObjectsByTimeWindow(
+ register: $register,
+ organizationSchema: $organizationSchema,
+ minutesBack: $minutesBack
+ );
+ if ($minutesBack === 0) {
+ $syncModeValue = 'full';
+ } else {
+ $syncModeValue = 'incremental';
+ }
+
+ $this->logger->info(
+ 'OrganizationSyncService: Found organisatie objects',
+ [
+ 'count' => count($organisatieObjects),
+ 'minutesBack' => $minutesBack,
+ 'syncMode' => $syncModeValue,
+ ]
+ );
- // Process each organisatie object
+ // Process each organisatie object.
foreach ($organisatieObjects as $organisatieObject) {
try {
- $this->processOrganisatieObject($organisatieObject, $register, $contactSchema, $stats);
+ $this->processOrganisatieObject(
+ organisatieObject: $organisatieObject,
+ register: $register,
+ contactSchema: $contactSchema,
+ stats: $stats
+ );
$stats['organizationsProcessed']++;
} catch (\Exception $e) {
- $error = 'Failed to process organisatie object ' . $organisatieObject->getId() . ': ' . $e->getMessage();
- $this->logger->error('OrganizationSyncService: ' . $error, [
- 'exception' => $e,
- 'objectId' => $organisatieObject->getId()
- ]);
+ $error = 'Failed to process organisatie object '.$organisatieObject->getId().': '.$e->getMessage();
+ $this->logger->error(
+ 'OrganizationSyncService: '.$error,
+ [
+ 'exception' => $e,
+ 'objectId' => $organisatieObject->getId(),
+ ]
+ );
$stats['errors'][] = $error;
}
- }
+ }//end foreach
- $stats['endTime'] = date('Y-m-d H:i:s');
- $startTime = new \DateTime($stats['startTime']);
- $endTime = new \DateTime($stats['endTime']);
+ $stats['endTime'] = date('Y-m-d H:i:s');
+ $startTime = new \DateTime($stats['startTime']);
+ $endTime = new \DateTime($stats['endTime']);
$stats['duration'] = $endTime->diff($startTime)->format('%H:%I:%S');
$this->logger->info('OrganizationSyncService: Completed comprehensive synchronization', $stats);
return $stats;
-
} catch (\Exception $e) {
- $error = 'Synchronization failed: ' . $e->getMessage();
- $this->logger->error('OrganizationSyncService: ' . $error, [
- 'exception' => $e
- ]);
+ $error = 'Synchronization failed: '.$e->getMessage();
+ $this->logger->error(
+ 'OrganizationSyncService: '.$error,
+ ['exception' => $e]
+ );
$stats['errors'][] = $error;
- $stats['endTime'] = date('Y-m-d H:i:s');
+ $stats['endTime'] = date('Y-m-d H:i:s');
return $stats;
- }
- }
+ }//end try
+
+ }//end performFullSync()
/**
* Gets organisatie objects filtered by time window
*
- * @param string $register The register ID
+ * @param string $register The register ID
* @param string $organizationSchema The organization schema ID
- * @param int $minutesBack Number of minutes to look back (0 = all objects)
+ * @param int $minutesBack Number of minutes to look back (0 = all objects)
*
* @return array Array of organisatie objects
*/
@@ -488,324 +635,453 @@ private function getOrganisatieObjectsByTimeWindow(string $register, string $org
try {
$objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
- // Build base query for register and schema
+ // Build base query for register and schema.
$query = [
'@self' => [
'register' => (int) $register,
- 'schema' => (int) $organizationSchema
- ]
+ 'schema' => (int) $organizationSchema,
+ ],
];
- // Add time-based filtering if minutesBack > 0
+ // Add time-based filtering if minutesBack > 0.
if ($minutesBack > 0) {
$cutoffTime = new \DateTime();
- $cutoffTime->sub(new \DateInterval('PT' . $minutesBack . 'M'));
+ $cutoffTime->sub(new \DateInterval('PT'.$minutesBack.'M'));
$cutoffTimeString = $cutoffTime->format('Y-m-d\TH:i:sP');
- // Add time filtering to the query
- // Filter objects that were updated within the time window
+ // Add time filtering to the query.
+ // Filter objects that were updated within the time window.
$query['@self']['updated'] = ['gte' => $cutoffTimeString];
- $this->logger->debug('OrganizationSyncService: Using searchObjects with time-based filtering', [
- 'register' => $register,
- 'schema' => $organizationSchema,
- 'minutesBack' => $minutesBack,
- 'cutoffTime' => $cutoffTimeString,
- 'currentTime' => (new \DateTime())->format('Y-m-d\TH:i:sP'),
- 'timeWindowStart' => $cutoffTimeString,
- 'query' => $query
- ]);
+ $this->logger->debug(
+ 'OrganizationSyncService: Using searchObjects with time-based filtering',
+ [
+ 'register' => $register,
+ 'schema' => $organizationSchema,
+ 'minutesBack' => $minutesBack,
+ 'cutoffTime' => $cutoffTimeString,
+ 'currentTime' => (new \DateTime())->format('Y-m-d\TH:i:sP'),
+ 'timeWindowStart' => $cutoffTimeString,
+ 'query' => $query,
+ ]
+ );
} else {
- $this->logger->debug('OrganizationSyncService: Using searchObjects for all objects', [
- 'register' => $register,
- 'schema' => $organizationSchema,
- 'query' => $query
- ]);
- }
+ $this->logger->debug(
+ 'OrganizationSyncService: Using searchObjects for all objects',
+ [
+ 'register' => $register,
+ 'schema' => $organizationSchema,
+ 'query' => $query,
+ ]
+ );
+ }//end if
- // Use searchObjects method for filtering
- $objects = $objectService->searchObjects($query);
+ // Use searchObjects method for filtering.
+ $objects = $objectService->searchObjects(query: $query, _rbac: false, _multitenancy: false);
- $this->logger->debug('OrganizationSyncService: Retrieved organisatie objects with searchObjects', [
- 'register' => $register,
- 'schema' => $organizationSchema,
- 'minutesBack' => $minutesBack,
- 'count' => count($objects)
- ]);
+ $this->logger->debug(
+ 'OrganizationSyncService: Retrieved organisatie objects with searchObjects',
+ [
+ 'register' => $register,
+ 'schema' => $organizationSchema,
+ 'minutesBack' => $minutesBack,
+ 'count' => count($objects),
+ ]
+ );
return $objects;
} catch (\Exception $e) {
- $this->logger->error('OrganizationSyncService: Failed to retrieve organisatie objects with searchObjects', [
- 'register' => $register,
- 'schema' => $organizationSchema,
- 'minutesBack' => $minutesBack,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'OrganizationSyncService: Failed to retrieve organisatie objects with searchObjects',
+ [
+ 'register' => $register,
+ 'schema' => $organizationSchema,
+ 'minutesBack' => $minutesBack,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return [];
- }
- }
+ }//end try
+
+ }//end getOrganisatieObjectsByTimeWindow()
/**
* Processes a single organisatie object
*
* @param object $organisatieObject The organisatie object to process
- * @param string $register The register ID
- * @param string $contactSchema The contact schema ID
- * @param array &$stats Statistics array (passed by reference)
+ * @param string $register The register ID
+ * @param string $contactSchema The contact schema ID
+ * @param array $stats Statistics array (passed by reference)
*
* @return void
*/
private function processOrganisatieObject(object $organisatieObject, string $register, string $contactSchema, array &$stats): void
{
- $objectData = $organisatieObject->getObject();
- $organisatieId = $objectData['id'] ?? $organisatieObject->getId();
-
- $this->logger->debug('OrganizationSyncService: Processing organisatie object', [
- 'organisatieId' => $organisatieId,
- 'naam' => $objectData['naam'] ?? 'Unknown'
- ]);
-
- // Step 1: Ensure organisation entity exists
- $organisationEntity = $this->ensureOrganisationEntity($organisatieObject, $stats);
- if (!$organisationEntity) {
- $this->logger->warning('OrganizationSyncService: Could not ensure organisation entity', [
- 'organisatieId' => $organisatieId
- ]);
+ $objectData = $organisatieObject->getObject();
+ $organisatieId = ($objectData['id'] ?? $organisatieObject->getId());
+
+ $this->logger->debug(
+ 'OrganizationSyncService: Processing organisatie object',
+ [
+ 'organisatieId' => $organisatieId,
+ 'naam' => ($objectData['naam'] ?? 'Unknown'),
+ ]
+ );
+
+ // Step 1: Ensure organisation entity exists.
+ $organisationEntity = $this->ensureOrganisationEntity(organisatieObject: $organisatieObject, stats: $stats);
+ if ($organisationEntity === null) {
+ $this->logger->warning(
+ 'OrganizationSyncService: Could not ensure organisation entity',
+ ['organisatieId' => $organisatieId]
+ );
return;
}
- // Step 2: Get all contact persons for this organisation
- $contactPersons = $this->getContactPersonsForOrganisation($organisatieId, $register, $contactSchema);
- $this->logger->debug('OrganizationSyncService: Found contact persons', [
- 'organisatieId' => $organisatieId,
- 'contactCount' => count($contactPersons)
- ]);
+ // Step 2: Get all contact persons for this organisation.
+ $contactPersons = $this->getContactPersonsForOrganisation(organisatieId: $organisatieId, register: $register, contactSchema: $contactSchema);
+ $this->logger->debug(
+ 'OrganizationSyncService: Found contact persons',
+ [
+ 'organisatieId' => $organisatieId,
+ 'contactCount' => count($contactPersons),
+ ]
+ );
- // Step 3: Process each contact person to ensure they have user accounts
+ // Step 3: Process each contact person to ensure they have user accounts.
$usernames = [];
foreach ($contactPersons as $contactPerson) {
- $username = $this->processContactPerson($contactPerson, $stats);
- if ($username) {
+ $username = $this->processContactPerson(contactPerson: $contactPerson, stats: $stats);
+ if (empty($username) === false) {
$usernames[] = $username;
}
}
- // Step 4: Update organisation entity with all usernames
- $this->updateOrganisationEntityUsers($organisationEntity, $usernames, $stats);
- }
+ // Step 4: Update organisation entity with all usernames.
+ $this->updateOrganisationEntityUsers(organisationEntity: $organisationEntity, usernames: $usernames, stats: $stats);
+
+ }//end processOrganisatieObject()
+
+ /**
+ * Public wrapper for ensureOrganisationEntity.
+ *
+ * Used by ContactpersoonService for backup entity creation when org entity is missing.
+ *
+ * @param object $organisatieObject The organisatie object.
+ * @param array $stats Statistics array (passed by reference).
+ * @param bool $sendEmails Whether to send registration/activation emails.
+ *
+ * @return object|null The organisation entity or null on failure.
+ */
+ public function ensureOrganisationEntityPublic(object $organisatieObject, array &$stats, bool $sendEmails=true): ?object
+ {
+ return $this->ensureOrganisationEntity(organisatieObject: $organisatieObject, stats: $stats, sendEmails: $sendEmails);
+
+ }//end ensureOrganisationEntityPublic()
/**
- * Ensures organisation entity exists for organisatie object
+ * Ensures organisation entity exists for organisatie object.
*
- * @param object $organisatieObject The organisatie object
- * @param array &$stats Statistics array (passed by reference)
+ * @param object $organisatieObject The organisatie object.
+ * @param array $stats Statistics array (passed by reference).
+ * @param bool $sendEmails Whether to send emails.
*
- * @return object|null The organisation entity or null on failure
+ * @return object|null The organisation entity or null on failure.
*/
- private function ensureOrganisationEntity(object $organisatieObject, array &$stats): ?object
+ private function ensureOrganisationEntity(object $organisatieObject, array &$stats, bool $sendEmails=true): ?object
{
try {
- // Get the full object data - the passed object might not have all fields populated
+ // Get the full object data - the passed object might not have all fields populated.
$organisatieId = $organisatieObject->getUuid();
-
- // Fetch the complete object from the database to ensure we have all data
+
+ // Fetch the complete object from the database to ensure we have all data.
try {
$objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
- $fullObject = $objectService->find(
+ $fullObject = $objectService->find(
id: $organisatieId,
register: $organisatieObject->getRegister(),
- schema: $organisatieObject->getSchema()
+ schema: $organisatieObject->getSchema(),
+ _rbac: false,
+ _multitenancy: false
);
- if ($fullObject) {
+ if (empty($fullObject) === false) {
$organisatieObject = $fullObject;
}
} catch (\Exception $e) {
- $this->logger->warning('Could not fetch full organisation object, using provided object', [
- 'organisatieId' => $organisatieId,
- 'error' => $e->getMessage()
- ]);
- }
-
+ $this->logger->warning(
+ 'Could not fetch full organisation object, using provided object',
+ [
+ 'organisatieId' => $organisatieId,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+
$objectData = $organisatieObject->getObject();
- $this->logger->debug('Ensuring organisation entity', [
- 'app' => 'softwarecatalog',
- 'organisatieId' => $organisatieId,
- 'naam' => $objectData['naam'] ?? $objectData['name'] ?? 'Unknown',
- 'status' => $objectData['status'] ?? 'Unknown',
- ]);
+ $this->logger->debug(
+ 'Ensuring organisation entity',
+ [
+ 'app' => 'softwarecatalog',
+ 'organisatieId' => $organisatieId,
+ 'naam' => ($objectData['naam'] ?? $objectData['name'] ?? 'Unknown'),
+ 'status' => ($objectData['status'] ?? 'Unknown'),
+ ]
+ );
- // Get configuration for object updates
+ // Get configuration for object updates.
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $register = $voorzieningenConfig['register'] ?? '';
- $organizationSchema = $voorzieningenConfig['organisatie_schema'] ?? '';
+ $register = ($voorzieningenConfig['register'] ?? '');
+ $organizationSchema = ($voorzieningenConfig['organisatie_schema'] ?? '');
- // Try to find existing organisation entity
+ // Try to find existing organisation entity.
$organisationMapper = \OC::$server->get('OCA\OpenRegister\Db\OrganisationMapper');
try {
$organisationEntity = $organisationMapper->findByUuid($organisatieId);
- // Entity exists - update it if needed
- $status = strtolower($objectData['status'] ?? 'actief');
- $shouldBeActive = in_array($status, ['actief', 'active']);
-
- $this->logger->debug('Existing entity found', [
- 'app' => 'softwarecatalog',
- 'organisatieId' => $organisatieId,
- 'entityId' => $organisationEntity->getId(),
- 'shouldBeActive' => $shouldBeActive,
- 'needsUpdate' => $organisationEntity->getActive() !== $shouldBeActive
- ]);
+ // Entity exists - update it if needed.
+ $status = strtolower(($objectData['status'] ?? 'actief'));
+ $shouldBeActive = in_array($status, ['actief', 'active']) === true;
+
+ $this->logger->debug(
+ 'Existing entity found',
+ [
+ 'app' => 'softwarecatalog',
+ 'organisatieId' => $organisatieId,
+ 'entityId' => $organisationEntity->getId(),
+ 'shouldBeActive' => $shouldBeActive,
+ 'needsUpdate' => $organisationEntity->getActive() !== $shouldBeActive,
+ ]
+ );
if ($organisationEntity->getActive() !== $shouldBeActive) {
- $this->logger->debug('Updating entity status', [
- 'app' => 'softwarecatalog',
- 'organisatieId' => $organisatieId,
- 'oldActive' => $organisationEntity->getActive(),
- 'newActive' => $shouldBeActive
- ]);
+ $this->logger->debug(
+ 'Updating entity status',
+ [
+ 'app' => 'softwarecatalog',
+ 'organisatieId' => $organisatieId,
+ 'oldActive' => $organisationEntity->getActive(),
+ 'newActive' => $shouldBeActive,
+ ]
+ );
$wasActive = $organisationEntity->getActive();
$organisationEntity->setActive($shouldBeActive);
$organisationMapper->save($organisationEntity);
$stats['entitiesUpdated']++;
- // Send activation email if organization became active
- if ($shouldBeActive && !$wasActive) {
- $this->logger->info('[FLOW] Sending organization activation email', [
- 'organisatieId' => $organisatieId
- ]);
- $emailSent = $this->sendOrganizationActivationEmail($objectData);
- if ($emailSent) {
- $this->logger->info('📧 Organization activation email sent successfully', [
- 'organisatieId' => $organisatieId
- ]);
+ // Send activation email if organization became active.
+ if ($sendEmails !== false && $shouldBeActive === true && $wasActive === false) {
+ $this->logger->info(
+ '[FLOW] Sending organization activation email',
+ ['organisatieId' => $organisatieId]
+ );
+ $emailSent = $this->sendOrganizationActivationEmail(organizationData: $objectData);
+ if (empty($emailSent) === false) {
+ $this->logger->info(
+ '📧 Organization activation email sent successfully',
+ ['organisatieId' => $organisatieId]
+ );
} else {
- $this->logger->info('📧 Organization activation email not sent (disabled or not configured)', [
- 'organisatieId' => $organisatieId
- ]);
+ $this->logger->info(
+ '📧 Organization activation email not sent (disabled or not configured)',
+ ['organisatieId' => $organisatieId]
+ );
}
- }
- }
+ }//end if
+ }//end if
- // Update organisatie object owner to organisation entity UUID
- $this->updateOrganisatieObjectOwner($organisatieObject, $organisationEntity, $register, $organizationSchema);
+ // Update organisatie object owner to organisation entity UUID.
+ $this->updateOrganisatieObjectOwner(
+ organisatieObject: $organisatieObject,
+ organisationEntity: $organisationEntity,
+ register: $register,
+ organizationSchema: $organizationSchema
+ );
return $organisationEntity;
-
} catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
- // Entity doesn't exist, create it
- $this->logger->debug('Creating new organisation entity', [
- 'app' => 'softwarecatalog',
- 'organisatieId' => $organisatieId,
- 'naam' => $objectData['naam'] ?? 'Unknown'
- ]);
+ // Entity not found by UUID — try finding by slug before creating.
+ // This handles the case where the entity exists but with a different UUID (e.g. from slug collision).
+ $orgName = ($objectData['naam'] ?? $objectData['name'] ?? '');
+ if (empty($orgName) === false) {
+ $slug = strtolower(preg_replace('/[^a-z0-9]+/', '-', strtolower($orgName)));
+ $slug = trim($slug, '-');
+ try {
+ $organisationEntity = $organisationMapper->findBySlug($slug);
+ $this->logger->info(
+ 'OrganizationSyncService: Found existing entity by slug, updating UUID to match object',
+ [
+ 'app' => 'softwarecatalog',
+ 'organisatieId' => $organisatieId,
+ 'oldEntityUuid' => $organisationEntity->getUuid(),
+ 'slug' => $slug,
+ ]
+ );
+
+ // Update the entity's UUID to match the object UUID so future lookups work.
+ $organisationEntity->setUuid($organisatieId);
+ $organisationMapper->save($organisationEntity);
+ $stats['entitiesUpdated']++;
+
+ // Update organisatie object owner to this entity.
+ $this->updateOrganisatieObjectOwner(
+ organisatieObject: $organisatieObject,
+ organisationEntity: $organisationEntity,
+ register: $register,
+ organizationSchema: $organizationSchema
+ );
+
+ return $organisationEntity;
+ } catch (\OCP\AppFramework\Db\DoesNotExistException $slugEx) {
+ // Not found by slug either — proceed with creation.
+ }//end try
+ }//end if
+
+ $this->logger->debug(
+ 'Creating new organisation entity',
+ [
+ 'app' => 'softwarecatalog',
+ 'organisatieId' => $organisatieId,
+ 'naam' => ($objectData['naam'] ?? 'Unknown'),
+ ]
+ );
$organisationEntity = $this->organisatieService->createOrganisationInOpenRegister($objectData);
- if ($organisationEntity) {
+ if (empty($organisationEntity) === false) {
$stats['entitiesCreated']++;
- $this->logger->debug('New organisation entity created', [
- 'app' => 'softwarecatalog',
- 'organisatieId' => $organisatieId,
- 'entityId' => $organisationEntity->getId(),
- 'active' => $organisationEntity->getActive(),
- 'name' => $organisationEntity->getName()
- ]);
-
- // Send registration email for new organization
- $this->logger->info('[FLOW] Sending organization registration email', [
- 'organisatieId' => $organisatieId
- ]);
- $emailSent = $this->sendOrganizationRegistrationEmail($objectData);
- if ($emailSent) {
- $this->logger->info('📧 Organization registration email sent successfully', [
- 'organisatieId' => $organisatieId
- ]);
- } else {
- $this->logger->info('📧 Organization registration email not sent (disabled or not configured)', [
- 'organisatieId' => $organisatieId
- ]);
- }
+ $this->logger->debug(
+ 'New organisation entity created',
+ [
+ 'app' => 'softwarecatalog',
+ 'organisatieId' => $organisatieId,
+ 'entityId' => $organisationEntity->getId(),
+ 'active' => $organisationEntity->getActive(),
+ 'name' => $organisationEntity->getName(),
+ ]
+ );
+
+ // Send registration email for new organization (skip for backfill).
+ if (empty($sendEmails) === false) {
+ $this->logger->info(
+ '[FLOW] Sending organization registration email',
+ ['organisatieId' => $organisatieId]
+ );
+ $emailSent = $this->sendOrganizationRegistrationEmail(organizationData: $objectData);
+ if (empty($emailSent) === false) {
+ $this->logger->info(
+ '📧 Organization registration email sent successfully',
+ ['organisatieId' => $organisatieId]
+ );
+ } else {
+ $this->logger->info(
+ '📧 Organization registration email not sent (disabled or not configured)',
+ ['organisatieId' => $organisatieId]
+ );
+ }
+ }//end if
- // Update organisatie object owner to organisation entity UUID
- $this->updateOrganisatieObjectOwner($organisatieObject, $organisationEntity, $register, $organizationSchema);
+ // Update organisatie object owner to organisation entity UUID.
+ $this->updateOrganisatieObjectOwner(
+ organisatieObject: $organisatieObject,
+ organisationEntity: $organisationEntity,
+ register: $register,
+ organizationSchema: $organizationSchema
+ );
} else {
- $this->logger->error('❌ ORGANISATION ENTITY CREATION FAILED', [
- 'app' => 'softwarecatalog',
- 'organisatieId' => $organisatieId
- ]);
- }
- return $organisationEntity;
- }
+ $this->logger->error(
+ '❌ ORGANISATION ENTITY CREATION FAILED',
+ [
+ 'app' => 'softwarecatalog',
+ 'organisatieId' => $organisatieId,
+ ]
+ );
+ }//end if
+ return $organisationEntity;
+ }//end try
} catch (\Exception $e) {
- $this->logger->error('💥 ENSURE ORGANISATION ENTITY EXCEPTION', [
- 'app' => 'softwarecatalog',
- 'organisatieId' => $organisatieObject->getId(),
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ '💥 ENSURE ORGANISATION ENTITY EXCEPTION',
+ [
+ 'app' => 'softwarecatalog',
+ 'organisatieId' => $organisatieObject->getId(),
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return null;
- }
- }
+ }//end try
+
+ }//end ensureOrganisationEntity()
/**
* Safely sends organization registration email with error handling
*
- * @param array $organizationData The organization data
- * @return bool True if email was sent successfully, false otherwise
+ * @param array $organizationData The organization data.
+ *
+ * @return bool True if email was sent successfully, false otherwise.
*/
private function sendOrganizationRegistrationEmail(array $organizationData): bool
{
try {
return $this->emailService->sendOrganizationRegistrationEmail($organizationData);
} catch (\Exception $e) {
- $this->logger->warning('OrganizationSyncService: Organization registration email failed', [
- 'organizationId' => $organizationData['id'] ?? 'unknown',
- 'organizationName' => $organizationData['naam'] ?? 'unknown',
- 'error' => $e->getMessage()
- ]);
+ $this->logger->warning(
+ 'OrganizationSyncService: Organization registration email failed',
+ [
+ 'organizationId' => ($organizationData['id'] ?? 'unknown'),
+ 'organizationName' => ($organizationData['naam'] ?? 'unknown'),
+ 'error' => $e->getMessage(),
+ ]
+ );
return false;
}
- }
+
+ }//end sendOrganizationRegistrationEmail()
/**
* Safely sends organization activation email with error handling
*
- * @param array $organizationData The organization data
- * @return bool True if email was sent successfully, false otherwise
+ * @param array $organizationData The organization data.
+ *
+ * @return bool True if email was sent successfully, false otherwise.
*/
private function sendOrganizationActivationEmail(array $organizationData): bool
{
try {
return $this->emailService->sendOrganizationActivationEmail($organizationData);
} catch (\Exception $e) {
- $this->logger->warning('OrganizationSyncService: Organization activation email failed', [
- 'organizationId' => $organizationData['id'] ?? 'unknown',
- 'organizationName' => $organizationData['naam'] ?? 'unknown',
- 'error' => $e->getMessage()
- ]);
+ $this->logger->warning(
+ 'OrganizationSyncService: Organization activation email failed',
+ [
+ 'organizationId' => ($organizationData['id'] ?? 'unknown'),
+ 'organizationName' => ($organizationData['naam'] ?? 'unknown'),
+ 'error' => $e->getMessage(),
+ ]
+ );
return false;
}
- }
+
+ }//end sendOrganizationActivationEmail()
/**
* Gets all contact persons for a specific organisation
*
* @param string $organisatieId The organisation ID
- * @param string $register The register ID
+ * @param string $register The register ID
* @param string $contactSchema The contact schema ID
*
* @return array Array of contact person objects
*/
private function getContactPersonsForOrganisation(string $organisatieId, string $register, string $contactSchema): array
{
- if (empty($contactSchema)) {
+ if (empty($contactSchema) === true) {
$this->logger->debug('OrganizationSyncService: No contact schema configured, skipping contact person lookup');
return [];
}
@@ -813,42 +1089,48 @@ private function getContactPersonsForOrganisation(string $organisatieId, string
try {
$objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
- // Use searchObjects for more efficient filtering on-demand
+ // Use searchObjects for more efficient filtering on-demand.
$query = [
- '@self' => [
+ '@self' => [
'register' => (int) $register,
- 'schema' => (int) $contactSchema
+ 'schema' => (int) $contactSchema,
],
- 'organisatie' => $organisatieId
+ 'organisatie' => $organisatieId,
];
- $contactPersons = $objectService->searchObjects($query);
+ $contactPersons = $objectService->searchObjects(query: $query, _rbac: false, _multitenancy: false);
- $this->logger->debug('OrganizationSyncService: Retrieved contact persons on-demand', [
- 'organisatieId' => $organisatieId,
- 'register' => $register,
- 'schema' => $contactSchema,
- 'count' => count($contactPersons)
- ]);
+ $this->logger->debug(
+ 'OrganizationSyncService: Retrieved contact persons on-demand',
+ [
+ 'organisatieId' => $organisatieId,
+ 'register' => $register,
+ 'schema' => $contactSchema,
+ 'count' => count($contactPersons),
+ ]
+ );
return $contactPersons;
-
} catch (\Exception $e) {
- $this->logger->error('OrganizationSyncService: Failed to get contact persons', [
- 'organisatieId' => $organisatieId,
- 'register' => $register,
- 'schema' => $contactSchema,
- 'exception' => $e
- ]);
+ $this->logger->error(
+ 'OrganizationSyncService: Failed to get contact persons',
+ [
+ 'organisatieId' => $organisatieId,
+ 'register' => $register,
+ 'schema' => $contactSchema,
+ 'exception' => $e,
+ ]
+ );
return [];
- }
- }
+ }//end try
+
+ }//end getContactPersonsForOrganisation()
/**
* Processes a contact person to ensure they have a user account
*
* @param object $contactPerson The contact person object
- * @param array &$stats Statistics array (passed by reference)
+ * @param array $stats Statistics array (passed by reference)
*
* @return string|null The username if successful, null otherwise
*/
@@ -856,72 +1138,96 @@ private function processContactPerson(object $contactPerson, array &$stats): ?st
{
try {
$contactData = $contactPerson->getObject();
- // Schema uses 'e-mailadres' but some data may use 'email'
- $email = $contactData['email'] ?? $contactData['e-mailadres'] ?? '';
- $existingUsername = $contactData['username'] ?? '';
-
- if (empty($email)) {
- $this->logger->debug('OrganizationSyncService: Contact person has no email, skipping', [
- 'contactId' => $contactPerson->getId()
- ]);
+ // Schema uses 'e-mailadres' but some data may use 'email'.
+ $email = ($contactData['email'] ?? $contactData['e-mailadres'] ?? '');
+ $existingUsername = ($contactData['username'] ?? '');
+
+ if (empty($email) === true) {
+ $this->logger->debug(
+ 'OrganizationSyncService: Contact person has no email, skipping',
+ [
+ 'contactId' => $contactPerson->getId(),
+ ]
+ );
return null;
}
- // Check if user already exists
+ // Check if user already exists.
$userManager = \OC::$server->get('OCP\IUserManager');
- $username = $existingUsername ?: $email;
+ if (empty($existingUsername) === false) {
+ $username = $existingUsername;
+ } else {
+ $username = $email;
+ }
+
$user = $userManager->get($username);
- if (!$user) {
- // Create user account
- $this->logger->info('OrganizationSyncService: Creating user account for contact person', [
- 'contactId' => $contactPerson->getId(),
- 'email' => $email,
- 'username' => $username
- ]);
+ if ($user === null) {
+ // Create user account.
+ $this->logger->info(
+ 'OrganizationSyncService: Creating user account for contact person',
+ [
+ 'contactId' => $contactPerson->getId(),
+ 'email' => $email,
+ 'username' => $username,
+ ]
+ );
$success = $this->contactpersoonService->processContactpersoon($contactPerson, false);
- if ($success) {
+ if (empty($success) === false) {
$stats['usersCreated']++;
- $this->logger->info('OrganizationSyncService: Successfully created user account', [
- 'contactId' => $contactPerson->getId(),
- 'username' => $username
- ]);
+ $this->logger->info(
+ 'OrganizationSyncService: Successfully created user account',
+ [
+ 'contactId' => $contactPerson->getId(),
+ 'username' => $username,
+ ]
+ );
return $username;
} else {
- $this->logger->error('OrganizationSyncService: Failed to create user account', [
- 'contactId' => $contactPerson->getId(),
- 'username' => $username
- ]);
+ $this->logger->error(
+ 'OrganizationSyncService: Failed to create user account',
+ [
+ 'contactId' => $contactPerson->getId(),
+ 'username' => $username,
+ ]
+ );
return null;
}
} else {
- // User exists, update username in contact if needed
- if (empty($existingUsername)) {
- $this->logger->debug('OrganizationSyncService: Updating contact person with username', [
- 'contactId' => $contactPerson->getId(),
- 'username' => $username
- ]);
+ // User exists, update username in contact if needed.
+ if (empty($existingUsername) === true) {
+ $this->logger->debug(
+ 'OrganizationSyncService: Updating contact person with username',
+ [
+ 'contactId' => $contactPerson->getId(),
+ 'username' => $username,
+ ]
+ );
$stats['usersUpdated']++;
}
- return $username;
- }
+ return $username;
+ }//end if
} catch (\Exception $e) {
- $this->logger->error('OrganizationSyncService: Failed to process contact person', [
- 'contactId' => $contactPerson->getId(),
- 'exception' => $e
- ]);
+ $this->logger->error(
+ 'OrganizationSyncService: Failed to process contact person',
+ [
+ 'contactId' => $contactPerson->getId(),
+ 'exception' => $e,
+ ]
+ );
return null;
- }
- }
+ }//end try
+
+ }//end processContactPerson()
/**
* Updates organisation entity with all usernames
*
* @param object $organisationEntity The organisation entity
- * @param array $usernames Array of usernames to add
- * @param array &$stats Statistics array (passed by reference)
+ * @param array $usernames Array of usernames to add
+ * @param array $stats Statistics array (passed by reference)
*
* @return void
*/
@@ -929,25 +1235,28 @@ private function updateOrganisationEntityUsers(object $organisationEntity, array
{
try {
$organisationUuid = $organisationEntity->getUuid();
- $currentUsers = $organisationEntity->getUsers() ?? [];
+ $currentUsers = ($organisationEntity->getUsers() ?? []);
- // Add admin users to ensure they're always included
- $adminUsers = $this->getAdminUsers();
+ // Add admin users to ensure they're always included.
+ $adminUsers = $this->getAdminUsers();
$allUsernames = array_unique(array_merge($usernames, $adminUsers));
- // Check if users list has changed
+ // Check if users list has changed.
$currentUsersSet = array_unique($currentUsers);
sort($currentUsersSet);
sort($allUsernames);
if ($currentUsersSet !== $allUsernames) {
- $this->logger->info('OrganizationSyncService: Updating organisation entity users', [
- 'organisationUuid' => $organisationUuid,
- 'currentUsers' => count($currentUsers),
- 'newUsers' => count($allUsernames),
- 'addedUsers' => array_diff($allUsernames, $currentUsers),
- 'removedUsers' => array_diff($currentUsers, $allUsernames)
- ]);
+ $this->logger->info(
+ 'OrganizationSyncService: Updating organisation entity users',
+ [
+ 'organisationUuid' => $organisationUuid,
+ 'currentUsers' => count($currentUsers),
+ 'newUsers' => count($allUsernames),
+ 'addedUsers' => array_diff($allUsernames, $currentUsers),
+ 'removedUsers' => array_diff($currentUsers, $allUsernames),
+ ]
+ );
$organisationEntity->setUsers($allUsernames);
@@ -956,24 +1265,33 @@ private function updateOrganisationEntityUsers(object $organisationEntity, array
$stats['entitiesUpdated']++;
- $this->logger->info('OrganizationSyncService: Successfully updated organisation entity users', [
- 'organisationUuid' => $organisationUuid,
- 'totalUsers' => count($allUsernames)
- ]);
+ $this->logger->info(
+ 'OrganizationSyncService: Successfully updated organisation entity users',
+ [
+ 'organisationUuid' => $organisationUuid,
+ 'totalUsers' => count($allUsernames),
+ ]
+ );
} else {
- $this->logger->debug('OrganizationSyncService: Organisation entity users unchanged', [
- 'organisationUuid' => $organisationUuid,
- 'userCount' => count($allUsernames)
- ]);
- }
-
+ $this->logger->debug(
+ 'OrganizationSyncService: Organisation entity users unchanged',
+ [
+ 'organisationUuid' => $organisationUuid,
+ 'userCount' => count($allUsernames),
+ ]
+ );
+ }//end if
} catch (\Exception $e) {
- $this->logger->error('OrganizationSyncService: Failed to update organisation entity users', [
- 'organisationUuid' => $organisationEntity->getUuid(),
- 'exception' => $e
- ]);
- }
- }
+ $this->logger->error(
+ 'OrganizationSyncService: Failed to update organisation entity users',
+ [
+ 'organisationUuid' => $organisationEntity->getUuid(),
+ 'exception' => $e,
+ ]
+ );
+ }//end try
+
+ }//end updateOrganisationEntityUsers()
/**
* Gets admin users that should always be included in organizations
@@ -984,25 +1302,28 @@ private function getAdminUsers(): array
{
try {
$groupManager = \OC::$server->get('OCP\IGroupManager');
- $adminGroup = $groupManager->get('admin');
+ $adminGroup = $groupManager->get('admin');
- if ($adminGroup) {
- $adminUsers = $adminGroup->getUsers();
+ if (empty($adminGroup) === false) {
+ $adminUsers = $adminGroup->getUsers();
$adminUsernames = [];
foreach ($adminUsers as $user) {
$adminUsernames[] = $user->getUID();
}
+
return $adminUsernames;
}
return [];
} catch (\Exception $e) {
- $this->logger->error('OrganizationSyncService: Failed to get admin users', [
- 'exception' => $e
- ]);
+ $this->logger->error(
+ 'OrganizationSyncService: Failed to get admin users',
+ ['exception' => $e]
+ );
return [];
- }
- }
+ }//end try
+
+ }//end getAdminUsers()
/**
* Performs a quick sync status check with prediction of objects to be processed
@@ -1011,94 +1332,125 @@ private function getAdminUsers(): array
*
* @return array Status information about sync requirements including processing predictions
*/
- public function getSyncStatus(int $minutesBack = 10): array
+ public function getSyncStatus(int $minutesBack=10): array
{
try {
- // Check configuration
+ // Check configuration.
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $register = $voorzieningenConfig['register'] ?? '';
- $organizationSchema = $voorzieningenConfig['organisatie_schema'] ?? '';
- $contactSchema = $voorzieningenConfig['contactpersoon_schema'] ?? '';
+ $register = ($voorzieningenConfig['register'] ?? '');
+ $organizationSchema = ($voorzieningenConfig['organisatie_schema'] ?? '');
+ $contactSchema = ($voorzieningenConfig['contactpersoon_schema'] ?? '');
- if (empty($register) || empty($organizationSchema)) {
+ if (empty($register) === true || empty($organizationSchema) === true) {
return [
'configured' => false,
- 'message' => 'Sync not configured: missing register or organization schema'
+ 'message' => 'Sync not configured: missing register or organization schema',
];
}
- // Get total counts (all objects)
- $allOrganisatieObjects = $this->getOrganisatieObjectsByTimeWindow($register, $organizationSchema, 0);
-
- // Get incremental counts (objects to be processed in next sync)
- $incrementalOrganisatieObjects = $this->getOrganisatieObjectsByTimeWindow($register, $organizationSchema, $minutesBack);
-
- // Get organization entities count
+ // Get total counts (all objects).
+ $allOrganisatieObjects = $this->getOrganisatieObjectsByTimeWindow(
+ register: $register,
+ organizationSchema: $organizationSchema,
+ minutesBack: 0
+ );
+
+ // Get incremental counts (objects to be processed in next sync).
+ $incrementalOrganisatieObjects = $this->getOrganisatieObjectsByTimeWindow(
+ register: $register,
+ organizationSchema: $organizationSchema,
+ minutesBack: $minutesBack
+ );
+
+ // Get organization entities count.
$organisationMapper = \OC::$server->get('OCA\OpenRegister\Db\OrganisationMapper');
- $entitiesCount = 0;
+ $entitiesCount = 0;
try {
- $entities = $organisationMapper->findAllWithUserCount();
+ $entities = $organisationMapper->findAllWithUserCount();
$entitiesCount = count($entities);
} catch (\Exception $e) {
- // Ignore errors in count
+ // Ignore errors in count.
}
- // Predict contact persons to be processed
+ // Predict contact persons to be processed.
$predictedContactPersonsToProcess = 0;
- if (!empty($contactSchema)) {
+ if (empty($contactSchema) === false) {
foreach ($incrementalOrganisatieObjects as $orgObject) {
- $objectData = $orgObject->getObject();
- $organisatieId = $objectData['id'] ?? $orgObject->getId();
- $contactPersons = $this->getContactPersonsForOrganisation($organisatieId, $register, $contactSchema);
+ $objectData = $orgObject->getObject();
+ $organisatieId = ($objectData['id'] ?? $orgObject->getId());
+ $contactPersons = $this->getContactPersonsForOrganisation(
+ organisatieId: $organisatieId,
+ register: $register,
+ contactSchema: $contactSchema
+ );
$predictedContactPersonsToProcess += count($contactPersons);
}
}
- // Calculate efficiency metrics
- $efficiencyImprovement = count($allOrganisatieObjects) > 0
- ? round((1 - (count($incrementalOrganisatieObjects) / count($allOrganisatieObjects))) * 100, 1)
- : 0;
+ // Calculate efficiency metrics.
+ if (count($allOrganisatieObjects) > 0) {
+ $efficiencyImprovement = round(((1 - (count($incrementalOrganisatieObjects) / count($allOrganisatieObjects))) * 100), 1);
+ } else {
+ $efficiencyImprovement = 0;
+ }
- return [
- 'configured' => true,
- 'syncMode' => $minutesBack === 0 ? 'full' : 'incremental',
- 'timeWindow' => $minutesBack,
+ if ($minutesBack === 0) {
+ $syncModeValue = 'full';
+ } else {
+ $syncModeValue = 'incremental';
+ }
+
+ if (count($incrementalOrganisatieObjects) > 0) {
+ $orgCount = $this->formatNumber(number: count($incrementalOrganisatieObjects));
+ $contactCount = $this->formatNumber(number: $predictedContactPersonsToProcess);
+ // phpcs:ignore Generic.Files.LineLength.TooLong
+ $messageValue = "Ready to process {$orgCount} organizations and {$contactCount} contact persons";
+ } else {
+ $messageValue = 'No organizations to process in the current time window';
+ }
- // Total counts
- 'totalOrganizationObjects' => count($allOrganisatieObjects),
+ if ($minutesBack > 0) {
+ $nextScheduledSyncValue = "Will process organizations updated in the last {$minutesBack} minutes";
+ } else {
+ $nextScheduledSyncValue = 'Will process all organizations (full sync)';
+ }
+
+ return [
+ 'configured' => true,
+ 'syncMode' => $syncModeValue,
+ 'timeWindow' => $minutesBack,
+
+ // Total counts.
+ 'totalOrganizationObjects' => count($allOrganisatieObjects),
'totalOrganizationEntities' => $entitiesCount,
- // Processing predictions
- 'organizationsToProcess' => count($incrementalOrganisatieObjects),
- 'contactPersonsToProcess' => $predictedContactPersonsToProcess,
+ // Processing predictions.
+ 'organizationsToProcess' => count($incrementalOrganisatieObjects),
+ 'contactPersonsToProcess' => $predictedContactPersonsToProcess,
- // Efficiency metrics
- 'efficiencyImprovement' => $efficiencyImprovement . '%',
- 'processingReduction' => count($allOrganisatieObjects) - count($incrementalOrganisatieObjects),
+ // Efficiency metrics.
+ 'efficiencyImprovement' => $efficiencyImprovement.'%',
+ 'processingReduction' => (count($allOrganisatieObjects) - count($incrementalOrganisatieObjects)),
- // Configuration
- 'contactSchemaConfigured' => !empty($contactSchema),
- 'lastSyncTime' => $this->config->getValueString('softwarecatalog', 'last_sync_time', 'Never'),
+ // Configuration.
+ 'contactSchemaConfigured' => empty($contactSchema) === false,
+ 'lastSyncTime' => $this->config->getValueString('softwarecatalog', 'last_sync_time', 'Never'),
- // Email configuration status
- 'emailStatus' => $this->getEmailConfigurationStatus(),
+ // Email configuration status.
+ 'emailStatus' => $this->getEmailConfigurationStatus(),
- // Status messages
- 'message' => count($incrementalOrganisatieObjects) > 0
- ? "Ready to process {$this->formatNumber(count($incrementalOrganisatieObjects))} organizations and {$this->formatNumber($predictedContactPersonsToProcess)} contact persons"
- : 'No organizations to process in the current time window',
- 'nextScheduledSync' => $minutesBack > 0
- ? "Will process organizations updated in the last {$minutesBack} minutes"
- : 'Will process all organizations (full sync)'
+ // Status messages.
+ 'message' => $messageValue,
+ 'nextScheduledSync' => $nextScheduledSyncValue,
];
-
} catch (\Exception $e) {
return [
'configured' => false,
- 'message' => 'Error checking sync status: ' . $e->getMessage()
+ 'message' => 'Error checking sync status: '.$e->getMessage(),
];
- }
- }
+ }//end try
+
+ }//end getSyncStatus()
/**
* Gets email configuration status for sync reporting
@@ -1110,17 +1462,21 @@ private function getEmailConfigurationStatus(): array
try {
return $this->emailService->isEmailSystemConfigured();
} catch (\Exception $e) {
- $this->logger->warning('OrganizationSyncService: Failed to check email configuration status', [
- 'error' => $e->getMessage()
- ]);
+ $this->logger->warning(
+ 'OrganizationSyncService: Failed to check email configuration status',
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
return [
- 'configured' => false,
- 'reason' => 'Error checking email configuration: ' . $e->getMessage(),
+ 'configured' => false,
+ 'reason' => 'Error checking email configuration: '.$e->getMessage(),
'hasCredentials' => false,
- 'hasTemplates' => false
+ 'hasTemplates' => false,
];
}
- }
+
+ }//end getEmailConfigurationStatus()
/**
* Format numbers for better readability
@@ -1132,10 +1488,12 @@ private function getEmailConfigurationStatus(): array
private function formatNumber(int $number): string
{
if ($number >= 1000) {
- return number_format($number / 1000, 1) . 'k';
+ return number_format(($number / 1000), 1).'k';
}
+
return (string) $number;
- }
+
+ }//end formatNumber()
/**
* Records the last sync time
@@ -1145,7 +1503,8 @@ private function formatNumber(int $number): string
public function recordSyncTime(): void
{
$this->config->setValueString('softwarecatalog', 'last_sync_time', date('Y-m-d H:i:s'));
- }
+
+ }//end recordSyncTime()
/**
* Process a specific organization object (called from event listener)
@@ -1160,158 +1519,190 @@ public function recordSyncTime(): void
public function processSpecificOrganization($organizationObject): array
{
$startTime = microtime(true);
- $stats = [
- 'organizationsProcessed' => 0,
- 'entitiesCreated' => 0,
- 'entitiesUpdated' => 0,
+ $stats = [
+ 'organizationsProcessed' => 0,
+ 'entitiesCreated' => 0,
+ 'entitiesUpdated' => 0,
'contactPersonsProcessed' => 0,
- 'usersCreated' => 0,
- 'errors' => [],
- 'startTime' => date('Y-m-d H:i:s')
+ 'usersCreated' => 0,
+ 'errors' => [],
+ 'startTime' => date('Y-m-d H:i:s'),
];
try {
- $objectData = $organizationObject->getObject();
+ $objectData = $organizationObject->getObject();
$organizationUuid = $organizationObject->getUuid();
- $this->logger->critical('🏢 ORGANIZATION PROCESSING STARTED', [
- 'app' => 'softwarecatalog',
- 'trigger' => 'ObjectCreatedEvent',
- 'organizationId' => $organizationUuid,
- 'organizationName' => $objectData['naam'] ?? 'Unknown',
- 'organizationStatus' => $objectData['status'] ?? 'Unknown',
- 'timestamp' => date('Y-m-d H:i:s'),
- 'microtime' => microtime(true)
- ]);
-
-
+ $this->logger->info(
+ '🏢 ORGANIZATION PROCESSING STARTED',
+ [
+ 'app' => 'softwarecatalog',
+ 'trigger' => 'ObjectCreatedEvent',
+ 'organizationId' => $organizationUuid,
+ 'organizationName' => ($objectData['naam'] ?? 'Unknown'),
+ 'organizationStatus' => ($objectData['status'] ?? 'Unknown'),
+ 'timestamp' => date('Y-m-d H:i:s'),
+ 'microtime' => microtime(true),
+ ]
+ );
- // Process organization entity
- $this->logger->info('[FLOW] Step 1: Creating/updating organisation entity', [
- 'organizationId' => $organizationUuid,
- 'action' => 'ensure_organisation_entity'
- ]);
+ // Process organization entity.
+ $this->logger->info(
+ '[FLOW] Step 1: Creating/updating organisation entity',
+ [
+ 'organizationId' => $organizationUuid,
+ 'action' => 'ensure_organisation_entity',
+ ]
+ );
- $organisationEntity = $this->ensureOrganisationEntity($organizationObject, $stats);
+ $organisationEntity = $this->ensureOrganisationEntity(organisatieObject: $organizationObject, stats: $stats);
- if ($organisationEntity) {
+ if (empty($organisationEntity) === false) {
$stats['organizationsProcessed']++;
- $this->logger->critical('✅ ORGANISATION ENTITY CREATED/UPDATED', [
- 'app' => 'softwarecatalog',
- 'organizationUuid' => $organizationUuid,
- 'entityId' => $organisationEntity->getId(),
- 'entityActive' => $organisationEntity->getActive(),
- 'entitiesCreated' => $stats['entitiesCreated'],
- 'entitiesUpdated' => $stats['entitiesUpdated']
- ]);
-
- // Step 1.5: Process nested contact persons (from registration form data)
- $this->logger->info('[FLOW] Step 1.5: Processing nested contactpersonen from organization data', [
- 'organizationId' => $organizationUuid,
- 'action' => 'process_nested_contactpersonen'
- ]);
+ $this->logger->info(
+ '✅ ORGANISATION ENTITY CREATED/UPDATED',
+ [
+ 'app' => 'softwarecatalog',
+ 'organizationUuid' => $organizationUuid,
+ 'entityId' => $organisationEntity->getId(),
+ 'entityActive' => $organisationEntity->getActive(),
+ 'entitiesCreated' => $stats['entitiesCreated'],
+ 'entitiesUpdated' => $stats['entitiesUpdated'],
+ ]
+ );
- $this->processNestedContactPersons($organizationObject, $stats);
+ // Step 1.5: Process nested contact persons (from registration form data).
+ $this->logger->info(
+ '[FLOW] Step 1.5: Processing nested contactpersonen from organization data',
+ [
+ 'organizationId' => $organizationUuid,
+ 'action' => 'process_nested_contactpersonen',
+ ]
+ );
- // Step 2: Find and process related contactpersonen objects (separate objects, not nested)
- $this->logger->info('[FLOW] Step 2: Finding related contactpersoon objects', [
- 'organizationId' => $organizationUuid,
- 'action' => 'process_related_contactpersonen'
- ]);
+ $this->processNestedContactPersons(organizationObject: $organizationObject, stats: $stats);
- $this->processRelatedContactPersons($organizationUuid, $stats);
+ // Step 2: Find and process related contactpersonen objects (separate objects, not nested).
+ $this->logger->info(
+ '[FLOW] Step 2: Finding related contactpersoon objects',
+ [
+ 'organizationId' => $organizationUuid,
+ 'action' => 'process_related_contactpersonen',
+ ]
+ );
+ $this->processRelatedContactPersons(organizationUuid: $organizationUuid, organizationObject: $organizationObject, stats: $stats);
} else {
- $this->logger->error('❌ ORGANISATION ENTITY FAILED', [
- 'app' => 'softwarecatalog',
- 'organizationUuid' => $organizationUuid,
- 'error' => 'Failed to create/update organisation entity'
- ]);
+ $this->logger->error(
+ '❌ ORGANISATION ENTITY FAILED',
+ [
+ 'app' => 'softwarecatalog',
+ 'organizationUuid' => $organizationUuid,
+ 'error' => 'Failed to create/update organisation entity',
+ ]
+ );
$stats['errors'][] = 'Failed to create/update organisation entity';
- }
+ }//end if
- $stats['endTime'] = date('Y-m-d H:i:s');
- $stats['duration'] = round(microtime(true) - $startTime, 3);
+ $stats['endTime'] = date('Y-m-d H:i:s');
+ $stats['duration'] = round((microtime(true) - $startTime), 3);
- $this->logger->critical('🏁 ORGANIZATION PROCESSING COMPLETED', [
- 'app' => 'softwarecatalog',
- 'organizationId' => $organizationUuid,
- 'stats' => $stats,
- 'processingTime' => $stats['duration'] . 's'
- ]);
+ $this->logger->info(
+ '🏁 ORGANIZATION PROCESSING COMPLETED',
+ [
+ 'app' => 'softwarecatalog',
+ 'organizationId' => $organizationUuid,
+ 'stats' => $stats,
+ 'processingTime' => $stats['duration'].'s',
+ ]
+ );
return $stats;
-
} catch (\Exception $e) {
$stats['errors'][] = $e->getMessage();
- $this->logger->error('💥 ORGANIZATION PROCESSING EXCEPTION', [
- 'app' => 'softwarecatalog',
- 'organizationId' => $organizationObject->getUuid(),
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ '💥 ORGANIZATION PROCESSING EXCEPTION',
+ [
+ 'app' => 'softwarecatalog',
+ 'organizationId' => $organizationObject->getUuid(),
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return $stats;
- }
- }
+ }//end try
+
+ }//end processSpecificOrganization()
/**
* Process nested contact persons within an organization object
*
- * @param \OCA\OpenRegister\Db\ObjectEntity $organizationObject The organization object containing contact persons
- * @param array $stats The statistics array to update
+ * @param \OCA\OpenRegister\Db\ObjectEntity $organizationObject The organization object.
+ * @param array $stats The statistics array to update.
+ *
* @return void
*/
private function processNestedContactPersons($organizationObject, array &$stats): void
{
try {
- $objectData = $organizationObject->getObject();
+ $objectData = $organizationObject->getObject();
$organizationUuid = $organizationObject->getUuid();
- // Check if organization has nested contact persons
- $contactPersons = $objectData['contactpersonen'] ?? $objectData['contactPersons'] ?? [];
+ // Check if organization has nested contact persons.
+ $contactPersons = ($objectData['contactpersonen'] ?? $objectData['contactPersons'] ?? []);
- if (empty($contactPersons)) {
- $this->logger->info('[FLOW] No nested contact persons found in organization', [
- 'organizationId' => $organizationUuid
- ]);
+ if (empty($contactPersons) === true) {
+ $this->logger->info(
+ '[FLOW] No nested contact persons found in organization',
+ ['organizationId' => $organizationUuid]
+ );
return;
}
- $this->logger->critical('👥 PROCESSING NESTED CONTACT PERSONS', [
- 'app' => 'softwarecatalog',
- 'organizationId' => $organizationUuid,
- 'contactCount' => count($contactPersons)
- ]);
+ $this->logger->info(
+ '👥 PROCESSING NESTED CONTACT PERSONS',
+ [
+ 'app' => 'softwarecatalog',
+ 'organizationId' => $organizationUuid,
+ 'contactCount' => count($contactPersons),
+ ]
+ );
- // Get configuration for contact person creation
+ // Get configuration for contact person creation.
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $register = $voorzieningenConfig['register'] ?? '';
- $contactSchema = $voorzieningenConfig['contactpersoon_schema'] ?? '';
+ $register = ($voorzieningenConfig['register'] ?? '');
+ $contactSchema = ($voorzieningenConfig['contactpersoon_schema'] ?? '');
- if (empty($register) || empty($contactSchema)) {
- $this->logger->warning('[FLOW] Contact person processing skipped - configuration missing', [
- 'organizationId' => $organizationUuid,
- 'register' => $register,
- 'contactSchema' => $contactSchema
- ]);
+ if (empty($register) === true || empty($contactSchema) === true) {
+ $this->logger->warning(
+ '[FLOW] Contact person processing skipped - configuration missing',
+ [
+ 'organizationId' => $organizationUuid,
+ 'register' => $register,
+ 'contactSchema' => $contactSchema,
+ ]
+ );
return;
}
foreach ($contactPersons as $index => $contactData) {
try {
- // Handle UUID references - if contactData is a string (UUID), fetch the actual object
- if (is_string($contactData)) {
- $this->logger->info('[FLOW] Contact person is a UUID reference, fetching object', [
- 'organizationId' => $organizationUuid,
- 'contactIndex' => $index,
- 'contactUuid' => $contactData
- ]);
+ // Handle UUID references - if contactData is a string (UUID), fetch the actual object.
+ if (is_string($contactData) === true) {
+ $this->logger->info(
+ '[FLOW] Contact person is a UUID reference, fetching object',
+ [
+ 'organizationId' => $organizationUuid,
+ 'contactIndex' => $index,
+ 'contactUuid' => $contactData,
+ ]
+ );
- // Fetch the contact person object using the UUID
+ // Fetch the contact person object using the UUID.
$objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
$contactObject = $objectService->find(
id: $contactData,
@@ -1322,142 +1713,266 @@ private function processNestedContactPersons($organizationObject, array &$stats)
);
if ($contactObject === null) {
- $this->logger->warning('[FLOW] Contact person not found by UUID', [
- 'organizationId' => $organizationUuid,
- 'contactUuid' => $contactData
- ]);
+ $this->logger->warning(
+ '[FLOW] Contact person not found by UUID',
+ [
+ 'organizationId' => $organizationUuid,
+ 'contactUuid' => $contactData,
+ ]
+ );
continue;
}
- // Get the object data as array
- $contactData = $contactObject instanceof \OCA\OpenRegister\Db\ObjectEntity
- ? $contactObject->getObject()
- : (is_array($contactObject) ? $contactObject : []);
+ // Get the object data as array.
+ if ($contactObject instanceof \OCA\OpenRegister\Db\ObjectEntity) {
+ $contactData = $contactObject->getObject();
+ } else {
+ if (is_array($contactObject) === true) {
+ $contactData = $contactObject;
+ } else {
+ $contactData = [];
+ }
+ }
- // Add the UUID if not present
- if (!isset($contactData['id']) && $contactObject instanceof \OCA\OpenRegister\Db\ObjectEntity) {
+ // Add the UUID if not present.
+ if (isset($contactData['id']) === false && $contactObject instanceof \OCA\OpenRegister\Db\ObjectEntity) {
$contactData['id'] = $contactObject->getUuid();
}
- }
-
- $this->logger->info('[FLOW] Processing nested contact person', [
- 'organizationId' => $organizationUuid,
- 'contactIndex' => $index,
- 'contactEmail' => $contactData['email'] ?? $contactData['e-mailadres'] ?? 'unknown'
- ]);
+ }//end if
- // Create contact person object in OpenRegister if it doesn't exist
- $this->createOrUpdateContactPersonObject($contactData, $organizationUuid, $register, $contactSchema, $stats);
+ $this->logger->info(
+ '[FLOW] Processing nested contact person',
+ [
+ 'organizationId' => $organizationUuid,
+ 'contactIndex' => $index,
+ 'contactEmail' => ($contactData['email'] ?? $contactData['e-mailadres'] ?? 'unknown'),
+ ]
+ );
+ // Create contact person object in OpenRegister if it doesn't exist.
+ $this->createOrUpdateContactPersonObject(
+ contactData: $contactData,
+ organizationUuid: $organizationUuid,
+ register: $register,
+ contactSchema: $contactSchema,
+ stats: $stats
+ );
} catch (\Exception $e) {
- $this->logger->error('[FLOW] Failed to process nested contact person', [
- 'organizationId' => $organizationUuid,
- 'contactIndex' => $index,
- 'exception' => $e->getMessage()
- ]);
- $stats['errors'][] = "Contact person {$index}: " . $e->getMessage();
- }
- }
-
+ $this->logger->error(
+ '[FLOW] Failed to process nested contact person',
+ [
+ 'organizationId' => $organizationUuid,
+ 'contactIndex' => $index,
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ $stats['errors'][] = "Contact person {$index}: ".$e->getMessage();
+ }//end try
+ }//end foreach
} catch (\Exception $e) {
- $this->logger->error('[FLOW] Failed to process nested contact persons', [
- 'organizationId' => $organizationObject->getUuid(),
- 'exception' => $e->getMessage()
- ]);
- $stats['errors'][] = 'Failed to process nested contact persons: ' . $e->getMessage();
- }
- }
+ $this->logger->error(
+ '[FLOW] Failed to process nested contact persons',
+ [
+ 'organizationId' => $organizationObject->getUuid(),
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ $stats['errors'][] = 'Failed to process nested contact persons: '.$e->getMessage();
+ }//end try
+
+ }//end processNestedContactPersons()
/**
* Process related contactpersoon objects that have this organization in their organisation property
*
- * @param string $organizationUuid The organization UUID to find related contacts for
- * @param array $stats The statistics array to update
+ * @param string $organizationUuid The organization UUID.
+ * @param \OCA\OpenRegister\Db\ObjectEntity $organizationObject The organization object.
+ * @param array $stats The statistics array to update.
+ *
* @return void
*/
- private function processRelatedContactPersons(string $organizationUuid, array &$stats): void
+ private function processRelatedContactPersons(string $organizationUuid, $organizationObject, array &$stats): void
{
try {
- $this->logger->debug('Finding related contact persons', [
- 'app' => 'softwarecatalog',
- 'organizationId' => $organizationUuid,
- ]);
+ $this->logger->debug(
+ 'Finding related contact persons',
+ [
+ 'app' => 'softwarecatalog',
+ 'organizationId' => $organizationUuid,
+ ]
+ );
- // Get configuration
+ // Get configuration.
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $register = $voorzieningenConfig['register'] ?? '';
- $contactSchema = $voorzieningenConfig['contactpersoon_schema'] ?? '';
+ $register = ($voorzieningenConfig['register'] ?? '');
+ $contactSchema = ($voorzieningenConfig['contactpersoon_schema'] ?? '');
- if (empty($register) || empty($contactSchema)) {
- $this->logger->warning('[FLOW] Related contact processing skipped - configuration missing', [
- 'organizationId' => $organizationUuid,
- 'register' => $register,
- 'contactSchema' => $contactSchema
- ]);
+ if (empty($register) === true || empty($contactSchema) === true) {
+ $this->logger->warning(
+ '[FLOW] Related contact processing skipped - configuration missing',
+ [
+ 'organizationId' => $organizationUuid,
+ 'register' => $register,
+ 'contactSchema' => $contactSchema,
+ ]
+ );
return;
}
- // Find all contactpersoon objects that have this organization in their organisation property
+ // Find all contactpersoon objects that have this organization in their organisation property.
$objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
- // Search for contactpersoon objects with this organization reference
- // Try both 'organisatie' and 'organisation' field names
+ // Search for contactpersoon objects with this organization reference.
+ // Try both 'organisatie' and 'organisation' field names.
$query = [
- '@self' => [
+ '@self' => [
'register' => (int) $register,
- 'schema' => (int) $contactSchema
+ 'schema' => (int) $contactSchema,
],
- 'organisatie' => $organizationUuid
+ 'organisatie' => $organizationUuid,
];
- $this->logger->critical('[FLOW] Searching for related contact persons with organisatie field', [
- 'organizationId' => $organizationUuid,
- 'register' => $register,
- 'contactSchema' => $contactSchema,
- 'query' => $query
- ]);
+ $this->logger->info(
+ '[FLOW] Searching for related contact persons with organisatie field',
+ [
+ 'organizationId' => $organizationUuid,
+ 'register' => $register,
+ 'contactSchema' => $contactSchema,
+ 'query' => $query,
+ ]
+ );
$relatedContacts = $objectService->searchObjects($query);
-
- // If not found, try with 'organisation' field
- if (empty($relatedContacts)) {
+
+ // If not found, try with 'organisation' field.
+ if (empty($relatedContacts) === true) {
$query['organisation'] = $organizationUuid;
unset($query['organisatie']);
-
- $this->logger->critical('[FLOW] Retrying search with organisation field', [
- 'organizationId' => $organizationUuid,
- 'query' => $query
- ]);
-
+
+ $this->logger->info(
+ '[FLOW] Retrying search with organisation field',
+ [
+ 'organizationId' => $organizationUuid,
+ 'query' => $query,
+ ]
+ );
+
$relatedContacts = $objectService->searchObjects($query);
}
- if (empty($relatedContacts)) {
- $this->logger->info('[FLOW] No related contact persons found', [
- 'organizationId' => $organizationUuid
- ]);
- return;
- }
+ if (empty($relatedContacts) === true) {
+ // Fallback: fetch contacts by UUID from the org's contactpersonen array.
+ // This handles the case where contacts were linked via inversedBy (org → contact).
+ // but the contact's own organisatie field was never set.
+ $this->logger->info(
+ '[FLOW] No contacts found by organisatie field, checking org contactpersonen array',
+ ['organizationId' => $organizationUuid]
+ );
+
+ // Re-fetch the org object without _extend to get raw UUIDs.
+ try {
+ $voorzieningenConfig2 = $this->settingsService->getVoorzieningenConfig();
+ $orgRegister = ($voorzieningenConfig2['register'] ?? '');
+ $orgSchema = ($voorzieningenConfig2['organisatie_schema'] ?? '');
+
+ $rawOrgObject = $objectService->find(
+ id: $organizationUuid,
+ register: $orgRegister,
+ schema: $orgSchema,
+ _rbac: false,
+ _multitenancy: false
+ );
- $this->logger->critical('👥 PROCESSING RELATED CONTACT PERSONS', [
- 'app' => 'softwarecatalog',
- 'organizationId' => $organizationUuid,
- 'contactCount' => count($relatedContacts)
- ]);
+ if ($rawOrgObject !== null) {
+ $rawOrgData = $rawOrgObject->getObject();
+ } else {
+ $rawOrgData = [];
+ }
+
+ $contactUuids = ($rawOrgData['contactpersonen'] ?? []);
+
+ $this->logger->info(
+ '[FLOW] Raw org contactpersonen array',
+ [
+ 'organizationId' => $organizationUuid,
+ 'contactpersonen' => $contactUuids,
+ 'count' => count($contactUuids),
+ ]
+ );
+
+ if (empty($contactUuids) === false) {
+ foreach ($contactUuids as $contactUuid) {
+ if (is_string($contactUuid) === false || empty($contactUuid) === true) {
+ continue;
+ }
+
+ try {
+ $contactObj = $objectService->find(
+ id: $contactUuid,
+ register: $register,
+ schema: $contactSchema,
+ _rbac: false,
+ _multitenancy: false
+ );
+ if ($contactObj !== null) {
+ $relatedContacts[] = $contactObj;
+ }
+ } catch (\Exception $fetchEx) {
+ $this->logger->warning(
+ '[FLOW] Could not fetch contact from contactpersonen array',
+ [
+ 'organizationId' => $organizationUuid,
+ 'contactUuid' => $contactUuid,
+ 'exception' => $fetchEx->getMessage(),
+ ]
+ );
+ }//end try
+ }//end foreach
+ }//end if
+ } catch (\Exception $orgEx) {
+ $this->logger->warning(
+ '[FLOW] Could not re-fetch org for contactpersonen fallback',
+ [
+ 'organizationId' => $organizationUuid,
+ 'exception' => $orgEx->getMessage(),
+ ]
+ );
+ }//end try
+
+ if (empty($relatedContacts) === true) {
+ $this->logger->info(
+ '[FLOW] No related contact persons found (including contactpersonen fallback)',
+ ['organizationId' => $organizationUuid]
+ );
+ return;
+ }
+ }//end if
+
+ $this->logger->info(
+ '👥 PROCESSING RELATED CONTACT PERSONS',
+ [
+ 'app' => 'softwarecatalog',
+ 'organizationId' => $organizationUuid,
+ 'contactCount' => count($relatedContacts),
+ ]
+ );
foreach ($relatedContacts as $contactObject) {
try {
$contactUuid = $contactObject->getUuid();
$contactData = $contactObject->getObject();
- // Check if contact data is complete (has email)
- $email = $contactData['email'] ?? $contactData['e-mailadres'] ?? '';
- if (empty($email) && !empty($contactUuid)) {
- // Re-fetch the full contact object if email is missing
- $this->logger->info('[FLOW] Contact data incomplete, re-fetching full object', [
- 'organizationId' => $organizationUuid,
- 'contactId' => $contactUuid
- ]);
+ // Check if contact data is complete (has email).
+ $email = ($contactData['email'] ?? $contactData['e-mailadres'] ?? '');
+ if (empty($email) === true && empty($contactUuid) === false) {
+ // Re-fetch the full contact object if email is missing.
+ $this->logger->info(
+ '[FLOW] Contact data incomplete, re-fetching full object',
+ [
+ 'organizationId' => $organizationUuid,
+ 'contactId' => $contactUuid,
+ ]
+ );
try {
$fullContactObject = $objectService->find(
@@ -1469,92 +1984,136 @@ private function processRelatedContactPersons(string $organizationUuid, array &$
);
if ($fullContactObject !== null) {
$contactObject = $fullContactObject;
- $contactData = $contactObject->getObject();
- $email = $contactData['email'] ?? $contactData['e-mailadres'] ?? '';
+ $contactData = $contactObject->getObject();
+ $email = ($contactData['email'] ?? $contactData['e-mailadres'] ?? '');
}
} catch (\Exception $e) {
- $this->logger->warning('[FLOW] Failed to re-fetch contact object', [
- 'organizationId' => $organizationUuid,
- 'contactId' => $contactUuid,
- 'exception' => $e->getMessage()
- ]);
- }
+ $this->logger->warning(
+ '[FLOW] Failed to re-fetch contact object',
+ [
+ 'organizationId' => $organizationUuid,
+ 'contactId' => $contactUuid,
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end if
+
+ // Ensure the contact has the organisatie field set before processing.
+ // (may be missing when contacts were linked via inversedBy from the org side).
+ if (empty($contactData['organisatie']) === true) {
+ $contactData['organisatie'] = $organizationUuid;
+ $contactObject->setObject($contactData);
+ $objectMapper = \OC::$server->get('OCA\OpenRegister\Db\ObjectEntityMapper');
+ $objectMapper->update($contactObject);
+ $this->logger->info(
+ '[FLOW] Set missing organisatie field on related contact',
+ [
+ 'contactId' => $contactUuid,
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
}
- $this->logger->info('[FLOW] Processing related contact person', [
- 'organizationId' => $organizationUuid,
- 'contactId' => $contactUuid,
- 'contactEmail' => $email ?: 'unknown'
- ]);
-
- // Process the contact person through processSpecificContactPerson
- $contactStats = $this->processSpecificContactPerson($contactObject);
-
- // Merge stats
- $stats['contactPersonsProcessed'] += $contactStats['contactPersonsProcessed'] ?? 0;
- $stats['usersCreated'] += $contactStats['usersCreated'] ?? 0;
- $stats['usersUpdated'] += $contactStats['usersUpdated'] ?? 0;
- if (!empty($contactStats['errors'])) {
- $stats['errors'] = array_merge($stats['errors'], $contactStats['errors']);
+ $contactEmailValue = $email;
+ if ($contactEmailValue === false || $contactEmailValue === '' || $contactEmailValue === null) {
+ $contactEmailValue = 'unknown';
}
- } catch (\Exception $e) {
- $this->logger->error('[FLOW] Failed to process related contact person', [
- 'organizationId' => $organizationUuid,
- 'contactId' => $contactObject->getUuid(),
- 'exception' => $e->getMessage()
- ]);
- $stats['errors'][] = "Related contact {$contactObject->getUuid()}: " . $e->getMessage();
- }
- }
+ $this->logger->info(
+ '[FLOW] Processing related contact person',
+ [
+ 'organizationId' => $organizationUuid,
+ 'contactId' => $contactUuid,
+ 'contactEmail' => $contactEmailValue,
+ ]
+ );
+
+ // Process the contact person through processSpecificContactPerson.
+ $contactStats = $this->processSpecificContactPerson(contactObject: $contactObject);
+ // Merge stats.
+ $stats['contactPersonsProcessed'] += ($contactStats['contactPersonsProcessed'] ?? 0);
+ $stats['usersCreated'] += ($contactStats['usersCreated'] ?? 0);
+ $stats['usersUpdated'] += ($contactStats['usersUpdated'] ?? 0);
+ if (empty($contactStats['errors']) === false) {
+ $stats['errors'] = array_merge($stats['errors'], $contactStats['errors']);
+ }
+ } catch (\Exception $e) {
+ $this->logger->error(
+ '[FLOW] Failed to process related contact person',
+ [
+ 'organizationId' => $organizationUuid,
+ 'contactId' => $contactObject->getUuid(),
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ $stats['errors'][] = "Related contact {$contactObject->getUuid()}: ".$e->getMessage();
+ }//end try
+ }//end foreach
} catch (\Exception $e) {
- $this->logger->error('[FLOW] Failed to process related contact persons', [
- 'organizationId' => $organizationUuid,
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine()
- ]);
- $stats['errors'][] = 'Failed to process related contact persons: ' . $e->getMessage();
- }
- }
+ $this->logger->error(
+ '[FLOW] Failed to process related contact persons',
+ [
+ 'organizationId' => $organizationUuid,
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]
+ );
+ $stats['errors'][] = 'Failed to process related contact persons: '.$e->getMessage();
+ }//end try
+
+ }//end processRelatedContactPersons()
/**
* Create or update a contact person object and user account
*
- * @param array $contactData The contact person data
- * @param string $organizationUuid The organization UUID
- * @param string $register The register ID
- * @param string $contactSchema The contact schema ID
- * @param array $stats The statistics array to update
+ * @param array $contactData The contact person data.
+ * @param string $organizationUuid The organization UUID.
+ * @param string $register The register ID.
+ * @param string $contactSchema The contact schema ID.
+ * @param array $stats The statistics array to update.
+ *
* @return void
*/
- private function createOrUpdateContactPersonObject(array $contactData, string $organizationUuid, string $register, string $contactSchema, array &$stats): void
- {
+ private function createOrUpdateContactPersonObject(
+ array $contactData,
+ string $organizationUuid,
+ string $register,
+ string $contactSchema,
+ array &$stats
+ ): void {
try {
$objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
- $email = $contactData['email'] ?? $contactData['e-mailadres'] ?? '';
- if (empty($email)) {
- $this->logger->warning('[FLOW] Contact person has no email, skipping', [
- 'organizationId' => $organizationUuid,
- 'contactData' => array_keys($contactData)
- ]);
+ $email = ($contactData['email'] ?? $contactData['e-mailadres'] ?? '');
+ if (empty($email) === true) {
+ $this->logger->warning(
+ '[FLOW] Contact person has no email, skipping',
+ [
+ 'organizationId' => $organizationUuid,
+ 'contactData' => array_keys($contactData),
+ ]
+ );
return;
}
- // Check if contact already exists (has id from cascading or previous creation)
- $existingContactId = $contactData['id'] ?? $contactData['uuid'] ?? null;
- $contactObject = null;
+ // Check if contact already exists (has id from cascading or previous creation).
+ $existingContactId = ($contactData['id'] ?? $contactData['uuid'] ?? null);
+ $contactObject = null;
- if ($existingContactId) {
- // Contact already exists - fetch it instead of trying to re-create
- $this->logger->info('📧 FETCHING EXISTING CONTACT PERSON', [
- 'app' => 'softwarecatalog',
- 'organizationId' => $organizationUuid,
- 'contactId' => $existingContactId,
- 'email' => $email
- ]);
+ if (empty($existingContactId) === false) {
+ // Contact already exists - fetch it instead of trying to re-create.
+ $this->logger->info(
+ '📧 FETCHING EXISTING CONTACT PERSON',
+ [
+ 'app' => 'softwarecatalog',
+ 'organizationId' => $organizationUuid,
+ 'contactId' => $existingContactId,
+ 'email' => $email,
+ ]
+ );
try {
$contactObject = $objectService->find(
@@ -1565,25 +2124,32 @@ private function createOrUpdateContactPersonObject(array $contactData, string $o
_multitenancy: false
);
} catch (\Exception $e) {
- $this->logger->warning('[FLOW] Could not fetch existing contact, will create new', [
- 'organizationId' => $organizationUuid,
- 'contactId' => $existingContactId,
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->warning(
+ '[FLOW] Could not fetch existing contact, will create new',
+ [
+ 'organizationId' => $organizationUuid,
+ 'contactId' => $existingContactId,
+ 'exception' => $e->getMessage(),
+ ]
+ );
}
- }
+ }//end if
- // If contact doesn't exist, create it (but don't set organisatie as string - it's handled by inversedBy)
+ // If contact doesn't exist, create it (but don't set organisatie as string - it's handled by inversedBy).
if ($contactObject === null) {
- $this->logger->critical('📧 CREATING NEW CONTACT PERSON OBJECT', [
- 'app' => 'softwarecatalog',
- 'organizationId' => $organizationUuid,
- 'email' => $email,
- 'name' => ($contactData['voornaam'] ?? '') . ' ' . ($contactData['achternaam'] ?? '')
- ]);
+ $this->logger->info(
+ '📧 CREATING NEW CONTACT PERSON OBJECT',
+ [
+ 'app' => 'softwarecatalog',
+ 'organizationId' => $organizationUuid,
+ 'email' => $email,
+ 'name' => ($contactData['voornaam'] ?? '').' '.($contactData['achternaam'] ?? ''),
+ ]
+ );
- // Remove organisatie from contactData to avoid validation error
- // The relationship is handled by the inversedBy configuration
+ // Temporarily remove organisatie from contactData to avoid validation error.
+ // (schema expects object type but field stores a UUID string).
+ $savedOrganisatie = $contactData['organisatie'] ?? null;
unset($contactData['organisatie']);
unset($contactData['id']);
unset($contactData['uuid']);
@@ -1595,131 +2161,238 @@ private function createOrUpdateContactPersonObject(array $contactData, string $o
_rbac: false,
_multitenancy: false
);
- }
- if ($contactObject) {
- $stats['contactPersonsProcessed']++;
+ // Restore the organisatie field so the link to the organisation is preserved.
+ if ($contactObject !== false && $savedOrganisatie !== null) {
+ $restoredData = $contactObject->getObject();
+ $restoredData['organisatie'] = $savedOrganisatie;
+ $contactObject->setObject($restoredData);
+ $objectMapper = \OC::$server->get('OCA\OpenRegister\Db\ObjectEntityMapper');
+ $objectMapper->update($contactObject);
+ }
+ }//end if
- $this->logger->info('✅ CONTACT PERSON OBJECT READY', [
- 'app' => 'softwarecatalog',
- 'organizationId' => $organizationUuid,
- 'contactId' => $contactObject->getUuid(),
- 'email' => $email,
- 'wasExisting' => !empty($existingContactId)
- ]);
+ if (empty($contactObject) === false) {
+ $stats['contactPersonsProcessed']++;
- // Create user account if username is missing AND organization is active
+ // Ensure the contact has the organisatie field set (may be missing when linked via inversedBy).
$contactObjectData = $contactObject->getObject();
- if (empty($contactObjectData['username'])) {
- // Check if organization exists in organisation entity table (only active orgs have entries)
+ if (empty($contactObjectData['organisatie']) === true && empty($organizationUuid) === false) {
+ $contactObjectData['organisatie'] = $organizationUuid;
+ $contactObject->setObject($contactObjectData);
+ $contactObject->setOrganisation($organizationUuid);
+ $objectMapper = \OC::$server->get('OCA\OpenRegister\Db\ObjectEntityMapper');
+ $objectMapper->update($contactObject);
+ $this->logger->info(
+ '[FLOW] Set missing organisatie field on contact person',
+ [
+ 'contactId' => $contactObject->getUuid(),
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
+ }
+
+ $this->logger->info(
+ '✅ CONTACT PERSON OBJECT READY',
+ [
+ 'app' => 'softwarecatalog',
+ 'organizationId' => $organizationUuid,
+ 'contactId' => $contactObject->getUuid(),
+ 'email' => $email,
+ 'wasExisting' => empty($existingContactId) === false,
+ ]
+ );
+
+ // Create user account if username is missing AND organization is active.
+ if (empty($contactObjectData['username']) === true) {
+ // Check if organization exists in organisation entity table.
+ $organisationEntity = null;
try {
$organisationMapper = \OC::$server->get('OCA\OpenRegister\Db\OrganisationMapper');
$organisationEntity = $organisationMapper->findByUuid($organizationUuid);
+ } catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
+ // Backup: org entity missing — create it now so user creation can proceed.
+ $this->logger->info(
+ 'Organisation entity missing for '.$organizationUuid.', creating backup entity',
+ [
+ 'app' => 'softwarecatalog',
+ 'contactId' => $contactObject->getUuid(),
+ ]
+ );
+ try {
+ $voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
+ $orgObject = $objectService->find(
+ id: $organizationUuid,
+ register: ($voorzieningenConfig['register'] ?? ''),
+ schema: ($voorzieningenConfig['organisatie_schema'] ?? '')
+ );
+ if (empty($orgObject) === false) {
+ $backupStats = [
+ 'entitiesCreated' => 0,
+ 'entitiesUpdated' => 0,
+ ];
+ $organisationEntity = $this->ensureOrganisationEntity(organisatieObject: $orgObject, stats: $backupStats);
+ }
+ } catch (\Exception $backupEx) {
+ $this->logger->error(
+ 'Backup org entity creation failed',
+ [
+ 'app' => 'softwarecatalog',
+ 'organizationId' => $organizationUuid,
+ 'error' => $backupEx->getMessage(),
+ ]
+ );
+ }//end try
+ }//end try
- if ($organisationEntity && $organisationEntity->getActive()) {
- // Determine if this is the first contact for the organization
+ try {
+ if ($organisationEntity !== false && $organisationEntity->getActive() === true) {
+ // Determine if this is the first contact for the organization.
$isFirstContact = $this->contactpersonHandler->isFirstContactForOrganization($contactObject, $contactObjectData);
- $this->logger->critical('👤 CREATING USER ACCOUNT (org is active)', [
- 'app' => 'softwarecatalog',
- 'contactId' => $contactObject->getUuid(),
- 'organizationId' => $organizationUuid,
- 'organizationActive' => true,
- 'email' => $email,
- 'isFirstContact' => $isFirstContact
- ]);
+ $this->logger->info(
+ 'Creating user account for contact person (org is active)',
+ [
+ 'app' => 'softwarecatalog',
+ 'contactId' => $contactObject->getUuid(),
+ 'organizationId' => $organizationUuid,
+ 'email' => $email,
+ 'isFirstContact' => $isFirstContact,
+ ]
+ );
$user = $this->contactpersonHandler->createUserAccount($contactObject, $isFirstContact);
- if ($user) {
+ if (empty($user) === false) {
$stats['usersCreated']++;
+
+ // FIX #434: Refresh contactObjectData from the entity AFTER.
+ // createUserAccount() because createUserAccount() modifies the.
+ // entity in-place (sets username). Using the stale $contactObjectData.
+ // captured before createUserAccount() would overwrite those changes.
+ // and could lose the organisatie field.
+ $contactObjectData = $contactObject->getObject();
$contactObjectData['username'] = $user->getUID();
- // Remove organisatie field to avoid validation error
- // (it's stored as UUID string but schema expects object type)
- unset($contactObjectData['organisatie']);
+ // FIX #434: Use ObjectEntityMapper::update() directly instead of.
+ // ObjectService::saveObject() to persist the username. This avoids:.
+ // 1. Having to strip the organisatie field (saveObject validates it.
+ // as object type but it is stored as a UUID string).
+ // 2. Triggering ObjectUpdatedEvent cascades that re-enter.
+ // processContactpersoon() with missing organisatie field, causing.
+ // the org lookup to fail.
+ // 3. Potential data loss from stale variables.
+ $contactObject->setObject($contactObjectData);
- $this->logger->critical('💾 ABOUT TO SAVE CONTACT WITH USERNAME', [
- 'app' => 'softwarecatalog',
- 'contactId' => $contactObject->getUuid(),
- 'username' => $user->getUID(),
- 'contactDataKeys' => array_keys($contactObjectData),
- 'hasOrganisatie' => isset($contactObjectData['organisatie'])
- ]);
+ $this->logger->debug(
+ 'Saving contact with username via direct mapper update',
+ [
+ 'app' => 'softwarecatalog',
+ 'contactId' => $contactObject->getUuid(),
+ 'username' => $user->getUID(),
+ 'hasOrganisatie' => isset($contactObjectData['organisatie']) === true,
+ ]
+ );
- // Update the contact object with username
- $contactObject->setObject($contactObjectData);
try {
- $objectService->saveObject(
- object: $contactObject,
- register: $register,
- schema: $contactSchema,
- _rbac: false,
- _multitenancy: false
+ $objectMapper = \OC::$server->get('OCA\OpenRegister\Db\ObjectEntityMapper');
+ $objectMapper->update($contactObject);
+ $this->logger->info(
+ 'Contact saved with username',
+ [
+ 'app' => 'softwarecatalog',
+ 'contactId' => $contactObject->getUuid(),
+ 'username' => $user->getUID(),
+ ]
);
- $this->logger->critical('✅ CONTACT SAVED WITH USERNAME', [
- 'app' => 'softwarecatalog',
- 'contactId' => $contactObject->getUuid(),
- 'username' => $user->getUID()
- ]);
} catch (\Exception $saveEx) {
- $this->logger->error('❌ FAILED TO SAVE CONTACT WITH USERNAME', [
- 'app' => 'softwarecatalog',
- 'contactId' => $contactObject->getUuid(),
- 'error' => $saveEx->getMessage(),
- 'trace' => $saveEx->getTraceAsString()
- ]);
- }
+ $this->logger->warning(
+ 'Failed to save username to contact object (user was created)',
+ [
+ 'app' => 'softwarecatalog',
+ 'contactId' => $contactObject->getUuid(),
+ 'username' => $user->getUID(),
+ 'error' => $saveEx->getMessage(),
+ ]
+ );
+ }//end try
- // Add user to organization entity in database
+ // Add user to organization entity in database.
$this->contactpersonHandler->addUserToOrganizationEntity($contactObject, $user->getUID(), $organizationUuid);
- // Update contactpersoon object owner to user UID
- $this->updateContactpersoonObjectOwner($contactObject, $user->getUID(), $register, $contactSchema, $organizationUuid);
-
- $this->logger->info('User account created', [
- 'app' => 'softwarecatalog',
- 'contactId' => $contactObject->getUuid(),
- 'username' => $user->getUID(),
- ]);
+ // Update contactpersoon object owner to user UID and organisation.
+ $this->updateContactpersoonObjectOwner(
+ contactObject: $contactObject,
+ userUID: $user->getUID(),
+ register: $register,
+ contactSchema: $contactSchema,
+ organizationUuidOverride: $organizationUuid
+ );
+
+ $this->logger->info(
+ 'User account created',
+ [
+ 'app' => 'softwarecatalog',
+ 'contactId' => $contactObject->getUuid(),
+ 'username' => $user->getUID(),
+ ]
+ );
} else {
- $this->logger->error('❌ USER ACCOUNT CREATION FAILED', [
- 'app' => 'softwarecatalog',
- 'contactId' => $contactObject->getUuid(),
- 'email' => $email
- ]);
+ $this->logger->error(
+ 'User account creation failed',
+ [
+ 'app' => 'softwarecatalog',
+ 'contactId' => $contactObject->getUuid(),
+ 'email' => $email,
+ ]
+ );
$stats['errors'][] = "Failed to create user account for {$email}";
- }
+ }//end if
} else {
- $this->logger->info('Skipping user creation - organization not active or not found in entity table', [
- 'contactId' => $contactObject->getUuid(),
- 'organizationId' => $organizationUuid,
- 'organizationFound' => $organisationEntity !== null,
- 'organizationActive' => $organisationEntity ? $organisationEntity->getActive() : false,
- 'email' => $email
- ]);
- }
- } catch (\Exception $e) {
- // Organization not found in entity table = not active
- $this->logger->info('Skipping user creation - organization not found in entity table (not active)', [
- 'contactId' => $contactObject->getUuid(),
- 'organizationId' => $organizationUuid,
- 'reason' => 'Organization not found in entity table',
- 'email' => $email
- ]);
- }
- }
- }
+ if ($organisationEntity !== null) {
+ $organizationActiveValue = $organisationEntity->getActive();
+ } else {
+ $organizationActiveValue = false;
+ }
+ $this->logger->info(
+ 'Skipping user creation - organization not active or not found in entity table',
+ [
+ 'contactId' => $contactObject->getUuid(),
+ 'organizationId' => $organizationUuid,
+ 'organizationFound' => $organisationEntity !== null,
+ 'organizationActive' => $organizationActiveValue,
+ 'email' => $email,
+ ]
+ );
+ }//end if
+ } catch (\Exception $e) {
+ // Organization not found in entity table = not active.
+ $this->logger->info(
+ 'Skipping user creation - organization not found in entity table (not active)',
+ [
+ 'contactId' => $contactObject->getUuid(),
+ 'organizationId' => $organizationUuid,
+ 'reason' => 'Organization not found in entity table',
+ 'email' => $email,
+ ]
+ );
+ }//end try
+ }//end if
+ }//end if
} catch (\Exception $e) {
- $this->logger->error('[FLOW] Failed to create/update contact person object', [
- 'organizationId' => $organizationUuid,
- 'email' => $contactData['email'] ?? $contactData['e-mailadres'] ?? 'unknown',
- 'exception' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
- $stats['errors'][] = "Contact person creation failed: " . $e->getMessage();
- }
- }
+ $this->logger->error(
+ '[FLOW] Failed to create/update contact person object',
+ [
+ 'organizationId' => $organizationUuid,
+ 'email' => ($contactData['email'] ?? $contactData['e-mailadres'] ?? 'unknown'),
+ 'exception' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+ $stats['errors'][] = "Contact person creation failed: ".$e->getMessage();
+ }//end try
+
+ }//end createOrUpdateContactPersonObject()
/**
* Process a specific contact person object (called from event listener)
@@ -1735,126 +2408,212 @@ public function processSpecificContactPerson($contactObject): array
{
$stats = [
'contactPersonsProcessed' => 0,
- 'usersCreated' => 0,
- 'usersUpdated' => 0,
- 'errors' => [],
- 'startTime' => date('Y-m-d H:i:s')
+ 'usersCreated' => 0,
+ 'usersUpdated' => 0,
+ 'errors' => [],
+ 'startTime' => date('Y-m-d H:i:s'),
];
try {
$voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
- $register = $voorzieningenConfig['register'] ?? '';
- $contactSchema = $voorzieningenConfig['contactpersoon_schema'] ?? '';
+ $register = ($voorzieningenConfig['register'] ?? '');
+ $contactSchema = ($voorzieningenConfig['contactpersoon_schema'] ?? '');
- $this->logger->info('[EVENT] OrganizationSyncService: Processing specific contact person', [
- 'contactId' => $contactObject->getUuid(),
- 'trigger' => 'event_listener'
- ]);
+ $this->logger->info(
+ '[EVENT] OrganizationSyncService: Processing specific contact person',
+ [
+ 'contactId' => $contactObject->getUuid(),
+ 'trigger' => 'event_listener',
+ ]
+ );
$contactEntityObject = $contactObject->getObject();
- // Skip if no organization reference
+ // Skip if no organization reference.
$organizationUuid = $contactEntityObject['organisatie'] ?? null;
- if (empty($organizationUuid)) {
- $this->logger->warning('[EVENT] OrganizationSyncService: Contact person has no organization reference', [
- 'contactId' => $contactObject->getUuid()
- ]);
+ if (empty($organizationUuid) === true) {
+ $this->logger->warning(
+ '[EVENT] OrganizationSyncService: Contact person has no organization reference',
+ [
+ 'contactId' => $contactObject->getUuid(),
+ ]
+ );
return $stats;
}
- // Create user account if username is missing AND organization is active
- if (empty($contactEntityObject['username'])) {
- // Check if organization exists in organisation entity table (only active orgs have entries)
+ // Create user account if username is missing AND organization is active.
+ if (empty($contactEntityObject['username']) === true) {
+ // Check if organization exists in organisation entity table.
+ $organisationEntity = null;
try {
$organisationMapper = \OC::$server->get('OCA\OpenRegister\Db\OrganisationMapper');
$organisationEntity = $organisationMapper->findByUuid($organizationUuid);
+ } catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
+ // Backup: org entity missing — create it now so user creation can proceed.
+ $this->logger->info(
+ '[EVENT] Organisation entity missing for '.$organizationUuid.', creating backup entity',
+ [
+ 'app' => 'softwarecatalog',
+ 'contactId' => $contactObject->getUuid(),
+ ]
+ );
+ try {
+ $voorzieningenConfig = $this->settingsService->getVoorzieningenConfig();
+ $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
+ $orgObject = $objectService->find(
+ id: $organizationUuid,
+ register: ($voorzieningenConfig['register'] ?? ''),
+ schema: ($voorzieningenConfig['organisatie_schema'] ?? '')
+ );
+ if (empty($orgObject) === false) {
+ $backupStats = [
+ 'entitiesCreated' => 0,
+ 'entitiesUpdated' => 0,
+ ];
+ $organisationEntity = $this->ensureOrganisationEntity(organisatieObject: $orgObject, stats: $backupStats);
+ }
+ } catch (\Exception $backupEx) {
+ $this->logger->error(
+ 'Backup org entity creation failed',
+ [
+ 'app' => 'softwarecatalog',
+ 'organizationId' => $organizationUuid,
+ 'error' => $backupEx->getMessage(),
+ ]
+ );
+ }//end try
+ }//end try
- if ($organisationEntity && $organisationEntity->getActive()) {
- // Determine if this is the first contact for the organization
+ try {
+ if ($organisationEntity !== false && $organisationEntity->getActive() === true) {
+ // Determine if this is the first contact for the organization.
$isFirstContact = $this->contactpersonHandler->isFirstContactForOrganization($contactObject, $contactEntityObject);
- $this->logger->info('[EVENT] OrganizationSyncService: Creating user account for contact person (org is active)', [
- 'contactId' => $contactObject->getUuid(),
- 'organizationId' => $organizationUuid,
- 'organizationActive' => true,
- 'email' => $contactEntityObject['email'] ?? $contactEntityObject['e-mailadres'] ?? 'unknown',
- 'isFirstContact' => $isFirstContact
- ]);
+ $this->logger->info(
+ '[EVENT] OrganizationSyncService: Creating user account for contact person (org is active)',
+ [
+ 'contactId' => $contactObject->getUuid(),
+ 'organizationId' => $organizationUuid,
+ 'organizationActive' => true,
+ 'email' => ($contactEntityObject['email'] ?? $contactEntityObject['e-mailadres'] ?? 'unknown'),
+ 'isFirstContact' => $isFirstContact,
+ ]
+ );
$user = $this->contactpersonHandler->createUserAccount($contactObject, $isFirstContact);
// Check if user was created successfully (can be null if no email).
if ($user !== null) {
$contactEntityObject['username'] = $user->getUID();
- // Remove organisatie field to avoid validation error
- // (it's stored as UUID string but schema expects object type)
+ // Temporarily remove organisatie field to avoid validation error.
+ // (it's stored as UUID string but schema expects object type).
+ // Save the value so we can restore it after the save.
+ $savedOrganisatie = $contactEntityObject['organisatie'] ?? null;
unset($contactEntityObject['organisatie']);
// Update the contact object with the username (using RBAC bypass).
- $contactObject->setObject($contactEntityObject);
- $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
- $objectService->saveObject(
- object: $contactObject,
- register: $register,
- schema: $contactSchema,
- _rbac: false,
- _multitenancy: false
- );
+ // Wrapped in its own try/catch because saveObject triggers event listeners.
+ // that may fail — but the user was already created successfully above.
+ try {
+ $contactObject->setObject($contactEntityObject);
+ $objectService = \OC::$server->get('OCA\OpenRegister\Service\ObjectService');
+ $objectService->saveObject(
+ object: $contactObject,
+ register: $register,
+ schema: $contactSchema,
+ _rbac: false,
+ _multitenancy: false
+ );
+ } catch (\Exception $saveEx) {
+ $this->logger->warning(
+ '[EVENT] Failed to save username to contact object (user was created)',
+ [
+ 'contactId' => $contactObject->getUuid(),
+ 'username' => $user->getUID(),
+ 'error' => $saveEx->getMessage(),
+ ]
+ );
+ }
// Add user to organization entity in database.
$this->contactpersonHandler->addUserToOrganizationEntity($contactObject, $user->getUID(), $organizationUuid);
// Update contactpersoon object owner to user UID.
- $this->updateContactpersoonObjectOwner($contactObject, $user->getUID(), $register, $contactSchema, $organizationUuid);
+ $this->updateContactpersoonObjectOwner(
+ contactObject: $contactObject,
+ userUID: $user->getUID(),
+ register: $register,
+ contactSchema: $contactSchema,
+ organizationUuidOverride: $organizationUuid
+ );
$stats['usersCreated']++;
} else {
- $this->logger->debug('[EVENT] Skipping contact - user account creation failed (likely no email)', [
- 'app' => 'softwarecatalog',
- 'contactId' => $contactObject->getUuid()
- ]);
- }
+ $this->logger->debug(
+ '[EVENT] Skipping contact - user account creation failed (likely no email)',
+ [
+ 'app' => 'softwarecatalog',
+ 'contactId' => $contactObject->getUuid(),
+ ]
+ );
+ }//end if
} else {
- $this->logger->info('[EVENT] OrganizationSyncService: Skipping user creation - organization not active or not found in entity table', [
- 'contactId' => $contactObject->getUuid(),
- 'organizationId' => $organizationUuid,
- 'organizationFound' => $organisationEntity !== null,
- 'organizationActive' => $organisationEntity ? $organisationEntity->getActive() : false,
- 'email' => $contactEntityObject['email'] ?? $contactEntityObject['e-mailadres'] ?? 'unknown'
- ]);
- }
+ if ($organisationEntity !== null) {
+ $organizationActiveValue = $organisationEntity->getActive();
+ } else {
+ $organizationActiveValue = false;
+ }
+
+ $this->logger->info(
+ '[EVENT] OrganizationSyncService: Skipping user creation - organization not active or entity not found',
+ [
+ 'contactId' => $contactObject->getUuid(),
+ 'organizationId' => $organizationUuid,
+ 'organizationFound' => $organisationEntity !== null,
+ 'organizationActive' => $organizationActiveValue,
+ 'email' => ($contactEntityObject['email'] ?? $contactEntityObject['e-mailadres'] ?? 'unknown'),
+ ]
+ );
+ }//end if
} catch (\Exception $e) {
- // Organization not found in entity table = not active
- $this->logger->info('[EVENT] OrganizationSyncService: Skipping user creation - organization not found in entity table (not active)', [
- 'contactId' => $contactObject->getUuid(),
- 'organizationId' => $organizationUuid,
- 'reason' => 'Organization not found in entity table',
- 'email' => $contactEntityObject['email'] ?? $contactEntityObject['e-mailadres'] ?? 'unknown'
- ]);
- }
- }
+ $this->logger->error(
+ '[EVENT] User creation failed for contact',
+ [
+ 'contactId' => $contactObject->getUuid(),
+ 'organizationId' => $organizationUuid,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end if
$stats['contactPersonsProcessed']++;
- $stats['endTime'] = date('Y-m-d H:i:s');
- $stats['duration'] = (new \DateTime($stats['endTime']))->getTimestamp() - (new \DateTime($stats['startTime']))->getTimestamp();
+ $stats['endTime'] = date('Y-m-d H:i:s');
+ $stats['duration'] = ((new \DateTime($stats['endTime']))->getTimestamp() - (new \DateTime($stats['startTime']))->getTimestamp());
- $this->logger->info('[EVENT] OrganizationSyncService: Specific contact person processing completed', [
- 'contactId' => $contactObject->getUuid(),
- 'stats' => $stats
- ]);
+ $this->logger->info(
+ '[EVENT] OrganizationSyncService: Specific contact person processing completed',
+ [
+ 'contactId' => $contactObject->getUuid(),
+ 'stats' => $stats,
+ ]
+ );
return $stats;
-
} catch (\Exception $e) {
$stats['errors'][] = $e->getMessage();
- $this->logger->error('[EVENT] OrganizationSyncService: Failed to process specific contact person', [
- 'contactId' => $contactObject->getUuid(),
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ '[EVENT] OrganizationSyncService: Failed to process specific contact person',
+ [
+ 'contactId' => $contactObject->getUuid(),
+ 'exception' => $e->getMessage(),
+ ]
+ );
return $stats;
- }
- }
+ }//end try
+
+ }//end processSpecificContactPerson()
/**
* Performs optimized manual synchronization for large datasets
@@ -1867,85 +2626,92 @@ public function processSpecificContactPerson($contactObject): array
*
* @return array Comprehensive synchronization results
*/
- public function performOptimizedManualSync(int $maxRounds = 10, int $batchSize = 100): array
+ public function performOptimizedManualSync(int $maxRounds=10, int $batchSize=100): array
{
$totalStartTime = time();
- $allResults = [
- 'totalRounds' => 0,
- 'organizationsProcessed' => 0,
+ $allResults = [
+ 'totalRounds' => 0,
+ 'organizationsProcessed' => 0,
'contactPersonsProcessed' => 0,
- 'entitiesCreated' => 0,
- 'entitiesUpdated' => 0,
- 'usersCreated' => 0,
- 'usersUpdated' => 0,
- 'totalExecutionTime' => 0,
- 'timeoutReached' => false,
- 'roundsCompleted' => [],
- 'errors' => [],
- 'startTime' => date('Y-m-d H:i:s')
+ 'entitiesCreated' => 0,
+ 'entitiesUpdated' => 0,
+ 'usersCreated' => 0,
+ 'usersUpdated' => 0,
+ 'totalExecutionTime' => 0,
+ 'timeoutReached' => false,
+ 'roundsCompleted' => [],
+ 'errors' => [],
+ 'startTime' => date('Y-m-d H:i:s'),
];
- $this->logger->info('[MANUAL] OrganizationSyncService: Starting optimized manual sync', [
- 'maxRounds' => $maxRounds,
- 'batchSize' => $batchSize,
- 'trigger' => 'manual'
- ]);
+ $this->logger->info(
+ '[MANUAL] OrganizationSyncService: Starting optimized manual sync',
+ [
+ 'maxRounds' => $maxRounds,
+ 'batchSize' => $batchSize,
+ 'trigger' => 'manual',
+ ]
+ );
for ($round = 1; $round <= $maxRounds; $round++) {
$roundStartTime = time();
- // Process organizations batch
- $orgResults = $this->performOrganizationsSync($batchSize, 45);
+ // Process organizations batch.
+ $orgResults = $this->performOrganizationsSync(batchSize: $batchSize, maxExecutionSeconds: 45);
- // Process contacts batch
- $contactResults = $this->performContactSync($batchSize, 15);
+ // Process contacts batch.
+ $contactResults = $this->performContactSync(batchSize: $batchSize, maxExecutionSeconds: 15);
- // Accumulate results
- $allResults['organizationsProcessed'] += $orgResults['organizationsProcessed'];
+ // Accumulate results.
+ $allResults['organizationsProcessed'] += $orgResults['organizationsProcessed'];
$allResults['contactPersonsProcessed'] += $contactResults['contactPersonsProcessed'];
- $allResults['entitiesCreated'] += $orgResults['entitiesCreated'];
- $allResults['entitiesUpdated'] += $orgResults['entitiesUpdated'];
- $allResults['usersCreated'] += $contactResults['usersCreated'];
- $allResults['usersUpdated'] += $contactResults['usersUpdated'];
+ $allResults['entitiesCreated'] += $orgResults['entitiesCreated'];
+ $allResults['entitiesUpdated'] += $orgResults['entitiesUpdated'];
+ $allResults['usersCreated'] += $contactResults['usersCreated'] ?? 0;
+ $allResults['usersUpdated'] += $contactResults['usersUpdated'] ?? 0;
- $roundTime = time() - $roundStartTime;
+ $roundTime = (time() - $roundStartTime);
$allResults['roundsCompleted'][] = [
- 'round' => $round,
- 'organizationsProcessed' => $orgResults['organizationsProcessed'],
+ 'round' => $round,
+ 'organizationsProcessed' => $orgResults['organizationsProcessed'],
'contactPersonsProcessed' => $contactResults['contactPersonsProcessed'],
- 'duration' => $roundTime,
- 'orgTimeoutReached' => $orgResults['timeoutReached'] ?? false,
- 'contactTimeoutReached' => $contactResults['timeoutReached'] ?? false
+ 'duration' => $roundTime,
+ 'orgTimeoutReached' => $orgResults['timeoutReached'] ?? false,
+ 'contactTimeoutReached' => $contactResults['timeoutReached'] ?? false,
];
- // If no items were processed in this round, we're done
+ // If no items were processed in this round, we're done.
if ($orgResults['organizationsProcessed'] === 0 && $contactResults['contactPersonsProcessed'] === 0) {
- $this->logger->info('[MANUAL] OrganizationSyncService: No more items to process, stopping', [
- 'round' => $round,
- 'totalProcessed' => $allResults['organizationsProcessed'] + $allResults['contactPersonsProcessed']
- ]);
+ $this->logger->info(
+ '[MANUAL] OrganizationSyncService: No more items to process, stopping',
+ [
+ 'round' => $round,
+ 'totalProcessed' => ($allResults['organizationsProcessed'] + $allResults['contactPersonsProcessed']),
+ ]
+ );
break;
}
$allResults['totalRounds'] = $round;
- // Small pause between rounds to prevent resource exhaustion
+ // Small pause between rounds to prevent resource exhaustion.
if ($round < $maxRounds) {
sleep(1);
}
- }
+ }//end for
- // Final user sync
+ // Final user sync.
$this->performUserSync();
$this->recordSyncTime();
- $allResults['totalExecutionTime'] = time() - $totalStartTime;
- $allResults['endTime'] = date('Y-m-d H:i:s');
+ $allResults['totalExecutionTime'] = (time() - $totalStartTime);
+ $allResults['endTime'] = date('Y-m-d H:i:s');
$this->logger->info('[MANUAL] OrganizationSyncService: Optimized manual sync completed', $allResults);
return $allResults;
- }
+
+ }//end performOptimizedManualSync()
/**
* Performs scheduled synchronization with comprehensive logging
@@ -1958,74 +2724,93 @@ public function performOptimizedManualSync(int $maxRounds = 10, int $batchSize =
*
* @return array Synchronization results with detailed logging information
*/
- public function performScheduledSync(int $minutesBack = 0): array
+ public function performScheduledSync(int $minutesBack=0): array
{
- $this->logger->info('[CRONJOB] OrganizationSyncService: Starting scheduled synchronization', [
- 'minutesBack' => $minutesBack,
- 'syncMode' => $minutesBack === 0 ? 'full' : 'incremental',
- 'trigger' => 'cronjob'
- ]);
-
- try {
- // Perform optimized batch synchronization
- // Use smaller batches for scheduled sync to ensure it completes within time limits
- $orgBatchSize = 25; // Conservative batch size for organizations
- $contactBatchSize = 50; // Larger batch size for contacts (faster processing)
- $maxOrgTime = 30; // 30 seconds max for organizations
- $maxContactTime = 15; // 15 seconds max for contacts
+ if ($minutesBack === 0) {
+ $syncModeValue = 'full';
+ } else {
+ $syncModeValue = 'incremental';
+ }
- $syncResults = $this->performOrganizationsSync($orgBatchSize, $maxOrgTime);
+ $this->logger->info(
+ '[CRONJOB] OrganizationSyncService: Starting scheduled synchronization',
+ [
+ 'minutesBack' => $minutesBack,
+ 'syncMode' => $syncModeValue,
+ 'trigger' => 'cronjob',
+ ]
+ );
- $contactResults = $this->performContactSync($contactBatchSize, $maxContactTime);
- $syncResults = array_merge($contactResults, $syncResults);
+ try {
+ // Perform optimized batch synchronization.
+ // Use smaller batches for scheduled sync to ensure it completes within time limits.
+ $orgBatchSize = 25;
+ // Conservative batch size for organizations.
+ $contactBatchSize = 50;
+ // Larger batch size for contacts (faster processing).
+ $maxOrgTime = 30;
+ // 30 seconds max for organizations.
+ $maxContactTime = 15;
+ // 15 seconds max for contacts.
+ $syncResults = $this->performOrganizationsSync(batchSize: $orgBatchSize, maxExecutionSeconds: $maxOrgTime);
+
+ $contactResults = $this->performContactSync(batchSize: $contactBatchSize, maxExecutionSeconds: $maxContactTime);
+ $syncResults = array_merge($contactResults, $syncResults);
$this->performUserSync();
- // Record the sync time
+ // Record the sync time.
$this->recordSyncTime();
- // Log summary results
- $this->logger->info('[CRONJOB] OrganizationSyncService: Scheduled synchronization completed', [
- 'organizationsProcessed' => $syncResults['organizationsProcessed'],
- 'entitiesCreated' => $syncResults['entitiesCreated'],
- 'entitiesUpdated' => $syncResults['entitiesUpdated'],
- 'contactPersonsProcessed' => $syncResults['contactPersonsProcessed'],
- 'usersCreated' => $syncResults['usersCreated'],
- 'usersUpdated' => $syncResults['usersUpdated'],
- 'errorCount' => count($syncResults['errors']),
- 'duration' => $syncResults['duration']
- ]);
-
- // Log errors if any occurred
- if (!empty($syncResults['errors'])) {
- $this->logger->warning('OrganizationSyncService: Scheduled synchronization completed with errors', [
- 'errors' => $syncResults['errors']
- ]);
+ // Log summary results.
+ $this->logger->info(
+ '[CRONJOB] OrganizationSyncService: Scheduled synchronization completed',
+ [
+ 'organizationsProcessed' => $syncResults['organizationsProcessed'] ?? 0,
+ 'entitiesCreated' => $syncResults['entitiesCreated'] ?? 0,
+ 'entitiesUpdated' => $syncResults['entitiesUpdated'] ?? 0,
+ 'contactPersonsProcessed' => $syncResults['contactPersonsProcessed'] ?? 0,
+ 'errorCount' => count($syncResults['errors'] ?? []),
+ 'duration' => $syncResults['duration'] ?? 0,
+ ]
+ );
+
+ // Log errors if any occurred.
+ if (empty($syncResults['errors']) === false) {
+ $this->logger->warning(
+ 'OrganizationSyncService: Scheduled synchronization completed with errors',
+ [
+ 'errors' => $syncResults['errors'],
+ ]
+ );
}
return $syncResults;
-
} catch (\Exception $e) {
- $this->logger->error('[CRONJOB] OrganizationSyncService: Scheduled synchronization failed', [
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ '[CRONJOB] OrganizationSyncService: Scheduled synchronization failed',
+ [
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return [
- 'organizationsProcessed' => 0,
- 'entitiesCreated' => 0,
- 'entitiesUpdated' => 0,
+ 'organizationsProcessed' => 0,
+ 'entitiesCreated' => 0,
+ 'entitiesUpdated' => 0,
'contactPersonsProcessed' => 0,
- 'usersCreated' => 0,
- 'usersUpdated' => 0,
- 'errors' => ['Scheduled synchronization failed: ' . $e->getMessage()],
- 'startTime' => date('Y-m-d H:i:s'),
- 'endTime' => date('Y-m-d H:i:s'),
- 'duration' => '00:00:00'
+ 'usersCreated' => 0,
+ 'usersUpdated' => 0,
+ 'errors' => ['Scheduled synchronization failed: '.$e->getMessage()],
+ 'startTime' => date('Y-m-d H:i:s'),
+ 'endTime' => date('Y-m-d H:i:s'),
+ 'duration' => '00:00:00',
];
- }
- }
+ }//end try
+
+ }//end performScheduledSync()
/**
* Performs manual synchronization with API-specific logging
@@ -2038,12 +2823,21 @@ public function performScheduledSync(int $minutesBack = 0): array
*
* @return array Synchronization results formatted for API response
*/
- public function performManualSync(int $minutesBack = 0): array
+ public function performManualSync(int $minutesBack=0): array
{
- $this->logger->info('OrganizationSyncService: Manual organization synchronization started via API', [
- 'minutesBack' => $minutesBack,
- 'syncMode' => $minutesBack === 0 ? 'full' : 'incremental'
- ]);
+ if ($minutesBack === 0) {
+ $syncModeValue = 'full';
+ } else {
+ $syncModeValue = 'incremental';
+ }
+
+ $this->logger->info(
+ 'OrganizationSyncService: Manual organization synchronization started via API',
+ [
+ 'minutesBack' => $minutesBack,
+ 'syncMode' => $syncModeValue,
+ ]
+ );
try {
$syncResults = $this->performOrganizationsSync();
@@ -2052,37 +2846,43 @@ public function performManualSync(int $minutesBack = 0): array
$this->performUserSync();
- // Record the sync time for consistency with scheduled sync
+ // Record the sync time for consistency with scheduled sync.
$this->recordSyncTime();
- $this->logger->info('OrganizationSyncService: Manual organization synchronization completed via API', [
- 'organizationsProcessed' => $syncResults['organizationsProcessed'],
- 'entitiesCreated' => $syncResults['entitiesCreated'],
- 'entitiesUpdated' => $syncResults['entitiesUpdated'],
- 'usersCreated' => $syncResults['usersCreated'],
- 'errorCount' => count($syncResults['errors'])
- ]);
+ $this->logger->info(
+ 'OrganizationSyncService: Manual organization synchronization completed via API',
+ [
+ 'organizationsProcessed' => $syncResults['organizationsProcessed'],
+ 'entitiesCreated' => $syncResults['entitiesCreated'],
+ 'entitiesUpdated' => $syncResults['entitiesUpdated'],
+ 'usersCreated' => $syncResults['usersCreated'],
+ 'errorCount' => count($syncResults['errors']),
+ ]
+ );
return [
'success' => true,
'results' => $syncResults,
- 'message' => 'Synchronization completed successfully'
+ 'message' => 'Synchronization completed successfully',
];
-
} catch (\Exception $e) {
- $this->logger->error('OrganizationSyncService: Manual organization synchronization failed via API', [
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine()
- ]);
+ $this->logger->error(
+ 'OrganizationSyncService: Manual organization synchronization failed via API',
+ [
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]
+ );
return [
'success' => false,
- 'message' => 'Synchronization failed: ' . $e->getMessage(),
- 'trace' => $e->getTraceAsString()
+ 'message' => 'Synchronization failed: '.$e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
];
- }
- }
+ }//end try
+
+ }//end performManualSync()
/**
* Gets synchronization status with error handling
@@ -2090,187 +2890,256 @@ public function performManualSync(int $minutesBack = 0): array
* This method provides status information with proper error handling
* for API responses.
*
- * @return array Status information with error handling
+ * @param int $minutesBack The number of minutes to look back.
+ *
+ * @return array Status information with error handling.
*/
- public function getSyncStatusWithErrorHandling(int $minutesBack = 10): array
+ public function getSyncStatusWithErrorHandling(int $minutesBack=10): array
{
try {
- $status = $this->getSyncStatus($minutesBack);
+ $status = $this->getSyncStatus(minutesBack: $minutesBack);
return $status;
} catch (\Exception $e) {
- $this->logger->error('OrganizationSyncService: Failed to get sync status', [
- 'minutesBack' => $minutesBack,
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'OrganizationSyncService: Failed to get sync status',
+ [
+ 'minutesBack' => $minutesBack,
+ 'exception' => $e->getMessage(),
+ ]
+ );
+
+ if ($minutesBack === 0) {
+ $syncModeValue = 'full';
+ } else {
+ $syncModeValue = 'incremental';
+ }
return [
'configured' => false,
- 'syncMode' => $minutesBack === 0 ? 'full' : 'incremental',
+ 'syncMode' => $syncModeValue,
'timeWindow' => $minutesBack,
- 'message' => 'Error getting sync status: ' . $e->getMessage()
+ 'message' => 'Error getting sync status: '.$e->getMessage(),
];
- }
- }
+ }//end try
+
+ }//end getSyncStatusWithErrorHandling()
/**
* Updates the organisatie object's @self metadata to set owner to the organisation entity UUID
*
- * @param object $organisatieObject The organisatie object to update
- * @param object $organisationEntity The organisation entity
- * @param string $register The register ID
- * @param string $organizationSchema The organization schema ID
+ * @param object $organisatieObject The organisatie object to update.
+ * @param object $organisationEntity The organisation entity.
+ * @param string $register The register ID.
+ * @param string $organizationSchema The organization schema ID.
+ *
* @return void
*/
- private function updateOrganisatieObjectOwner(object $organisatieObject, object $organisationEntity, string $register, string $organizationSchema): void
- {
+ private function updateOrganisatieObjectOwner(
+ object $organisatieObject,
+ object $organisationEntity,
+ string $register,
+ string $organizationSchema
+ ): void {
try {
- $organisatieId = $organisatieObject->getUuid();
+ $organisatieId = $organisatieObject->getUuid();
$organisationEntityUuid = $organisationEntity->getUuid();
- $this->logger->info('OrganizationSyncService: Updating organisatie object owner and organisation', [
- 'organisatieId' => $organisatieId,
- 'organisationEntityUuid' => $organisationEntityUuid,
- 'register' => $register,
- 'schema' => $organizationSchema
- ]);
+ $this->logger->info(
+ 'OrganizationSyncService: Updating organisatie object owner and organisation',
+ [
+ 'organisatieId' => $organisatieId,
+ 'organisationEntityUuid' => $organisationEntityUuid,
+ 'register' => $register,
+ 'schema' => $organizationSchema,
+ ]
+ );
- // Get the current object data
+ // Get the current object data.
$currentObject = $organisatieObject->jsonSerialize();
- // Get current @self metadata or create new
- $selfMetadata = $currentObject['@self'] ?? [];
+ // Get current @self metadata or create new.
+ $selfMetadata = ($currentObject['@self'] ?? []);
- // Check if both owner and organisation are already set correctly
- $ownerAlreadySet = ($selfMetadata['owner'] ?? null) === $organisationEntityUuid;
+ // Check if both owner and organisation are already set correctly.
+ $ownerAlreadySet = ($selfMetadata['owner'] ?? null) === $organisationEntityUuid;
$organisationAlreadySet = ($currentObject['organisation'] ?? null) === $organisationEntityUuid;
- if ($ownerAlreadySet && $organisationAlreadySet) {
- $this->logger->debug('OrganizationSyncService: Owner and organisation already set correctly, skipping update', [
- 'organisatieId' => $organisatieId,
- 'organisationEntityUuid' => $organisationEntityUuid
- ]);
+ if ($ownerAlreadySet !== false && $organisationAlreadySet === true) {
+ $this->logger->debug(
+ 'OrganizationSyncService: Owner and organisation already set correctly, skipping update',
+ [
+ 'organisatieId' => $organisatieId,
+ 'organisationEntityUuid' => $organisationEntityUuid,
+ ]
+ );
return;
}
- // Update the owner field in @self metadata to the organisation entity UUID
+ // Update the owner field in @self metadata to the organisation entity UUID.
$selfMetadata['owner'] = $organisationEntityUuid;
- // Update the organisation field in @self metadata (so organisation owns itself)
+ // Update the organisation field in @self metadata (so organisation owns itself).
$selfMetadata['organisation'] = $organisationEntityUuid;
- // Update the organisation property to the organisation entity UUID (so organisation owns itself)
+ // Update the organisation property to the organisation entity UUID (so organisation owns itself).
$currentObject['organisation'] = $organisationEntityUuid;
- // Update the object with the new @self metadata
+ // Update the object with the new @self metadata.
$currentObject['@self'] = $selfMetadata;
$organisatieObject->setObject($currentObject);
- // Also update the entity's owner and organisation fields directly
- // These are separate from the object data and control multi-tenancy
+ // Also update the entity's owner and organisation fields directly.
+ // These are separate from the object data and control multi-tenancy.
$organisatieObject->setOwner($organisationEntityUuid);
$organisatieObject->setOrganisation($organisationEntityUuid);
- // Save using ObjectEntityMapper directly to bypass validation and ensure metadata is persisted
+ // Save using ObjectEntityMapper directly to bypass validation and ensure metadata is persisted.
$objectMapper = \OC::$server->get('OCA\OpenRegister\Db\ObjectEntityMapper');
$objectMapper->update($organisatieObject);
- $this->logger->info('OrganizationSyncService: Successfully updated organisatie object owner and organisation', [
- 'organisatieId' => $organisatieId,
- 'organisationEntityUuid' => $organisationEntityUuid,
- 'ownerSet' => $selfMetadata['owner'],
- 'organisationSet' => $currentObject['organisation']
- ]);
-
+ $this->logger->info(
+ 'OrganizationSyncService: Successfully updated organisatie object owner and organisation',
+ [
+ 'organisatieId' => $organisatieId,
+ 'organisationEntityUuid' => $organisationEntityUuid,
+ 'ownerSet' => $selfMetadata['owner'],
+ 'organisationSet' => $currentObject['organisation'],
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('OrganizationSyncService: Failed to update organisatie object owner and organisation', [
- 'organisatieId' => $organisatieObject->getUuid(),
- 'organisationEntityUuid' => $organisationEntity->getUuid(),
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine()
- ]);
- }
- }
+ $this->logger->error(
+ 'OrganizationSyncService: Failed to update organisatie object owner and organisation',
+ [
+ 'organisatieId' => $organisatieObject->getUuid(),
+ 'organisationEntityUuid' => $organisationEntity->getUuid(),
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]
+ );
+ }//end try
+
+ }//end updateOrganisatieObjectOwner()
/**
* Updates the contactpersoon object's @self metadata to set owner to the user UID
*
- * @param object $contactObject The contactpersoon object to update
- * @param string $userUID The user UID to set as owner
- * @param string $register The register ID
- * @param string $contactSchema The contact schema ID
- * @param string|null $organizationUuidOverride Optional organization UUID to use (from caller context)
+ * @param object $contactObject The contactpersoon object to update.
+ * @param string $userUID The user UID to set as owner.
+ * @param string $register The register ID.
+ * @param string $contactSchema The contact schema ID.
+ * @param string|null $organizationUuidOverride Optional organization UUID override.
+ *
* @return void
*/
- private function updateContactpersoonObjectOwner(object $contactObject, string $userUID, string $register, string $contactSchema, ?string $organizationUuidOverride = null): void
- {
+ private function updateContactpersoonObjectOwner(
+ object $contactObject,
+ string $userUID,
+ string $register,
+ string $contactSchema,
+ ?string $organizationUuidOverride=null
+ ): void {
try {
$contactId = $contactObject->getUuid();
- $this->logger->info('OrganizationSyncService: Updating contactpersoon object owner', [
- 'contactId' => $contactId,
- 'userUID' => $userUID,
- 'register' => $register,
- 'schema' => $contactSchema,
- 'organizationUuidOverride' => $organizationUuidOverride
- ]);
+ $this->logger->info(
+ 'OrganizationSyncService: Updating contactpersoon object owner',
+ [
+ 'contactId' => $contactId,
+ 'userUID' => $userUID,
+ 'register' => $register,
+ 'schema' => $contactSchema,
+ 'organizationUuidOverride' => $organizationUuidOverride,
+ ]
+ );
- // Get the current object data
+ // Get the current object data.
$currentObject = $contactObject->getObject();
- // Get current @self metadata or create new
- $selfMetadata = $currentObject['@self'] ?? [];
+ // Get current @self metadata or create new.
+ $selfMetadata = ($currentObject['@self'] ?? []);
- // Update the owner field to the user UID
+ // Update the owner field to the user UID.
$selfMetadata['owner'] = $userUID;
- // Set the organisation field in @self metadata to the organization UUID
- // Use override if provided, otherwise try to get from object data
- $organizationUuid = $organizationUuidOverride ?? $currentObject['organisation'] ?? $currentObject['organisatie'] ?? '';
- if (!empty($organizationUuid)) {
+ // Set the organisation field in @self metadata to the organization UUID.
+ // Use override if provided, otherwise try to get from object data.
+ $organizationUuid = ($organizationUuidOverride ?? $currentObject['organisation'] ?? $currentObject['organisatie'] ?? '');
+ if (empty($organizationUuid) === false) {
$selfMetadata['organisation'] = $organizationUuid;
- $this->logger->info('OrganizationSyncService: Setting @self.organisation metadata', [
- 'contactId' => $contactId,
- 'organizationUuid' => $organizationUuid,
- 'source' => $organizationUuidOverride ? 'override' : 'object'
- ]);
+ if (empty($organizationUuidOverride) === false) {
+ $sourceValue = 'override';
+ } else {
+ $sourceValue = 'object';
+ }
+
+ $this->logger->info(
+ 'OrganizationSyncService: Setting @self.organisation metadata',
+ [
+ 'contactId' => $contactId,
+ 'organizationUuid' => $organizationUuid,
+ 'source' => $sourceValue,
+ ]
+ );
} else {
- $this->logger->warning('OrganizationSyncService: No organization UUID found for contact person', [
- 'contactId' => $contactId,
- 'contactData' => $currentObject
- ]);
+ $this->logger->warning(
+ 'OrganizationSyncService: No organization UUID found for contact person',
+ [
+ 'contactId' => $contactId,
+ 'contactData' => $currentObject,
+ ]
+ );
+ }//end if
+
+ // Restore the organisatie field if it was removed during username save.
+ // This is the safety net that ensures the contact→org link is never lost.
+ if (empty($organizationUuid) === false && empty($currentObject['organisatie']) === true) {
+ $currentObject['organisatie'] = $organizationUuid;
+ $this->logger->info(
+ 'OrganizationSyncService: Restored missing organisatie field on contact person',
+ [
+ 'contactId' => $contactId,
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
}
- // Update the object with the new @self metadata
+ // Update the object with the new @self metadata.
$currentObject['@self'] = $selfMetadata;
$contactObject->setObject($currentObject);
- // Also update the entity's owner and organisation fields directly
- // These are separate from the object data and control multi-tenancy
+ // Also update the entity's owner and organisation fields directly.
+ // These are separate from the object data and control multi-tenancy.
$contactObject->setOwner($userUID);
- if (!empty($organizationUuid)) {
+ if (empty($organizationUuid) === false) {
$contactObject->setOrganisation($organizationUuid);
}
- // Save using ObjectEntityMapper directly to bypass validation and ensure metadata is persisted
+ // Save using ObjectEntityMapper directly to bypass validation and ensure metadata is persisted.
$objectMapper = \OC::$server->get('OCA\OpenRegister\Db\ObjectEntityMapper');
$objectMapper->update($contactObject);
- $this->logger->info('OrganizationSyncService: Successfully updated contactpersoon object owner and organisation', [
- 'contactId' => $contactId,
- 'userUID' => $userUID,
- 'ownerSet' => $selfMetadata['owner'],
- 'organisationSet' => $selfMetadata['organisation'] ?? 'not set'
- ]);
-
+ $this->logger->info(
+ 'OrganizationSyncService: Successfully updated contactpersoon object owner and organisation',
+ [
+ 'contactId' => $contactId,
+ 'userUID' => $userUID,
+ 'ownerSet' => $selfMetadata['owner'],
+ 'organisationSet' => ($selfMetadata['organisation'] ?? 'not set'),
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('OrganizationSyncService: Failed to update contactpersoon object owner', [
- 'contactId' => $contactObject->getUuid(),
- 'userUID' => $userUID,
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine()
- ]);
- }
- }
-}
+ $this->logger->error(
+ 'OrganizationSyncService: Failed to update contactpersoon object owner',
+ [
+ 'contactId' => $contactObject->getUuid(),
+ 'userUID' => $userUID,
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]
+ );
+ }//end try
+
+ }//end updateContactpersoonObjectOwner()
+}//end class
diff --git a/lib/Service/ProgressTracker.php b/lib/Service/ProgressTracker.php
index 5a273171..99229a0a 100644
--- a/lib/Service/ProgressTracker.php
+++ b/lib/Service/ProgressTracker.php
@@ -1,15 +1,15 @@
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT: 1.0.0
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
@@ -23,12 +23,12 @@
/**
* Service for tracking and reporting progress of long-running operations
- *
+ *
* @category Service
* @package OCA\SoftwareCatalog\Service
* @author Conduction b.v.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT: 1.0.0
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
class ProgressTracker
@@ -37,39 +37,39 @@ class ProgressTracker
* Operation phases and their relative weights for progress calculation
*/
private const PHASES = [
- 'initializing' => ['weight' => 5, 'description' => 'Initializing'],
- 'validating' => ['weight' => 5, 'description' => 'Validating file'],
- 'parsing' => ['weight' => 10, 'description' => 'Parsing ArchiMate file'],
- 'analyzing' => ['weight' => 5, 'description' => 'Analyzing structure'],
- 'caching' => ['weight' => 10, 'description' => 'Loading existing objects'],
- 'processing_elements' => ['weight' => 30, 'description' => 'Processing elements'],
+ 'initializing' => ['weight' => 5, 'description' => 'Initializing'],
+ 'validating' => ['weight' => 5, 'description' => 'Validating file'],
+ 'parsing' => ['weight' => 10, 'description' => 'Parsing ArchiMate file'],
+ 'analyzing' => ['weight' => 5, 'description' => 'Analyzing structure'],
+ 'caching' => ['weight' => 10, 'description' => 'Loading existing objects'],
+ 'processing_elements' => ['weight' => 30, 'description' => 'Processing elements'],
'processing_relationships' => ['weight' => 15, 'description' => 'Processing relationships'],
'processing_organizations' => ['weight' => 10, 'description' => 'Processing organizations'],
- 'processing_views' => ['weight' => 10, 'description' => 'Processing views'],
- 'finalizing' => ['weight' => 5, 'description' => 'Finalizing import'],
- 'completed' => ['weight' => 0, 'description' => 'Completed']
+ 'processing_views' => ['weight' => 10, 'description' => 'Processing views'],
+ 'finalizing' => ['weight' => 5, 'description' => 'Finalizing import'],
+ 'completed' => ['weight' => 0, 'description' => 'Completed'],
];
/**
* Current progress state
- *
+ *
* @var array
*/
private array $progress = [
- 'operation_id' => null,
- 'operation_type' => null,
- 'phase' => 'initializing',
- 'phase_description' => 'Initializing',
- 'total_items' => 0,
- 'processed_items' => 0,
- 'current_item_type' => null,
- 'current_item_name' => null,
- 'percentage' => 0,
- 'start_time' => null,
+ 'operation_id' => null,
+ 'operation_type' => null,
+ 'phase' => 'initializing',
+ 'phase_description' => 'Initializing',
+ 'total_items' => 0,
+ 'processed_items' => 0,
+ 'current_item_type' => null,
+ 'current_item_name' => null,
+ 'percentage' => 0,
+ 'start_time' => null,
'estimated_completion' => null,
- 'errors' => [],
- 'warnings' => [],
- 'statistics' => []
+ 'errors' => [],
+ 'warnings' => [],
+ 'statistics' => [],
];
/**
@@ -82,59 +82,62 @@ public function __construct(
private readonly ISession $session,
private readonly LoggerInterface $logger
) {
- }
+ }//end __construct()
/**
* Start tracking a new operation
*
* @param string $operationType Type of operation (import, export)
* @param array $options Operation options and metadata
- *
+ *
* @return string Unique operation ID
*/
- public function startOperation(string $operationType, array $options = []): string
+ public function startOperation(string $operationType, array $options=[]): string
{
- $operationId = uniqid($operationType . '_', true);
-
+ $operationId = uniqid(prefix: $operationType.'_', more_entropy: true);
+
$this->progress = [
- 'operation_id' => $operationId,
- 'operation_type' => $operationType,
- 'phase' => 'initializing',
- 'phase_description' => self::PHASES['initializing']['description'],
- 'total_items' => $options['total_items'] ?? 0,
- 'processed_items' => 0,
- 'current_item_type' => null,
- 'current_item_name' => null,
- 'percentage' => 0,
- 'start_time' => time(),
+ 'operation_id' => $operationId,
+ 'operation_type' => $operationType,
+ 'phase' => 'initializing',
+ 'phase_description' => self::PHASES['initializing']['description'],
+ 'total_items' => $options['total_items'] ?? 0,
+ 'processed_items' => 0,
+ 'current_item_type' => null,
+ 'current_item_name' => null,
+ 'percentage' => 0,
+ 'start_time' => time(),
'estimated_completion' => null,
- 'errors' => [],
- 'warnings' => [],
- 'statistics' => $options['statistics'] ?? []
+ 'errors' => [],
+ 'warnings' => [],
+ 'statistics' => $options['statistics'] ?? [],
];
$this->saveProgress();
-
- $this->logger->info('Started progress tracking', [
- 'operation_id' => $operationId,
- 'operation_type' => $operationType,
- 'total_items' => $this->progress['total_items']
- ]);
+
+ $this->logger->info(
+ 'Started progress tracking',
+ [
+ 'operation_id' => $operationId,
+ 'operation_type' => $operationType,
+ 'total_items' => $this->progress['total_items'],
+ ]
+ );
return $operationId;
- }
+ }//end startOperation()
/**
* Set the current phase of the operation
*
* @param string $phase Phase identifier
* @param array $data Additional phase data
- *
+ *
* @return void
*/
- public function setPhase(string $phase, array $data = []): void
+ public function setPhase(string $phase, array $data=[]): void
{
- if (!isset(self::PHASES[$phase])) {
+ if (isset(self::PHASES[$phase]) === false) {
$this->logger->warning('Unknown progress phase', ['phase' => $phase]);
return;
}
@@ -142,24 +145,27 @@ public function setPhase(string $phase, array $data = []): void
$this->progress['phase'] = $phase;
$this->progress['phase_description'] = self::PHASES[$phase]['description'];
- // Update total items if provided
- if (isset($data['total_items'])) {
+ // Update total items if provided.
+ if (isset($data['total_items']) === true) {
$this->progress['total_items'] = $data['total_items'];
}
- // Reset processed items for new phase if specified
- if (isset($data['reset_progress']) && $data['reset_progress']) {
+ // Reset processed items for new phase if specified.
+ if (isset($data['reset_progress']) === true && $data['reset_progress'] === true) {
$this->progress['processed_items'] = 0;
}
$this->updateProgress();
-
- $this->logger->debug('Progress phase updated', [
- 'operation_id' => $this->progress['operation_id'],
- 'phase' => $phase,
- 'total_items' => $this->progress['total_items']
- ]);
- }
+
+ $this->logger->debug(
+ 'Progress phase updated',
+ [
+ 'operation_id' => $this->progress['operation_id'],
+ 'phase' => $phase,
+ 'total_items' => $this->progress['total_items'],
+ ]
+ );
+ }//end setPhase()
/**
* Update progress within the current phase
@@ -167,10 +173,10 @@ public function setPhase(string $phase, array $data = []): void
* @param int $processedItems Number of items processed
* @param string $currentItem Name of current item being processed
* @param string $itemType Type of current item
- *
+ *
* @return void
*/
- public function updateProgress(int $processedItems = null, string $currentItem = null, string $itemType = null): void
+ public function updateProgress(int $processedItems=null, string $currentItem=null, string $itemType=null): void
{
if ($processedItems !== null) {
$this->progress['processed_items'] = $processedItems;
@@ -184,137 +190,151 @@ public function updateProgress(int $processedItems = null, string $currentItem =
$this->progress['current_item_type'] = $itemType;
}
- // Calculate overall percentage based on phase weights and current progress
+ // Calculate overall percentage based on phase weights and current progress.
$this->progress['percentage'] = $this->calculateOverallPercentage();
-
- // Calculate estimated completion time
+
+ // Calculate estimated completion time.
$this->progress['estimated_completion'] = $this->calculateEstimatedCompletion();
$this->saveProgress();
- }
+ }//end updateProgress()
/**
* Increment the processed items counter by one
*
* @param string $currentItem Name of current item being processed
* @param string $itemType Type of current item
- *
+ *
* @return void
*/
- public function incrementProgress(string $currentItem = null, string $itemType = null): void
+ public function incrementProgress(string $currentItem=null, string $itemType=null): void
{
$this->updateProgress(
- $this->progress['processed_items'] + 1,
- $currentItem,
- $itemType
+ processedItems: $this->progress['processed_items'] + 1,
+ currentItem: $currentItem,
+ itemType: $itemType
);
- }
+ }//end incrementProgress()
/**
* Add an error to the progress tracking
*
* @param string $message Error message
* @param array $context Error context
- *
+ *
* @return void
*/
- public function addError(string $message, array $context = []): void
+ public function addError(string $message, array $context=[]): void
{
$this->progress['errors'][] = [
- 'message' => $message,
- 'context' => $context,
- 'timestamp' => time()
+ 'message' => $message,
+ 'context' => $context,
+ 'timestamp' => time(),
];
$this->saveProgress();
-
- $this->logger->error('Progress tracking error', [
- 'operation_id' => $this->progress['operation_id'],
- 'message' => $message,
- 'context' => $context
- ]);
- }
+
+ $this->logger->error(
+ 'Progress tracking error',
+ [
+ 'operation_id' => $this->progress['operation_id'],
+ 'message' => $message,
+ 'context' => $context,
+ ]
+ );
+ }//end addError()
/**
* Add a warning to the progress tracking
*
* @param string $message Warning message
* @param array $context Warning context
- *
+ *
* @return void
*/
- public function addWarning(string $message, array $context = []): void
+ public function addWarning(string $message, array $context=[]): void
{
$this->progress['warnings'][] = [
- 'message' => $message,
- 'context' => $context,
- 'timestamp' => time()
+ 'message' => $message,
+ 'context' => $context,
+ 'timestamp' => time(),
];
$this->saveProgress();
- }
+ }//end addWarning()
/**
* Update operation statistics
*
* @param array $statistics Statistics to merge
- *
+ *
* @return void
*/
public function updateStatistics(array $statistics): void
{
$this->progress['statistics'] = array_merge($this->progress['statistics'], $statistics);
$this->saveProgress();
- }
+ }//end updateStatistics()
/**
* Complete the operation
*
* @param array $finalStatistics Final operation statistics
- *
+ *
* @return void
*/
- public function completeOperation(array $finalStatistics = []): void
+ public function completeOperation(array $finalStatistics=[]): void
{
$this->progress['phase'] = 'completed';
- $this->progress['phase_description'] = self::PHASES['completed']['description'];
- $this->progress['percentage'] = 100;
- $this->progress['processed_items'] = $this->progress['total_items'];
+ $this->progress['phase_description'] = self::PHASES['completed']['description'];
+ $this->progress['percentage'] = 100;
+ $this->progress['processed_items'] = $this->progress['total_items'];
$this->progress['estimated_completion'] = time();
-
- if (!empty($finalStatistics)) {
+
+ if (empty($finalStatistics) === false) {
$this->progress['statistics'] = array_merge($this->progress['statistics'], $finalStatistics);
}
$this->saveProgress();
-
- $this->logger->info('Operation completed', [
- 'operation_id' => $this->progress['operation_id'],
- 'duration' => time() - $this->progress['start_time'],
- 'total_items' => $this->progress['total_items'],
- 'errors' => count($this->progress['errors']),
- 'warnings' => count($this->progress['warnings'])
- ]);
- }
+
+ $this->logger->info(
+ 'Operation completed',
+ [
+ 'operation_id' => $this->progress['operation_id'],
+ 'duration' => time() - $this->progress['start_time'],
+ 'total_items' => $this->progress['total_items'],
+ 'errors' => count($this->progress['errors']),
+ 'warnings' => count($this->progress['warnings']),
+ ]
+ );
+ }//end completeOperation()
/**
* Get current progress state
*
* @param string $operationId Operation ID to get progress for
- *
+ *
* @return array|null Progress data or null if not found
*/
- public function getProgress(string $operationId = null): ?array
+ public function getProgress(string $operationId=null): ?array
{
- if ($operationId && $operationId !== $this->progress['operation_id']) {
- // Load progress from session for different operation
- $sessionKey = 'progress_' . $operationId;
+ if ($operationId !== null && $operationId !== $this->progress['operation_id']) {
+ // Load progress from session for different operation.
+ $sessionKey = 'progress_'.$operationId;
$storedProgress = $this->session->get($sessionKey);
- return $storedProgress ?: null;
+ if ($storedProgress !== null && $storedProgress !== false) {
+ return $storedProgress;
+ }
+
+ return null;
}
- return $this->progress['operation_id'] ? $this->progress : null;
- }
+ if ($this->progress['operation_id'] !== null) {
+ return $this->progress;
+ }
+
+ return null;
+ }//end getProgress()
/**
* Calculate overall percentage based on phase weights and current progress
@@ -323,36 +343,41 @@ public function getProgress(string $operationId = null): ?array
*/
private function calculateOverallPercentage(): int
{
- $totalWeight = array_sum(array_column(self::PHASES, 'weight'));
- $completedWeight = 0;
- $currentPhaseWeight = 0;
+ $totalWeight = array_sum(array_column(self::PHASES, 'weight'));
+ $completedWeight = 0;
+ $currentPhaseWeight = 0;
$currentPhaseProgress = 0;
- $phases = array_keys(self::PHASES);
+ $phases = array_keys(self::PHASES);
$currentPhaseIndex = array_search($this->progress['phase'], $phases);
- // Add weight of all completed phases
+ // Add weight of all completed phases.
for ($i = 0; $i < $currentPhaseIndex; $i++) {
$completedWeight += self::PHASES[$phases[$i]]['weight'];
}
- // Calculate progress within current phase
+ // Calculate progress within current phase.
if ($currentPhaseIndex !== false) {
$currentPhaseWeight = self::PHASES[$this->progress['phase']]['weight'];
-
+
if ($this->progress['total_items'] > 0) {
- $currentPhaseProgress = ($this->progress['processed_items'] / $this->progress['total_items']) * $currentPhaseWeight;
+ $itemRatio = $this->progress['processed_items'] / $this->progress['total_items'];
+ $currentPhaseProgress = $itemRatio * $currentPhaseWeight;
} else {
- // If no items to process, consider phase as complete
+ // If no items to process, consider phase as complete.
$currentPhaseProgress = $currentPhaseWeight;
}
}
$overallProgress = $completedWeight + $currentPhaseProgress;
- $percentage = $totalWeight > 0 ? intval(($overallProgress / $totalWeight) * 100) : 0;
+ if ($totalWeight > 0) {
+ $percentage = intval(($overallProgress / $totalWeight) * 100);
+ } else {
+ $percentage = 0;
+ }
return min(100, max(0, $percentage));
- }
+ }//end calculateOverallPercentage()
/**
* Calculate estimated completion time
@@ -365,11 +390,11 @@ private function calculateEstimatedCompletion(): ?int
return null;
}
- $elapsed = time() - $this->progress['start_time'];
+ $elapsed = time() - $this->progress['start_time'];
$estimatedTotal = ($elapsed / $this->progress['percentage']) * 100;
-
+
return $this->progress['start_time'] + intval($estimatedTotal);
- }
+ }//end calculateEstimatedCompletion()
/**
* Save progress to session
@@ -378,23 +403,23 @@ private function calculateEstimatedCompletion(): ?int
*/
private function saveProgress(): void
{
- if ($this->progress['operation_id']) {
- $sessionKey = 'progress_' . $this->progress['operation_id'];
+ if ($this->progress['operation_id'] !== null) {
+ $sessionKey = 'progress_'.$this->progress['operation_id'];
$this->session->set($sessionKey, $this->progress);
}
- }
+ }//end saveProgress()
/**
* Clean up old progress entries from session
*
* @param int $maxAge Maximum age in seconds (default: 1 hour)
- *
+ *
* @return void
*/
- public function cleanupOldProgress(int $maxAge = 3600): void
+ public function cleanupOldProgress(int $maxAge=3600): void
{
- // Note: This would need to iterate through session keys to find and clean old progress entries
- // Implementation depends on session storage capabilities
+ // Note: This would need to iterate through session keys to find and clean old progress entries.
+ // Implementation depends on session storage capabilities.
$this->logger->debug('Progress cleanup requested', ['max_age' => $maxAge]);
- }
-}
\ No newline at end of file
+ }//end cleanupOldProgress()
+}//end class
diff --git a/lib/Service/SettingsService.php b/lib/Service/SettingsService.php
index 91381b0c..fc18f71b 100644
--- a/lib/Service/SettingsService.php
+++ b/lib/Service/SettingsService.php
@@ -42,12 +42,13 @@
*/
class SettingsService
{
+
/**
* The application name for identification and configuration purposes
*
* @var string The name of the app
*/
- private string $_appName;
+ private string $appName;
/**
* Cache for schema IDs by object type to avoid repeated database queries
@@ -80,11 +81,11 @@ class SettingsService
/**
* SettingsService constructor
*
- * @param IAppConfig $config App configuration interface
- * @param IRequest $request Request interface
- * @param ContainerInterface $container Container for dependency injection
- * @param IAppManager $appManager App manager interface
- * @param LoggerInterface $logger Logger interface
+ * @param IAppConfig $config App configuration interface
+ * @param IRequest $request Request interface
+ * @param ContainerInterface $container Container for dependency injection
+ * @param IAppManager $appManager App manager interface
+ * @param LoggerInterface $logger Logger interface
*/
public function __construct(
private readonly IAppConfig $config,
@@ -94,7 +95,7 @@ public function __construct(
private readonly LoggerInterface $logger
) {
$this->_appName = 'softwarecatalog';
- }
+ }//end __construct()
/**
* Checks if OpenRegister is installed and meets version requirements
@@ -103,9 +104,9 @@ public function __construct(
*
* @return bool True if OpenRegister is installed and meets version requirements
*/
- public function isOpenRegisterInstalled(?string $minVersion = self::MIN_OPENREGISTER_VERSION): bool
+ public function isOpenRegisterInstalled(?string $minVersion=self::MIN_OPENREGISTER_VERSION): bool
{
- if (!$this->appManager->isInstalled(self::OPENREGISTER_APP_ID)) {
+ if ($this->appManager->isInstalled(appId: self::OPENREGISTER_APP_ID) === false) {
return false;
}
@@ -115,7 +116,7 @@ public function isOpenRegisterInstalled(?string $minVersion = self::MIN_OPENREGI
$currentVersion = $this->appManager->getAppVersion(self::OPENREGISTER_APP_ID);
return version_compare($currentVersion, $minVersion, '>=');
- }
+ }//end isOpenRegisterInstalled()
/**
* Checks if OpenRegister is enabled
@@ -125,7 +126,7 @@ public function isOpenRegisterInstalled(?string $minVersion = self::MIN_OPENREGI
public function isOpenRegisterEnabled(): bool
{
return $this->appManager->isEnabledForUser(self::OPENREGISTER_APP_ID);
- }
+ }//end isOpenRegisterEnabled()
/**
* Attempts to retrieve the OpenRegister service from the container
@@ -136,12 +137,12 @@ public function isOpenRegisterEnabled(): bool
*/
public function getObjectService(): ?\OCA\OpenRegister\Service\ObjectService
{
- if (in_array('openregister', $this->appManager->getInstalledApps())) {
+ if (in_array(needle: 'openregister', haystack: $this->appManager->getInstalledApps()) === true) {
return $this->container->get('OCA\OpenRegister\Service\ObjectService');
}
throw new \RuntimeException('OpenRegister service is not available.');
- }
+ }//end getObjectService()
/**
* Get the OpenRegister RegisterService.
@@ -150,12 +151,12 @@ public function getObjectService(): ?\OCA\OpenRegister\Service\ObjectService
*/
public function getRegisterService(): ?\OCA\OpenRegister\Service\RegisterService
{
- if (in_array('openregister', $this->appManager->getInstalledApps())) {
+ if (in_array(needle: 'openregister', haystack: $this->appManager->getInstalledApps()) === true) {
return $this->container->get('OCA\OpenRegister\Service\RegisterService');
}
throw new \RuntimeException('OpenRegister RegisterService is not available.');
- }
+ }//end getRegisterService()
/**
* Attempts to retrieve the Configuration service from the container
@@ -166,12 +167,12 @@ public function getRegisterService(): ?\OCA\OpenRegister\Service\RegisterService
*/
public function getConfigurationService(): ?\OCA\OpenRegister\Service\ConfigurationService
{
- if (in_array('openregister', $this->appManager->getInstalledApps())) {
+ if (in_array(needle: 'openregister', haystack: $this->appManager->getInstalledApps()) === true) {
return $this->container->get('OCA\OpenRegister\Service\ConfigurationService');
}
throw new \RuntimeException('Configuration service is not available.');
- }
+ }//end getConfigurationService()
/**
* Retrieve the current settings
@@ -182,33 +183,49 @@ public function getConfigurationService(): ?\OCA\OpenRegister\Service\Configurat
*/
public function getSettings(): array
{
- // Initialize the data array
+ // Initialize the data array.
$data = [];
- // Define the register-specific configuration
+ // Define the register-specific configuration.
$data['registerTypes'] = [
- 'amef' => [
- 'name' => 'AMEF',
+ 'amef' => [
+ 'name' => 'AMEF',
'description' => 'AMEF register for architectural elements',
- 'objectTypes' => ['organization', 'element', 'relation', 'view', 'model', 'property', 'property-definition'] // Complete AMEF object types
+ 'objectTypes' => ['organization', 'element', 'relation', 'view', 'model', 'property', 'property-definition'],
+ // Complete AMEF object types.
],
'voorzieningen' => [
- 'name' => 'Voorzieningen',
+ 'name' => 'Voorzieningen',
'description' => 'Voorzieningen register for software catalog services',
- 'objectTypes' => ['sector', 'suite', 'dienst', 'kwetsbaarheid', 'contactpersoon', 'organisatie', 'gebruik', 'contract', 'koppeling', 'beoordeeling', 'module', 'compliancy', 'moduleVersie'] // All voorzieningen schemas
- ]
+ // All voorzieningen schemas.
+ 'objectTypes' => [
+ 'sector',
+ 'suite',
+ 'dienst',
+ 'kwetsbaarheid',
+ 'contactpersoon',
+ 'organisatie',
+ 'gebruik',
+ 'contract',
+ 'koppeling',
+ 'beoordeeling',
+ 'module',
+ 'compliancy',
+ 'moduleVersie',
+ ],
+ ],
];
- // Deprecated: For backward compatibility only - use registerTypes instead
+ // Deprecated: For backward compatibility only - use registerTypes instead.
$data['objectTypes'] = [
'organization',
'contact',
];
- $data['openRegisters'] = false;
+ $data['openRegisters'] = false;
$data['availableRegisters'] = [];
- // Check if the OpenRegister service is available
+ // Check if the OpenRegister service is available.
try {
$openRegisters = $this->getObjectService();
if ($openRegisters !== null) {
@@ -217,31 +234,36 @@ public function getSettings(): array
// Add additional error handling for OpenRegister internal errors.
try {
$registerService = $this->getRegisterService();
- $rawRegisters = $registerService->findAll();
-
- // Convert Register entities to arrays first
- $rawRegisters = array_map(function($register) {
- return is_object($register) && method_exists($register, 'jsonSerialize')
- ? $register->jsonSerialize()
- : (array)$register;
- }, $rawRegisters);
+ $rawRegisters = $registerService->findAll();
+
+ // Convert Register entities to arrays first.
+ $rawRegisters = array_map(
+ function ($register) {
+ if (is_object($register) === true && method_exists($register, 'jsonSerialize') === true) {
+ return $register->jsonSerialize();
+ } else {
+ return (array) $register;
+ }
+ },
+ $rawRegisters
+ );
- // Collect all schema IDs that need to be fetched (batch approach)
+ // Collect all schema IDs that need to be fetched (batch approach).
$allSchemaIds = [];
foreach ($rawRegisters as $register) {
foreach (($register['schemas'] ?? []) as $schema) {
- if (is_int($schema) || is_numeric($schema)) {
- $allSchemaIds[] = (int)$schema;
+ if (is_int($schema) === true || is_numeric($schema) === true) {
+ $allSchemaIds[] = (int) $schema;
}
}
}
- // Batch fetch all schemas in one query if we have IDs
+ // Batch fetch all schemas in one query if we have IDs.
$schemaMap = [];
- if (!empty($allSchemaIds)) {
+ if (empty($allSchemaIds) === false) {
try {
$schemaMapper = $this->container->get(\OCA\OpenRegister\Db\SchemaMapper::class);
- $schemas = $schemaMapper->findMultipleOptimized(array_unique($allSchemaIds));
+ $schemas = $schemaMapper->findMultipleOptimized(array_unique($allSchemaIds));
foreach ($schemas as $schema) {
$schemaMap[$schema->getId()] = $schema->jsonSerialize();
}
@@ -250,110 +272,126 @@ public function getSettings(): array
}
}
- // Map schema details back to registers
- $rawRegisters = array_map(function($register) use ($schemaMap) {
- if (isset($register['schemas']) && is_array($register['schemas'])) {
- $schemaDetails = [];
- foreach ($register['schemas'] as $schema) {
- if (is_array($schema) && isset($schema['slug'])) {
- // Schema is already a full object
- $schemaDetails[] = $schema;
- } elseif (is_int($schema) || is_numeric($schema)) {
- // Schema is an ID - get from pre-fetched map
- if (isset($schemaMap[(int)$schema])) {
- $schemaDetails[] = $schemaMap[(int)$schema];
+ // Map schema details back to registers.
+ $rawRegisters = array_map(
+ function ($register) use ($schemaMap) {
+ if (isset($register['schemas']) === true && is_array($register['schemas']) === true) {
+ $schemaDetails = [];
+ foreach ($register['schemas'] as $schema) {
+ if (is_array($schema) === true && isset($schema['slug']) === true) {
+ // Schema is already a full object.
+ $schemaDetails[] = $schema;
+ } else if (is_int($schema) === true || is_numeric($schema) === true) {
+ // Schema is an ID - get from pre-fetched map.
+ if (isset($schemaMap[(int) $schema]) === true) {
+ $schemaDetails[] = $schemaMap[(int) $schema];
+ }
+ }
}
+
+ $register['schemas'] = $schemaDetails;
}
- }
- $register['schemas'] = $schemaDetails;
- }
- return $register;
- }, $rawRegisters);
-
- // Filter schemas to remove properties field for cleaner response
- $data['availableRegisters'] = array_map(function($register) {
- if (isset($register['schemas']) && is_array($register['schemas'])) {
- $register['schemas'] = array_map(function($schema) {
- // Keep only essential schema fields, remove properties
- if (is_array($schema)) {
- return array_filter($schema, function($key) {
- return !in_array($key, ['properties']);
- }, ARRAY_FILTER_USE_KEY);
+
+ return $register;
+ },
+ $rawRegisters
+ );
+
+ // Filter schemas to remove properties field for cleaner response.
+ $data['availableRegisters'] = array_map(
+ function ($register) {
+ if (isset($register['schemas']) === true && is_array($register['schemas']) === true) {
+ $register['schemas'] = array_map(
+ function ($schema) {
+ // Keep only essential schema fields, remove properties.
+ if (is_array($schema) === true) {
+ return array_filter(
+ $schema,
+ function ($key) {
+ return in_array($key, ['properties']) === false;
+ },
+ ARRAY_FILTER_USE_KEY
+ );
+ }
+
+ return $schema;
+ },
+ $register['schemas']
+ );
}
- return $schema;
- }, $register['schemas']);
- }
- return $register;
- }, $rawRegisters);
+
+ return $register;
+ },
+ $rawRegisters
+ );
} catch (\TypeError $e) {
- // Handle OpenRegister internal errors (e.g. RegisterMapper parameter issues)
+ // Handle OpenRegister internal errors (e.g. RegisterMapper parameter issues).
$this->logger->warning(
'OpenRegister internal error - using empty registers list',
[
'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine()
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
]
);
$data['availableRegisters'] = [];
} catch (\Exception $e) {
- // Handle any other OpenRegister errors
+ // Handle any other OpenRegister errors.
$this->logger->warning(
'OpenRegister getRegisters() failed - using empty registers list',
[
'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine()
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
]
);
$data['availableRegisters'] = [];
- }
- }
+ }//end try
+ }//end if
} catch (\RuntimeException $e) {
- // Service not available, continue with default values
+ // Service not available, continue with default values.
$this->logger->info(
'OpenRegister service not available',
[
- 'exception' => $e->getMessage()
+ 'exception' => $e->getMessage(),
]
);
- }
+ }//end try
- // Build defaults array dynamically based on register types and their object types
+ // Build defaults array dynamically based on register types and their object types.
$defaults = [];
foreach ($data['registerTypes'] as $registerType => $config) {
foreach ($config['objectTypes'] as $objectType) {
- // Always use openregister as source
- $defaults["{$registerType}_{$objectType}_source"] = 'openregister';
- $defaults["{$registerType}_{$objectType}_schema"] = '';
+ // Always use openregister as source.
+ $defaults["{$registerType}_{$objectType}_source"] = 'openregister';
+ $defaults["{$registerType}_{$objectType}_schema"] = '';
$defaults["{$registerType}_{$objectType}_register"] = '';
}
}
- // Also maintain backward compatibility for the old structure
+ // Also maintain backward compatibility for the old structure.
foreach ($data['objectTypes'] as $type) {
- $defaults["{$type}_source"] = 'openregister';
- $defaults["{$type}_schema"] = '';
+ $defaults["{$type}_source"] = 'openregister';
+ $defaults["{$type}_schema"] = '';
$defaults["{$type}_register"] = '';
}
- // Note: Old individual config keys are no longer used
- // They are maintained only for backward compatibility during migration
-
- // Get the current values from the configuration
+ // Note: Old individual config keys are no longer used.
+ // They are maintained only for backward compatibility during migration.
+ // Get the current values from the configuration.
try {
foreach ($defaults as $key => $defaultValue) {
$data['configuration'][$key] = $this->config->getValueString($this->_appName, $key, $defaultValue);
}
- // Add catalog location
+ // Add catalog location.
$data['catalogLocation'] = $this->getCatalogLocation();
return $data;
} catch (\Exception $e) {
- throw new \RuntimeException('Failed to retrieve settings: ' . $e->getMessage());
+ throw new \RuntimeException('Failed to retrieve settings: '.$e->getMessage());
}
- }
+ }//end getSettings()
/**
* Update the settings configuration
@@ -367,41 +405,48 @@ public function getSettings(): array
public function updateSettings(array $data): array
{
try {
- // Update each setting in the configuration
+ // Update each setting in the configuration.
foreach ($data as $key => $value) {
- // Skip empty keys
- if (empty($key)) {
- $this->logger->warning('Skipping empty key in updateSettings', [
- 'value' => $value
- ]);
+ // Skip empty keys.
+ if (empty($key) === true) {
+ $this->logger->warning(
+ 'Skipping empty key in updateSettings',
+ [
+ 'value' => $value,
+ ]
+ );
continue;
}
- // Handle arrays and objects by converting to JSON
- if (is_array($value) || is_object($value)) {
+ // Handle arrays and objects by converting to JSON.
+ if (is_array($value) === true || is_object($value) === true) {
$stringValue = json_encode($value);
} else {
- // Ensure value is converted to string as required by setValueString
- $stringValue = is_string($value) ? $value : (string) $value;
+ // Ensure value is converted to string as required by setValueString.
+ if (is_string($value) === true) {
+ $stringValue = $value;
+ } else {
+ $stringValue = (string) $value;
+ }
}
$this->config->setValueString($this->_appName, $key, $stringValue);
- // Retrieve the updated value to confirm the change
+ // Retrieve the updated value to confirm the change.
$data[$key] = $this->config->getValueString($this->_appName, $key);
- }
+ }//end foreach
$this->logger->info(
'Settings updated successfully',
[
- 'updatedKeys' => array_keys($data)
+ 'updatedKeys' => array_keys($data),
]
);
return $data;
} catch (\Exception $e) {
- throw new \RuntimeException('Failed to update settings: ' . $e->getMessage());
- }
- }
+ throw new \RuntimeException('Failed to update settings: '.$e->getMessage());
+ }//end try
+ }//end updateSettings()
/**
* Attempts to auto-configure registers and schemas
@@ -410,19 +455,21 @@ public function updateSettings(array $data): array
*
* @throws \RuntimeException If auto-configuration fails
*/
+
/**
* Auto-configure settings based on available registers and schemas
* This method now uses the consolidated auto-configuration logic
*
- * @param bool $force Whether to force reload regardless of version
- * @return array The auto-configuration results
+ * @param bool $force Whether to force reload regardless of version.
+ *
+ * @return array The auto-configuration results.
*
* @throws \RuntimeException If auto-configuration fails
*/
- public function autoConfigure(bool $force = false): array
+ public function autoConfigure(bool $force=false): array
{
- return $this->performConsolidatedAutoConfiguration($force);
- }
+ return $this->performConsolidatedAutoConfiguration(force: $force);
+ }//end autoConfigure()
/**
* Auto-configures settings specifically after importing the softwarecatalogus_register_magic.json
@@ -437,79 +484,96 @@ public function autoConfigure(bool $force = false): array
public function autoConfigureAfterImport(): array
{
try {
- // Check if auto-configuration has already been completed
+ // Check if auto-configuration has already been completed.
$autoConfigCompleted = $this->config->getValueString($this->_appName, 'auto_config_completed', 'false') === 'true';
- if ($autoConfigCompleted) {
+ if (empty($autoConfigCompleted) === false) {
$this->logger->info('Auto-configuration already completed, skipping');
return [];
}
$this->logger->info('Starting comprehensive auto-configuration after import');
- // Step 1: Create required user groups
+ // Step 1: Create required user groups.
$this->logger->info('Creating required user groups');
$this->createRequiredUserGroups();
$this->logger->info('User groups created successfully');
- // Step 2: Configure Voorzieningen using the consolidated method
+ // Step 2: Configure Voorzieningen using the consolidated method.
$this->logger->info('Running voorzieningen auto-configuration');
$voorzieningenResult = $this->configureVoorzieningen();
- if (!$voorzieningenResult['success']) {
- $this->logger->warning('Voorzieningen auto-configuration failed', [
- 'message' => $voorzieningenResult['message'] ?? 'Unknown error'
- ]);
+ if ($voorzieningenResult['success'] === false) {
+ $this->logger->warning(
+ 'Voorzieningen auto-configuration failed',
+ [
+ 'message' => $voorzieningenResult['message'] ?? 'Unknown error',
+ ]
+ );
return [];
}
- $this->logger->info('Voorzieningen auto-configuration completed successfully', [
- 'configured' => $voorzieningenResult['configured'] ?? []
- ]);
+ $this->logger->info(
+ 'Voorzieningen auto-configuration completed successfully',
+ [
+ 'configured' => $voorzieningenResult['configured'] ?? [],
+ ]
+ );
- // Step 3: Configure AMEF using the consolidated method
+ // Step 3: Configure AMEF using the consolidated method.
$this->logger->info('Running AMEF auto-configuration');
$amefResult = $this->configureAmef();
- if (!$amefResult['success']) {
- $this->logger->info('AMEF auto-configuration not completed', [
- 'message' => $amefResult['message'] ?? 'No AMEF register found'
- ]);
+ if ($amefResult['success'] === false) {
+ $this->logger->info(
+ 'AMEF auto-configuration not completed',
+ [
+ 'message' => $amefResult['message'] ?? 'No AMEF register found',
+ ]
+ );
} else {
- $this->logger->info('AMEF auto-configuration completed successfully', [
- 'configured' => $amefResult['configured'] ?? []
- ]);
+ $this->logger->info(
+ 'AMEF auto-configuration completed successfully',
+ [
+ 'configured' => $amefResult['configured'] ?? [],
+ ]
+ );
}
- // Step 4: Configure OpenCatalogi app settings for pages/menus/themes
+ // Step 4: Configure OpenCatalogi app settings for pages/menus/themes.
$this->logger->info('Running OpenCatalogi auto-configuration');
$openCatalogiResult = $this->configureOpenCatalogi();
- if ($openCatalogiResult['success']) {
- $this->logger->info('OpenCatalogi auto-configuration completed successfully', [
- 'configured' => $openCatalogiResult['configured'] ?? []
- ]);
+ if ($openCatalogiResult['success'] === true) {
+ $this->logger->info(
+ 'OpenCatalogi auto-configuration completed successfully',
+ [
+ 'configured' => $openCatalogiResult['configured'] ?? [],
+ ]
+ );
} else {
- $this->logger->info('OpenCatalogi auto-configuration skipped', [
- 'message' => $openCatalogiResult['message'] ?? 'OpenCatalogi not installed or not needed'
- ]);
+ $this->logger->info(
+ 'OpenCatalogi auto-configuration skipped',
+ [
+ 'message' => $openCatalogiResult['message'] ?? 'OpenCatalogi not installed or not needed',
+ ]
+ );
}
- // Mark auto-configuration as completed
+ // Mark auto-configuration as completed.
$this->config->setValueString($this->_appName, 'auto_config_completed', 'true');
$this->logger->info('Comprehensive auto-configuration marked as completed');
- // Return the consolidated configuration result
+ // Return the consolidated configuration result.
return [
- 'voorzieningen' => $voorzieningenResult,
- 'amef' => $amefResult,
- 'opencatalogi' => $openCatalogiResult,
- 'user_groups_created' => true
+ 'voorzieningen' => $voorzieningenResult,
+ 'amef' => $amefResult,
+ 'opencatalogi' => $openCatalogiResult,
+ 'user_groups_created' => true,
];
-
} catch (\Exception $e) {
- throw new \RuntimeException('Failed to auto-configure after import: ' . $e->getMessage());
- }
- }
+ throw new \RuntimeException('Failed to auto-configure after import: '.$e->getMessage());
+ }//end try
+ }//end autoConfigureAfterImport()
/**
* Configure OpenCatalogi app settings for pages, menus, and themes
@@ -522,25 +586,25 @@ public function autoConfigureAfterImport(): array
public function configureOpenCatalogi(): array
{
$result = [
- 'success' => false,
- 'message' => '',
- 'configured' => []
+ 'success' => false,
+ 'message' => '',
+ 'configured' => [],
];
try {
- // Check if opencatalogi app is installed
- if (!in_array('opencatalogi', $this->appManager->getInstalledApps())) {
+ // Check if opencatalogi app is installed.
+ if (in_array('opencatalogi', $this->appManager->getInstalledApps()) === false) {
$result['message'] = 'OpenCatalogi app is not installed';
return $result;
}
- // Get OpenRegister services
- $schemaMapper = $this->container->get('OCA\OpenRegister\Db\SchemaMapper');
+ // Get OpenRegister services.
+ $schemaMapper = $this->container->get('OCA\OpenRegister\Db\SchemaMapper');
$registerMapper = $this->container->get('OCA\OpenRegister\Db\RegisterMapper');
- // Find the publication register
+ // Find the publication register.
$publicationRegister = null;
- $registers = $registerMapper->findAll();
+ $registers = $registerMapper->findAll();
foreach ($registers as $register) {
if ($register->getSlug() === 'publication') {
$publicationRegister = $register;
@@ -555,78 +619,80 @@ public function configureOpenCatalogi(): array
$registerId = (string) $publicationRegister->getId();
- // Find page, menu, and theme schemas that have data in magic mapper tables
- // We look for schemas by slug and check if they have associated data
- $schemas = $schemaMapper->findAll();
- $pageSchemaId = null;
- $menuSchemaId = null;
+ // Find page, menu, and theme schemas that have data in magic mapper tables.
+ // We look for schemas by slug and check if they have associated data.
+ $schemas = $schemaMapper->findAll();
+ $pageSchemaId = null;
+ $menuSchemaId = null;
$themeSchemaId = null;
foreach ($schemas as $schema) {
- $slug = $schema->getSlug();
+ $slug = $schema->getSlug();
$schemaId = $schema->getId();
- // Check if this schema has a magic mapper table with data for register 1
- $tableName = 'oc_openregister_table_' . $registerId . '_' . $schemaId;
+ // Check if this schema has a magic mapper table with data for register 1.
+ $tableName = 'oc_openregister_table_'.$registerId.'_'.$schemaId;
- // Try to find schemas that have actual data
+ // Try to find schemas that have actual data.
if ($slug === 'page' && $pageSchemaId === null) {
- if ($this->tableHasData($tableName)) {
+ if ($this->tableHasData(tableName: $tableName) === true) {
$pageSchemaId = (string) $schemaId;
}
- } elseif ($slug === 'menu' && $menuSchemaId === null) {
- if ($this->tableHasData($tableName)) {
+ } else if ($slug === 'menu' && $menuSchemaId === null) {
+ if ($this->tableHasData(tableName: $tableName) === true) {
$menuSchemaId = (string) $schemaId;
}
- } elseif ($slug === 'theme' && $themeSchemaId === null) {
- if ($this->tableHasData($tableName)) {
+ } else if ($slug === 'theme' && $themeSchemaId === null) {
+ if ($this->tableHasData(tableName: $tableName) === true) {
$themeSchemaId = (string) $schemaId;
}
}
- }
+ }//end foreach
- // Set the opencatalogi app configuration
+ // Set the opencatalogi app configuration.
$configured = [];
if ($pageSchemaId !== null) {
$this->config->setValueString('opencatalogi', 'page_schema', $pageSchemaId);
$this->config->setValueString('opencatalogi', 'page_register', $registerId);
- $configured['page_schema'] = $pageSchemaId;
+ $configured['page_schema'] = $pageSchemaId;
$configured['page_register'] = $registerId;
}
if ($menuSchemaId !== null) {
$this->config->setValueString('opencatalogi', 'menu_schema', $menuSchemaId);
$this->config->setValueString('opencatalogi', 'menu_register', $registerId);
- $configured['menu_schema'] = $menuSchemaId;
+ $configured['menu_schema'] = $menuSchemaId;
$configured['menu_register'] = $registerId;
}
if ($themeSchemaId !== null) {
$this->config->setValueString('opencatalogi', 'theme_schema', $themeSchemaId);
$this->config->setValueString('opencatalogi', 'theme_register', $registerId);
- $configured['theme_schema'] = $themeSchemaId;
+ $configured['theme_schema'] = $themeSchemaId;
$configured['theme_register'] = $registerId;
}
- if (!empty($configured)) {
- $result['success'] = true;
+ if (empty($configured) === false) {
+ $result['success'] = true;
$result['configured'] = $configured;
- $result['message'] = 'OpenCatalogi configured successfully';
+ $result['message'] = 'OpenCatalogi configured successfully';
$this->logger->info('OpenCatalogi configuration set', $configured);
} else {
$result['message'] = 'No page/menu/theme schemas with data found';
}
-
} catch (\Exception $e) {
- $result['message'] = 'Failed to configure OpenCatalogi: ' . $e->getMessage();
- $this->logger->error('OpenCatalogi configuration failed', [
- 'exception' => $e->getMessage()
- ]);
- }
+ $result['message'] = 'Failed to configure OpenCatalogi: '.$e->getMessage();
+ $this->logger->error(
+ 'OpenCatalogi configuration failed',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
+ }//end try
return $result;
- }
+ }//end configureOpenCatalogi()
/**
* Check if a database table exists and has data
@@ -639,15 +705,15 @@ private function tableHasData(string $tableName): bool
{
try {
$connection = $this->container->get('OCP\IDBConnection');
- $sql = "SELECT COUNT(*) as cnt FROM {$tableName} WHERE _deleted IS NULL LIMIT 1";
- $stmt = $connection->executeQuery($sql);
- $row = $stmt->fetch();
- return ($row && (int) $row['cnt'] > 0);
+ $sql = "SELECT COUNT(*) as cnt FROM {$tableName} WHERE _deleted IS NULL LIMIT 1";
+ $stmt = $connection->executeQuery($sql);
+ $row = $stmt->fetch();
+ return ($row !== false && (int) $row['cnt'] > 0);
} catch (\Exception $e) {
- // Table doesn't exist or other error
+ // Table doesn't exist or other error.
return false;
}
- }
+ }//end tableHasData()
/**
* Gets the configured schema ID for a specific object type
@@ -658,84 +724,95 @@ private function tableHasData(string $tableName): bool
*/
public function getSchemaIdForObjectType(string $objectType): ?int
{
- // Check cache first for performance optimization
- if (array_key_exists($objectType, $this->schemaIdCache)) {
+ // Check cache first for performance optimization.
+ if (array_key_exists($objectType, $this->schemaIdCache) === true) {
$cachedValue = $this->schemaIdCache[$objectType];
- $this->logger->debug("SettingsService: Schema ID retrieved from cache", [
- 'objectType' => $objectType,
- 'cachedValue' => $cachedValue,
- 'fromCache' => true
- ]);
+ $this->logger->debug(
+ "SettingsService: Schema ID retrieved from cache",
+ [
+ 'objectType' => $objectType,
+ 'cachedValue' => $cachedValue,
+ 'fromCache' => true,
+ ]
+ );
return $cachedValue;
}
$startTime = microtime(true);
- $result = null;
+ $result = null;
$voorzieningenConfig = $this->getVoorzieningenConfig();
- $this->logger->debug("SettingsService: Starting schema ID lookup (cache miss)", [
- 'objectType' => $objectType,
- 'timestamp' => date('Y-m-d H:i:s')
- ]);
+ $this->logger->debug(
+ "SettingsService: Starting schema ID lookup (cache miss)",
+ [
+ 'objectType' => $objectType,
+ 'timestamp' => date('Y-m-d H:i:s'),
+ ]
+ );
- // First try register-specific configuration
- // Check for AMEF register specific schemas from JSON config
+ // First try register-specific configuration.
+ // Check for AMEF register specific schemas from JSON config.
$amefConfig = $this->config->getValueString($this->_appName, 'amef_config', '{}');
- if (!empty($amefConfig) && $amefConfig !== '{}') {
+ if (empty($amefConfig) === false && $amefConfig !== '{}') {
$decodedAmefConfig = json_decode($amefConfig, true);
- if (is_array($decodedAmefConfig)) {
- // Map object types to their corresponding AMEF config keys
+ if (is_array($decodedAmefConfig) === true) {
+ // Map object types to their corresponding AMEF config keys.
$amefKeyMap = [
- 'model' => 'model_schema',
- 'element' => 'element_schema',
- 'relationship' => 'relation_schema', // Note: relation vs relationship
- 'view' => 'view_schema',
- 'property_definition' => 'property_definition_schema', // Property definitions are root-level AMEF objects
- 'organization' => 'organization_schema'
- // NOTE: 'property' mapping removed - properties are never root-level AMEF objects, only nested within other elements
+ 'model' => 'model_schema',
+ 'element' => 'element_schema',
+ 'relationship' => 'relation_schema',
+ // Note: relation vs relationship.
+ 'view' => 'view_schema',
+ 'property_definition' => 'property_definition_schema',
+ // Property definitions are root-level AMEF objects.
+ 'organization' => 'organization_schema',
+ // NOTE: 'property' mapping removed - properties are never root-level AMEF objects, only nested within other elements.
];
$amefKey = $amefKeyMap[$objectType] ?? null;
- if ($amefKey && isset($decodedAmefConfig[$amefKey])) {
+ if ($amefKey !== false && isset($decodedAmefConfig[$amefKey]) === true) {
$schemaId = $decodedAmefConfig[$amefKey];
- if (!empty($schemaId)) {
+ if (empty($schemaId) === false) {
$result = (int) $schemaId;
- $this->logger->debug("SettingsService: Found schema ID in AMEF JSON config", [
- 'objectType' => $objectType,
- 'amefKey' => $amefKey,
- 'schemaId' => $result
- ]);
+ $this->logger->debug(
+ "SettingsService: Found schema ID in AMEF JSON config",
+ [
+ 'objectType' => $objectType,
+ 'amefKey' => $amefKey,
+ 'schemaId' => $result,
+ ]
+ );
}
}
- }
- }
+ }//end if
+ }//end if
$voorzieningenKeyMap = [
- 'module' => 'module_schema',
- 'compliancy' => 'compliancy_schema',
+ 'module' => 'module_schema',
+ 'compliancy' => 'compliancy_schema',
'moduleVersie' => 'moduleVersie_schema',
];
- // Only check voorzieningen config if object type exists in the key map
- if ($result === null && isset($voorzieningenKeyMap[$objectType])) {
+ // Only check voorzieningen config if object type exists in the key map.
+ if ($result === null && isset($voorzieningenKeyMap[$objectType]) === true) {
$voorzieningenKey = $voorzieningenKeyMap[$objectType];
- if (isset($voorzieningenConfig[$voorzieningenKey]) && $voorzieningenConfig[$voorzieningenKey] !== null) {
+ if (isset($voorzieningenConfig[$voorzieningenKey]) === true && $voorzieningenConfig[$voorzieningenKey] !== null) {
$result = (int) $voorzieningenConfig[$voorzieningenKey];
}
}
- // Check for AMEF register specific schemas (legacy individual keys)
+ // Check for AMEF register specific schemas (legacy individual keys).
if ($result === null && $objectType === 'organization') {
$schemaId = $this->config->getValueString($this->_appName, 'amef_organization_schema', '');
- if (!empty($schemaId)) {
+ if (empty($schemaId) === false) {
$result = (int) $schemaId;
} else {
- // Also check voorzieningen register for organization/organisatie
+ // Also check voorzieningen register for organization/organisatie.
$schemaId = $voorzieningenConfig['organisatie_schema'];
- if (!empty($schemaId)) {
+ if (empty($schemaId) === false) {
$result = (int) $schemaId;
}
}
@@ -743,48 +820,54 @@ public function getSchemaIdForObjectType(string $objectType): ?int
if ($objectType === 'organisatie' && $result === null) {
$schemaId = $voorzieningenConfig['organisatie_schema'];
- if (!empty($schemaId)) {
+ if (empty($schemaId) === false) {
$result = (int) $schemaId;
}
}
if ($objectType === 'contactpersoon' && $result === null) {
$schemaId = $voorzieningenConfig['contactpersoon_schema'];
- if (!empty($schemaId)) {
+ if (empty($schemaId) === false) {
$result = (int) $schemaId;
}
}
- // Fall back to generic configuration for backward compatibility
+ // Fall back to generic configuration for backward compatibility.
if ($result === null) {
$schemaId = $this->config->getValueString($this->_appName, "{$objectType}_schema", '');
- if (!empty($schemaId)) {
+ if (empty($schemaId) === false) {
$result = (int) $schemaId;
}
}
- // Cache the result (even if null) to avoid repeated lookups
+ // Cache the result (even if null) to avoid repeated lookups.
$this->schemaIdCache[$objectType] = $result;
$lookupTime = round((microtime(true) - $startTime) * 1000, 2);
if ($result !== null) {
- $this->logger->info("SettingsService: Found schema ID and cached result", [
- 'objectType' => $objectType,
- 'schemaId' => $result,
- 'lookupTime' => $lookupTime . 'ms',
- 'fromCache' => false
- ]);
+ $this->logger->info(
+ "SettingsService: Found schema ID and cached result",
+ [
+ 'objectType' => $objectType,
+ 'schemaId' => $result,
+ 'lookupTime' => $lookupTime.'ms',
+ 'fromCache' => false,
+ ]
+ );
} else {
- $this->logger->debug("SettingsService: No schema ID found, cached null result", [
- 'objectType' => $objectType,
- 'lookupTime' => $lookupTime . 'ms',
- 'fromCache' => false
- ]);
+ $this->logger->debug(
+ "SettingsService: No schema ID found, cached null result",
+ [
+ 'objectType' => $objectType,
+ 'lookupTime' => $lookupTime.'ms',
+ 'fromCache' => false,
+ ]
+ );
}
return $result;
- }
+ }//end getSchemaIdForObjectType()
/**
* Gets the configured register ID for a specific object type
@@ -795,52 +878,62 @@ public function getSchemaIdForObjectType(string $objectType): ?int
*/
public function getRegisterIdForObjectType(string $objectType): ?int
{
- // Check cache first for performance optimization
- if (array_key_exists($objectType, $this->registerIdCache)) {
+ // Check cache first for performance optimization.
+ if (array_key_exists($objectType, $this->registerIdCache) === true) {
$cachedValue = $this->registerIdCache[$objectType];
- $this->logger->debug("SettingsService: Register ID retrieved from cache", [
- 'objectType' => $objectType,
- 'cachedValue' => $cachedValue,
- 'fromCache' => true
- ]);
+ $this->logger->debug(
+ "SettingsService: Register ID retrieved from cache",
+ [
+ 'objectType' => $objectType,
+ 'cachedValue' => $cachedValue,
+ 'fromCache' => true,
+ ]
+ );
return $cachedValue;
}
$result = null;
- // Check AMEF register for organization
+ // Check AMEF register for organization.
if ($objectType === 'organization') {
$amefConfig = $this->getAmefConfig();
- if (isset($amefConfig['register']) && !empty($amefConfig['register'])) {
+ if (isset($amefConfig['register']) === true && empty($amefConfig['register']) === false) {
$result = (int) $amefConfig['register'];
}
}
- // Check Voorzieningen register for organisatie/organization and contactpersoon/contact
- if ($result === null && in_array($objectType, ['organisatie', 'organization', 'contactpersoon', 'contact'], true)) {
+ // Check Voorzieningen register for organisatie/organization and contactpersoon/contact.
+ if ($result === null && in_array($objectType, ['organisatie', 'organization', 'contactpersoon', 'contact'], true) === true) {
$voorzieningenConfig = $this->getVoorzieningenConfig();
- if (isset($voorzieningenConfig['register']) && !empty($voorzieningenConfig['register'])) {
+ if (isset($voorzieningenConfig['register']) === true && empty($voorzieningenConfig['register']) === false) {
$result = (int) $voorzieningenConfig['register'];
}
}
- // Fallback to legacy per-object-type register config
+ // Fallback to legacy per-object-type register config.
if ($result === null) {
$registerId = $this->config->getValueString($this->_appName, "{$objectType}_register", '');
- $result = $registerId ? (int) $registerId : null;
+ if (empty($registerId) === false) {
+ $result = (int) $registerId;
+ } else {
+ $result = null;
+ }
}
- // Cache the result (even if null) to avoid repeated lookups
+ // Cache the result (even if null) to avoid repeated lookups.
$this->registerIdCache[$objectType] = $result;
- $this->logger->debug("SettingsService: Register ID looked up and cached", [
- 'objectType' => $objectType,
- 'result' => $result,
- 'fromCache' => false
- ]);
+ $this->logger->debug(
+ "SettingsService: Register ID looked up and cached",
+ [
+ 'objectType' => $objectType,
+ 'result' => $result,
+ 'fromCache' => false,
+ ]
+ );
return $result;
- }
+ }//end getRegisterIdForObjectType()
/**
* Clear cached schema and register IDs
@@ -852,16 +945,19 @@ public function getRegisterIdForObjectType(string $objectType): ?int
*/
public function clearConfigurationCache(): void
{
- $this->logger->debug("SettingsService: Clearing configuration cache", [
- 'cached_schema_ids' => count($this->schemaIdCache),
- 'cached_register_ids' => count($this->registerIdCache)
- ]);
+ $this->logger->debug(
+ "SettingsService: Clearing configuration cache",
+ [
+ 'cached_schema_ids' => count($this->schemaIdCache),
+ 'cached_register_ids' => count($this->registerIdCache),
+ ]
+ );
- $this->schemaIdCache = [];
+ $this->schemaIdCache = [];
$this->registerIdCache = [];
$this->logger->info("SettingsService: Configuration cache cleared");
- }
+ }//end clearConfigurationCache()
/**
* Gets the configured register ID for the voorzieningen register
@@ -872,80 +968,110 @@ public function getVoorzieningenRegisterId(): ?int
{
$startTime = microtime(true);
- $this->logger->debug("SettingsService: Starting voorzieningen register ID lookup", [
- 'timestamp' => date('Y-m-d H:i:s')
- ]);
+ $this->logger->debug(
+ "SettingsService: Starting voorzieningen register ID lookup",
+ [
+ 'timestamp' => date('Y-m-d H:i:s'),
+ ]
+ );
- // Try voorzieningen-specific configuration first
- $this->logger->debug("SettingsService: Checking voorzieningen organisatie register", [
- 'configKey' => 'voorzieningen_organisatie_register'
- ]);
+ // Try voorzieningen-specific configuration first.
+ $this->logger->debug(
+ "SettingsService: Checking voorzieningen organisatie register",
+ [
+ 'configKey' => 'voorzieningen_organisatie_register',
+ ]
+ );
$registerId = $this->config->getValueString($this->_appName, 'voorzieningen_organisatie_register', '');
- $this->logger->debug("SettingsService: Voorzieningen organisatie register result", [
- 'configKey' => 'voorzieningen_organisatie_register',
- 'rawValue' => $registerId,
- 'isEmpty' => empty($registerId)
- ]);
+ $this->logger->debug(
+ "SettingsService: Voorzieningen organisatie register result",
+ [
+ 'configKey' => 'voorzieningen_organisatie_register',
+ 'rawValue' => $registerId,
+ 'isEmpty' => empty($registerId) === true,
+ ]
+ );
- if (!empty($registerId)) {
+ if (empty($registerId) === false) {
$result = (int) $registerId;
- $this->logger->info("SettingsService: Found voorzieningen organisatie register", [
- 'registerId' => $result,
- 'lookupTime' => round((microtime(true) - $startTime) * 1000, 2) . 'ms'
- ]);
+ $this->logger->info(
+ "SettingsService: Found voorzieningen organisatie register",
+ [
+ 'registerId' => $result,
+ 'lookupTime' => round((microtime(true) - $startTime) * 1000, 2).'ms',
+ ]
+ );
return $result;
}
- // Also try contactpersoon as fallback
- $this->logger->debug("SettingsService: Checking voorzieningen contactpersoon register", [
- 'configKey' => 'voorzieningen_contactpersoon_register'
- ]);
+ // Also try contactpersoon as fallback.
+ $this->logger->debug(
+ "SettingsService: Checking voorzieningen contactpersoon register",
+ [
+ 'configKey' => 'voorzieningen_contactpersoon_register',
+ ]
+ );
$registerId = $this->config->getValueString($this->_appName, 'voorzieningen_contactpersoon_register', '');
- $this->logger->debug("SettingsService: Voorzieningen contactpersoon register result", [
- 'configKey' => 'voorzieningen_contactpersoon_register',
- 'rawValue' => $registerId,
- 'isEmpty' => empty($registerId)
- ]);
+ $this->logger->debug(
+ "SettingsService: Voorzieningen contactpersoon register result",
+ [
+ 'configKey' => 'voorzieningen_contactpersoon_register',
+ 'rawValue' => $registerId,
+ 'isEmpty' => empty($registerId) === true,
+ ]
+ );
- if (!empty($registerId)) {
+ if (empty($registerId) === false) {
$result = (int) $registerId;
- $this->logger->info("SettingsService: Found voorzieningen contactpersoon register", [
- 'registerId' => $result,
- 'lookupTime' => round((microtime(true) - $startTime) * 1000, 2) . 'ms'
- ]);
+ $this->logger->info(
+ "SettingsService: Found voorzieningen contactpersoon register",
+ [
+ 'registerId' => $result,
+ 'lookupTime' => round((microtime(true) - $startTime) * 1000, 2).'ms',
+ ]
+ );
return $result;
}
- // Fall back to organization register for backward compatibility
- $this->logger->debug("SettingsService: Checking organization register for backward compatibility", [
- 'configKey' => 'organization_register'
- ]);
+ // Fall back to organization register for backward compatibility.
+ $this->logger->debug(
+ "SettingsService: Checking organization register for backward compatibility",
+ [
+ 'configKey' => 'organization_register',
+ ]
+ );
- $result = $this->getRegisterIdForObjectType('organization');
+ $result = $this->getRegisterIdForObjectType(objectType: 'organization');
if ($result !== null) {
- $this->logger->info("SettingsService: Found organization register for backward compatibility", [
- 'registerId' => $result,
- 'lookupTime' => round((microtime(true) - $startTime) * 1000, 2) . 'ms'
- ]);
+ $this->logger->info(
+ "SettingsService: Found organization register for backward compatibility",
+ [
+ 'registerId' => $result,
+ 'lookupTime' => round((microtime(true) - $startTime) * 1000, 2).'ms',
+ ]
+ );
return $result;
}
- $this->logger->warning("SettingsService: No register ID found for voorzieningen", [
- 'checkedConfigurations' => [
- 'voorzieningen_organisatie_register' => true,
- 'voorzieningen_contactpersoon_register' => true,
- 'organization_register' => true
- ],
- 'lookupTime' => round((microtime(true) - $startTime) * 1000, 2) . 'ms'
- ]);
+ $this->logger->warning(
+ "SettingsService: No register ID found for voorzieningen",
+ [
+ 'checkedConfigurations' => [
+ 'voorzieningen_organisatie_register' => true,
+ 'voorzieningen_contactpersoon_register' => true,
+ 'organization_register' => true,
+ ],
+ 'lookupTime' => round((microtime(true) - $startTime) * 1000, 2).'ms',
+ ]
+ );
return null;
- }
+ }//end getVoorzieningenRegisterId()
/**
* Checks if all required object types are configured
@@ -954,18 +1080,18 @@ public function getVoorzieningenRegisterId(): ?int
*/
public function isFullyConfigured(): bool
{
- // Use contactpersoon instead of contact to match the actual schema naming
+ // Use contactpersoon instead of contact to match the actual schema naming.
$objectTypes = ['organization', 'contactpersoon'];
foreach ($objectTypes as $type) {
- $schemaId = $this->getSchemaIdForObjectType($type);
- if (!$schemaId) {
+ $schemaId = $this->getSchemaIdForObjectType(objectType: $type);
+ if ($schemaId === null) {
return false;
}
}
return true;
- }
+ }//end isFullyConfigured()
/**
* Gets configuration status for each object type
@@ -974,30 +1100,30 @@ public function isFullyConfigured(): bool
*/
public function getConfigurationStatus(): array
{
- // Use the correct object type names that match the schema configuration
+ // Use the correct object type names that match the schema configuration.
$objectTypes = ['organization', 'organisatie', 'contact', 'contactpersoon'];
- $status = [];
+ $status = [];
- // Check organization (can be in AMEF as 'organization' or Voorzieningen as 'organisatie')
- $orgSchemaId = $this->getSchemaIdForObjectType('organization');
- $orgRegisterId = $this->getRegisterIdForObjectType('organization');
+ // Check organization (can be in AMEF as 'organization' or Voorzieningen as 'organisatie').
+ $orgSchemaId = $this->getSchemaIdForObjectType(objectType: 'organization');
+ $orgRegisterId = $this->getRegisterIdForObjectType(objectType: 'organization');
$status['organization'] = [
- 'configured' => !empty($orgSchemaId) && !empty($orgRegisterId),
- 'schemaId' => $orgSchemaId,
+ 'configured' => empty($orgSchemaId) === false && empty($orgRegisterId) === false,
+ 'schemaId' => $orgSchemaId,
'registerId' => $orgRegisterId,
];
- // Check contact (stored as 'contactpersoon' in Voorzieningen)
- $contactSchemaId = $this->getSchemaIdForObjectType('contactpersoon');
- $contactRegisterId = $this->getRegisterIdForObjectType('contactpersoon');
+ // Check contact (stored as 'contactpersoon' in Voorzieningen).
+ $contactSchemaId = $this->getSchemaIdForObjectType(objectType: 'contactpersoon');
+ $contactRegisterId = $this->getRegisterIdForObjectType(objectType: 'contactpersoon');
$status['contact'] = [
- 'configured' => !empty($contactSchemaId) && !empty($contactRegisterId),
- 'schemaId' => $contactSchemaId,
+ 'configured' => empty($contactSchemaId) === false && empty($contactRegisterId) === false,
+ 'schemaId' => $contactSchemaId,
'registerId' => $contactRegisterId,
];
return $status;
- }
+ }//end getConfigurationStatus()
/**
* Initializes the app with all required components
@@ -1006,147 +1132,176 @@ public function getConfigurationStatus(): array
*
* @return array The initialization results
*/
- public function initialize(?string $minOpenRegisterVersion = self::MIN_OPENREGISTER_VERSION): array
+ public function initialize(?string $minOpenRegisterVersion=self::MIN_OPENREGISTER_VERSION): array
{
$startTime = microtime(true);
- $results = [
- 'openRegister' => false,
- 'autoConfigured' => false,
- 'fullyConfigured' => false,
- 'settingsLoaded' => false,
+ $results = [
+ 'openRegister' => false,
+ 'autoConfigured' => false,
+ 'fullyConfigured' => false,
+ 'settingsLoaded' => false,
'configurationImported' => false,
'autoConfigAfterImport' => false,
- 'errors' => [],
- 'warnings' => [],
- 'timing' => []
+ 'errors' => [],
+ 'warnings' => [],
+ 'timing' => [],
];
- $this->logger->info('SettingsService: Starting initialization', [
- 'minOpenRegisterVersion' => $minOpenRegisterVersion
- ]);
+ $this->logger->info(
+ 'SettingsService: Starting initialization',
+ [
+ 'minOpenRegisterVersion' => $minOpenRegisterVersion,
+ ]
+ );
try {
- // Check if OpenRegister is installed and enabled
+ // Check if OpenRegister is installed and enabled.
$checkStart = microtime(true);
- if (!$this->isOpenRegisterInstalled($minOpenRegisterVersion)) {
+ if ($this->isOpenRegisterInstalled(minVersion: $minOpenRegisterVersion) === false) {
$error = 'OpenRegister is not installed or does not meet minimum version requirements';
$results['errors'][] = $error;
- $this->logger->error('SettingsService: ' . $error);
+ $this->logger->error('SettingsService: '.$error);
return $results;
}
- if (!$this->isOpenRegisterEnabled()) {
+ if ($this->isOpenRegisterEnabled() === false) {
$error = 'OpenRegister is not enabled';
$results['errors'][] = $error;
- $this->logger->error('SettingsService: ' . $error);
+ $this->logger->error('SettingsService: '.$error);
return $results;
}
$results['openRegister'] = true;
- $results['timing']['openregister_check'] = round((microtime(true) - $checkStart) * 1000, 2) . 'ms';
+ $results['timing']['openregister_check'] = round((microtime(true) - $checkStart) * 1000, 2).'ms';
$this->logger->info('SettingsService: OpenRegister is available');
- // Load settings from file if needed (do this first)
+ // Load settings from file if needed (do this first).
$loadStart = microtime(true);
try {
- if ($this->shouldLoadSettings()) {
+ if ($this->shouldLoadSettings() === true) {
$this->logger->info('SettingsService: Loading settings from file');
$loadResult = $this->loadSettings();
- $results['settingsLoaded'] = true;
- $results['configurationImported'] = !empty($loadResult['softwarecatalog_imported']);
- $this->logger->info('SettingsService: Settings loaded successfully', [
- 'imported' => $results['configurationImported']
- ]);
+ $results['settingsLoaded'] = true;
+ $results['configurationImported'] = empty($loadResult['softwarecatalog_imported']) === false;
+ $this->logger->info(
+ 'SettingsService: Settings loaded successfully',
+ [
+ 'imported' => $results['configurationImported'],
+ ]
+ );
} else {
- $results['settingsLoaded'] = true; // Already up to date
+ $results['settingsLoaded'] = true;
+ // Already up to date.
$this->logger->info('SettingsService: Settings already up to date');
}
} catch (\Exception $e) {
- $error = 'Settings loading failed: ' . $e->getMessage();
+ $error = 'Settings loading failed: '.$e->getMessage();
$results['errors'][] = $error;
- $this->logger->error('SettingsService: ' . $error, [
- 'exception' => $e
- ]);
- }
- $results['timing']['settings_load'] = round((microtime(true) - $loadStart) * 1000, 2) . 'ms';
+ $this->logger->error(
+ 'SettingsService: '.$error,
+ [
+ 'exception' => $e,
+ ]
+ );
+ }//end try
+
+ $results['timing']['settings_load'] = round((microtime(true) - $loadStart) * 1000, 2).'ms';
- // Try auto-configuration after import if not already configured
+ // Try auto-configuration after import if not already configured.
$autoConfigStart = microtime(true);
- if (!$this->isFullyConfigured()) {
+ if ($this->isFullyConfigured() === false) {
$this->logger->info('SettingsService: App not fully configured, attempting auto-configuration');
try {
- // First try the post-import auto-configuration (more specific)
+ // First try the post-import auto-configuration (more specific).
$configuration = $this->autoConfigureAfterImport();
- if (!empty($configuration)) {
- $this->updateSettings($configuration);
+ if (empty($configuration) === false) {
+ $this->updateSettings(data: $configuration);
$results['autoConfigAfterImport'] = true;
- $results['autoConfigured'] = true;
- $this->logger->info('SettingsService: Auto-configuration after import successful', [
- 'configuration' => array_keys($configuration)
- ]);
+ $results['autoConfigured'] = true;
+ $this->logger->info(
+ 'SettingsService: Auto-configuration after import successful',
+ [
+ 'configuration' => array_keys($configuration),
+ ]
+ );
} else {
- // Fallback to general auto-configuration
+ // Fallback to general auto-configuration.
$this->logger->info('SettingsService: Post-import auto-config yielded no results, trying general auto-config');
$configuration = $this->autoConfigure();
- if (!empty($configuration)) {
- $this->updateSettings($configuration);
+ if (empty($configuration) === false) {
+ $this->updateSettings(data: $configuration);
$results['autoConfigured'] = true;
- $this->logger->info('SettingsService: General auto-configuration successful', [
- 'configuration' => array_keys($configuration)
- ]);
+ $this->logger->info(
+ 'SettingsService: General auto-configuration successful',
+ [
+ 'configuration' => array_keys($configuration),
+ ]
+ );
}
- }
+ }//end if
} catch (\Exception $e) {
- $error = 'Auto-configuration failed: ' . $e->getMessage();
+ $error = 'Auto-configuration failed: '.$e->getMessage();
$results['errors'][] = $error;
- $this->logger->error('SettingsService: ' . $error, [
- 'exception' => $e
- ]);
- }
+ $this->logger->error(
+ 'SettingsService: '.$error,
+ [
+ 'exception' => $e,
+ ]
+ );
+ }//end try
} else {
$this->logger->info('SettingsService: App is already fully configured');
- }
- $results['timing']['auto_config'] = round((microtime(true) - $autoConfigStart) * 1000, 2) . 'ms';
+ }//end if
- // Final configuration status check
+ $results['timing']['auto_config'] = round((microtime(true) - $autoConfigStart) * 1000, 2).'ms';
+
+ // Final configuration status check.
$results['fullyConfigured'] = $this->isFullyConfigured();
- if (!$results['fullyConfigured']) {
+ if ($results['fullyConfigured'] === false) {
$warning = 'App is not fully configured after initialization. Manual configuration may be required.';
$results['warnings'][] = $warning;
- $this->logger->warning('SettingsService: ' . $warning, [
- 'configStatus' => $this->getConfigurationStatus()
- ]);
+ $this->logger->warning(
+ 'SettingsService: '.$warning,
+ [
+ 'configStatus' => $this->getConfigurationStatus(),
+ ]
+ );
}
- $results['timing']['total'] = round((microtime(true) - $startTime) * 1000, 2) . 'ms';
-
- $this->logger->info('SettingsService: Initialization completed', [
- 'results' => [
- 'openRegister' => $results['openRegister'],
- 'autoConfigured' => $results['autoConfigured'],
- 'fullyConfigured' => $results['fullyConfigured'],
- 'settingsLoaded' => $results['settingsLoaded'],
- 'errors' => count($results['errors']),
- 'warnings' => count($results['warnings'])
- ],
- 'timing' => $results['timing']
- ]);
+ $results['timing']['total'] = round((microtime(true) - $startTime) * 1000, 2).'ms';
+ $this->logger->info(
+ 'SettingsService: Initialization completed',
+ [
+ 'results' => [
+ 'openRegister' => $results['openRegister'],
+ 'autoConfigured' => $results['autoConfigured'],
+ 'fullyConfigured' => $results['fullyConfigured'],
+ 'settingsLoaded' => $results['settingsLoaded'],
+ 'errors' => count($results['errors']),
+ 'warnings' => count($results['warnings']),
+ ],
+ 'timing' => $results['timing'],
+ ]
+ );
} catch (\Exception $e) {
- $error = 'Initialization failed: ' . $e->getMessage();
+ $error = 'Initialization failed: '.$e->getMessage();
$results['errors'][] = $error;
- $this->logger->error('SettingsService: ' . $error, [
- 'exception' => $e,
- 'trace' => $e->getTraceAsString()
- ]);
- }
+ $this->logger->error(
+ 'SettingsService: '.$error,
+ [
+ 'exception' => $e,
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+ }//end try
return $results;
- }
+ }//end initialize()
/**
* Load settings from register configuration files
@@ -1157,21 +1312,21 @@ public function initialize(?string $minOpenRegisterVersion = self::MIN_OPENREGIS
*
* @throws \RuntimeException If settings loading fails
*/
- public function loadSettings(bool $force = false): array
+ public function loadSettings(bool $force=false): array
{
$results = [];
try {
- // Load settings from merged softwarecatalogus_register.json (magic mapper enabled for performance)
- $softwareCatalogPath = __DIR__ . '/../Settings/softwarecatalogus_register.json';
- if (file_exists($softwareCatalogPath)) {
- $softwareCatalogContent = file_get_contents($softwareCatalogPath);
+ // Load settings from merged softwarecatalogus_register.json (magic mapper enabled for performance).
+ $softwareCatalogPath = __DIR__.'/../Settings/softwarecatalogus_register.json';
+ if (file_exists($softwareCatalogPath) === true) {
+ $softwareCatalogContent = file_get_contents($softwareCatalogPath);
$softwareCatalogSettings = json_decode($softwareCatalogContent, true);
if (json_last_error() === JSON_ERROR_NONE) {
$results['softwarecatalog'] = $softwareCatalogSettings;
- // Import via configuration service if available with version checking
+ // Import via configuration service if available with version checking.
try {
$configurationService = $this->getConfigurationService();
@@ -1179,15 +1334,18 @@ public function loadSettings(bool $force = false): array
// This ensures changes to the JSON file trigger re-import even if app version is unchanged.
$configVersion = $softwareCatalogSettings['info']['version'] ?? '0.0.0';
- // Log the import attempt for debugging
- $this->logger->info('SettingsService: Attempting to import softwarecatalogus_register.json', [
- 'force' => $force,
- 'app_id' => \OCA\SoftwareCatalog\AppInfo\Application::APP_ID,
- 'config_version' => $configVersion,
- 'data_size' => strlen(json_encode($softwareCatalogSettings))
- ]);
-
- // Use importFromApp which handles Configuration entity creation automatically
+ // Log the import attempt for debugging.
+ $this->logger->info(
+ 'SettingsService: Attempting to import softwarecatalogus_register.json',
+ [
+ 'force' => $force,
+ 'app_id' => \OCA\SoftwareCatalog\AppInfo\Application::APP_ID,
+ 'config_version' => $configVersion,
+ 'data_size' => strlen(json_encode($softwareCatalogSettings)),
+ ]
+ );
+
+ // Use importFromApp which handles Configuration entity creation automatically.
$importResult = $configurationService->importFromApp(
appId: \OCA\SoftwareCatalog\AppInfo\Application::APP_ID,
data: $softwareCatalogSettings,
@@ -1195,39 +1353,44 @@ public function loadSettings(bool $force = false): array
force: $force
);
- $this->logger->info('SettingsService: Import completed successfully', [
- 'import_result' => $importResult
- ]);
+ $this->logger->info(
+ 'SettingsService: Import completed successfully',
+ [
+ 'import_result' => $importResult,
+ ]
+ );
$results['softwarecatalog_imported'] = true;
- $results['import_result'] = $importResult;
+ $results['import_result'] = $importResult;
} catch (\Exception $e) {
$results['softwarecatalog_import_error'] = $e->getMessage();
- $this->logger->error('Failed to import softwarecatalog settings: ' . $e->getMessage(), [
- 'exception' => $e,
- 'trace' => $e->getTraceAsString(),
- 'force_flag' => $force,
- 'app_id' => \OCA\SoftwareCatalog\AppInfo\Application::APP_ID
- ]);
-
- // In force mode, we want to surface import errors more prominently
- if ($force) {
- throw new \RuntimeException('Force import failed: ' . $e->getMessage(), 0, $e);
+ $this->logger->error(
+ 'Failed to import softwarecatalog settings: '.$e->getMessage(),
+ [
+ 'exception' => $e,
+ 'trace' => $e->getTraceAsString(),
+ 'force_flag' => $force,
+ 'app_id' => \OCA\SoftwareCatalog\AppInfo\Application::APP_ID,
+ ]
+ );
+
+ // In force mode, we want to surface import errors more prominently.
+ if (empty($force) === false) {
+ throw new \RuntimeException('Force import failed: '.$e->getMessage(), 0, $e);
}
- }
- }
- }
+ }//end try
+ }//end if
+ }//end if
- if (empty($results)) {
+ if (empty($results) === true) {
throw new \Exception('No register configuration files found');
}
return $results;
-
} catch (\Exception $e) {
- throw new \RuntimeException('Failed to load settings: ' . $e->getMessage());
- }
- }
+ throw new \RuntimeException('Failed to load settings: '.$e->getMessage());
+ }//end try
+ }//end loadSettings()
/**
* Gets the list of generic user groups from configuration
@@ -1238,17 +1401,21 @@ public function getGenericUserGroups(): array
{
$groupsJson = $this->config->getValueString($this->_appName, 'generic_user_groups', '');
- if (empty($groupsJson)) {
- // Return only truly generic groups as default (not role-specific)
- // Role-specific groups are now assigned based on organization type
+ if (empty($groupsJson) === true) {
+ // Return only truly generic groups as default (not role-specific).
+ // Role-specific groups are now assigned based on organization type.
return [
'software-catalog-users'
];
}
$groups = json_decode($groupsJson, true);
- return is_array($groups) ? $groups : [];
- }
+ if (is_array($groups) === true) {
+ return $groups;
+ } else {
+ return [];
+ }
+ }//end getGenericUserGroups()
/**
* Sets the list of generic user groups in configuration
@@ -1265,10 +1432,10 @@ public function setGenericUserGroups(array $groups): void
$this->logger->info(
'Updated generic user groups configuration',
[
- 'groups' => $groups
+ 'groups' => $groups,
]
);
- }
+ }//end setGenericUserGroups()
/**
* Gets the list of organization admin groups from configuration
@@ -1277,11 +1444,11 @@ public function setGenericUserGroups(array $groups): void
*/
public function getOrganizationAdminGroups(): array
{
- // DISABLED: No automatic group assignment for organization admins
- // Users should be assigned groups explicitly via the admin UI
- // Previously this returned ['organisaties-beheerder', 'organisatie-beheerder'] by default
+ // DISABLED: No automatic group assignment for organization admins.
+ // Users should be assigned groups explicitly via the admin UI.
+ // Previously this returned ['organisaties-beheerder', 'organisatie-beheerder'] by default.
return [];
- }
+ }//end getOrganizationAdminGroups()
/**
* Sets the list of organization admin groups in configuration
@@ -1298,10 +1465,10 @@ public function setOrganizationAdminGroups(array $groups): void
$this->logger->info(
'Updated organization admin groups configuration',
[
- 'groups' => $groups
+ 'groups' => $groups,
]
);
- }
+ }//end setOrganizationAdminGroups()
/**
* Gets the list of super user groups from configuration
@@ -1312,17 +1479,21 @@ public function getSuperUserGroups(): array
{
$groupsJson = $this->config->getValueString($this->_appName, 'super_user_groups', '');
- if (empty($groupsJson)) {
- // Return default groups if no configuration exists
+ if (empty($groupsJson) === true) {
+ // Return default groups if no configuration exists.
return [
'admin',
- 'software-catalog-admins'
+ 'software-catalog-admins',
];
}
$groups = json_decode($groupsJson, true);
- return is_array($groups) ? $groups : [];
- }
+ if (is_array($groups) === true) {
+ return $groups;
+ } else {
+ return [];
+ }
+ }//end getSuperUserGroups()
/**
* Sets the list of super user groups in configuration
@@ -1339,10 +1510,10 @@ public function setSuperUserGroups(array $groups): void
$this->logger->info(
'Updated super user groups configuration',
[
- 'groups' => $groups
+ 'groups' => $groups,
]
);
- }
+ }//end setSuperUserGroups()
/**
* Validates a list of group names
@@ -1354,22 +1525,22 @@ public function setSuperUserGroups(array $groups): void
public function validateGroups(array $groups): array
{
$results = [
- 'valid' => [],
+ 'valid' => [],
'invalid' => [],
- 'errors' => []
+ 'errors' => [],
];
foreach ($groups as $groupName) {
- if (empty($groupName) || !is_string($groupName)) {
+ if (empty($groupName) === true || is_string($groupName) === false) {
$results['invalid'][] = $groupName;
- $results['errors'][] = 'Group name cannot be empty';
+ $results['errors'][] = 'Group name cannot be empty';
continue;
}
- // Check for invalid characters
- if (preg_match('/[^a-zA-Z0-9._-]/', $groupName)) {
+ // Check for invalid characters.
+ if (preg_match('/[^a-zA-Z0-9._-]/', $groupName) === 1) {
$results['invalid'][] = $groupName;
- $results['errors'][] = "Group name '{$groupName}' contains invalid characters";
+ $results['errors'][] = "Group name '{$groupName}' contains invalid characters";
continue;
}
@@ -1377,7 +1548,7 @@ public function validateGroups(array $groups): array
}
return $results;
- }
+ }//end validateGroups()
/**
* Creates and configures required user groups for the software catalog
@@ -1392,48 +1563,48 @@ public function createAndConfigureUserGroups(): array
$this->logger->info('SettingsService: Starting user group creation and configuration');
$result = [
- 'success' => true,
- 'message' => 'User groups configured successfully',
- 'created' => [],
+ 'success' => true,
+ 'message' => 'User groups configured successfully',
+ 'created' => [],
'existing' => [],
- 'total' => 0
+ 'total' => 0,
];
- // Get the group manager
+ // Get the group manager.
$groupManager = \OC::$server->getGroupManager();
- // Define the required groups (matching role-based system)
+ // Define the required groups (matching role-based system).
$requiredGroups = [
- // Role-based user groups (exact match with ContactPersoon roles)
- 'aanbod-beheerder' => 'Manages software offerings and catalog content',
- 'gebruik-beheerder' => 'Manages software usage and procurement',
- 'gebruik-raadpleger' => 'Views software usage and procurement data',
- 'functioneel-beheerder' => 'Manages functional aspects of the system',
- 'vng-raadpleger' => 'Views VNG-related information',
- 'organisatie-beheerder' => 'Manages organization data and settings',
-
- // Plural form for organization contacts
- 'organisaties-beheerder' => 'Organization administrators (plural)',
-
- // Special groups (available for manual assignment)
- 'ambtenaar' => 'Civil servants - available for manual assignment (no automatic assignment)',
- 'software-catalog-users' => 'General software catalog users',
-
- // Super user groups
- 'software-catalog-admins' => 'Software catalog system administrators'
+ // Role-based user groups (exact match with ContactPersoon roles).
+ 'aanbod-beheerder' => 'Manages software offerings and catalog content',
+ 'gebruik-beheerder' => 'Manages software usage and procurement',
+ 'gebruik-raadpleger' => 'Views software usage and procurement data',
+ 'functioneel-beheerder' => 'Manages functional aspects of the system',
+ 'vng-raadpleger' => 'Views VNG-related information',
+ 'organisatie-beheerder' => 'Manages organization data and settings',
+
+ // Plural form for organization contacts.
+ 'organisaties-beheerder' => 'Organization administrators (plural)',
+
+ // Special groups (available for manual assignment).
+ 'ambtenaar' => 'Civil servants - available for manual assignment (no automatic assignment)',
+ 'software-catalog-users' => 'General software catalog users',
+
+ // Super user groups.
+ 'software-catalog-admins' => 'Software catalog system administrators',
];
foreach ($requiredGroups as $groupId => $description) {
$this->logger->debug("SettingsService: Processing group: {$groupId}");
- // Check if group already exists
- if ($groupManager->groupExists($groupId)) {
+ // Check if group already exists.
+ if ($groupManager->groupExists($groupId) === true) {
$result['existing'][] = $groupId;
$this->logger->debug("SettingsService: Group {$groupId} already exists");
continue;
}
- // Create the group
+ // Create the group.
$group = $groupManager->createGroup($groupId);
if ($group !== false) {
$result['created'][] = $groupId;
@@ -1446,21 +1617,26 @@ public function createAndConfigureUserGroups(): array
$result['total'] = count($requiredGroups);
- // Update the configuration with only truly generic groups (not role-specific)
- // Role-specific groups are now assigned based on organization type
- $this->setGenericUserGroups([
- 'software-catalog-users'
- ]);
+ // Update the configuration with only truly generic groups (not role-specific).
+ // Role-specific groups are now assigned based on organization type.
+ $this->setGenericUserGroups(
+ groups: [
+ 'software-catalog-users',
+ ]
+ );
- // No automatic organization admin groups - can be configured via settings
- $this->setOrganizationAdminGroups([]);
+ // No automatic organization admin groups - can be configured via settings.
+ $this->setOrganizationAdminGroups(groups: []);
- $this->setSuperUserGroups([
- 'admin', // Keep existing admin group
- 'software-catalog-admins'
- ]);
+ $this->setSuperUserGroups(
+ groups: [
+ // Keep existing admin group.
+ 'admin',
+ 'software-catalog-admins',
+ ]
+ );
- $createdCount = count($result['created']);
+ $createdCount = count($result['created']);
$existingCount = count($result['existing']);
if ($createdCount > 0) {
@@ -1469,29 +1645,34 @@ public function createAndConfigureUserGroups(): array
$result['message'] = "All {$existingCount} required groups already exist";
}
- $this->logger->info('SettingsService: User group creation and configuration completed', [
- 'created_groups' => $result['created'],
- 'existing_groups' => $result['existing'],
- 'total_required' => $result['total'],
- 'success' => $result['success']
- ]);
+ $this->logger->info(
+ 'SettingsService: User group creation and configuration completed',
+ [
+ 'created_groups' => $result['created'],
+ 'existing_groups' => $result['existing'],
+ 'total_required' => $result['total'],
+ 'success' => $result['success'],
+ ]
+ );
return $result;
-
} catch (\Exception $e) {
- $this->logger->error('SettingsService: Failed to create and configure user groups', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'SettingsService: Failed to create and configure user groups',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return [
- 'success' => false,
- 'message' => 'Failed to create user groups: ' . $e->getMessage(),
- 'created' => [],
+ 'success' => false,
+ 'message' => 'Failed to create user groups: '.$e->getMessage(),
+ 'created' => [],
'existing' => [],
- 'total' => 0,
- 'error' => $e->getMessage()
+ 'total' => 0,
+ 'error' => $e->getMessage(),
];
- }
- }
+ }//end try
+ }//end createAndConfigureUserGroups()
/**
* Creates required user groups for the software catalog
@@ -1510,44 +1691,44 @@ private function createRequiredUserGroups(): void
try {
$this->logger->info('Starting creation of required user groups');
- // Get the group manager
+ // Get the group manager.
$groupManager = \OC::$server->getGroupManager();
- // Define the required groups (matching role-based system)
+ // Define the required groups (matching role-based system).
$requiredGroups = [
- // Role-based user groups (exact match with ContactPersoon roles)
- 'aanbod-beheerder' => 'Manages software offerings and catalog content',
- 'gebruik-beheerder' => 'Manages software usage and procurement',
- 'gebruik-raadpleger' => 'Views software usage and procurement data',
- 'functioneel-beheerder' => 'Manages functional aspects of the system',
- 'vng-raadpleger' => 'Views VNG-related information',
- 'organisatie-beheerder' => 'Manages organization data and settings',
-
- // Plural form for organization contacts
- 'organisaties-beheerder' => 'Organization administrators (plural)',
-
- // Special groups (available for manual assignment)
- 'ambtenaar' => 'Civil servants - available for manual assignment (no automatic assignment)',
- 'software-catalog-users' => 'General software catalog users',
-
- // Super user groups
- 'software-catalog-admins' => 'Software catalog system administrators'
+ // Role-based user groups (exact match with ContactPersoon roles).
+ 'aanbod-beheerder' => 'Manages software offerings and catalog content',
+ 'gebruik-beheerder' => 'Manages software usage and procurement',
+ 'gebruik-raadpleger' => 'Views software usage and procurement data',
+ 'functioneel-beheerder' => 'Manages functional aspects of the system',
+ 'vng-raadpleger' => 'Views VNG-related information',
+ 'organisatie-beheerder' => 'Manages organization data and settings',
+
+ // Plural form for organization contacts.
+ 'organisaties-beheerder' => 'Organization administrators (plural)',
+
+ // Special groups (available for manual assignment).
+ 'ambtenaar' => 'Civil servants - available for manual assignment (no automatic assignment)',
+ 'software-catalog-users' => 'General software catalog users',
+
+ // Super user groups.
+ 'software-catalog-admins' => 'Software catalog system administrators',
];
- $createdGroups = [];
+ $createdGroups = [];
$existingGroups = [];
foreach ($requiredGroups as $groupId => $description) {
$this->logger->debug("Processing group: {$groupId}");
- // Check if group already exists
- if ($groupManager->groupExists($groupId)) {
+ // Check if group already exists.
+ if ($groupManager->groupExists($groupId) === true) {
$existingGroups[] = $groupId;
$this->logger->debug("Group {$groupId} already exists, skipping");
continue;
}
- // Create the group
+ // Create the group.
$group = $groupManager->createGroup($groupId);
if ($group !== false) {
$createdGroups[] = $groupId;
@@ -1557,33 +1738,43 @@ private function createRequiredUserGroups(): void
}
}
- // Update the configuration with only truly generic groups (not role-specific)
- // Role-specific groups are now assigned based on organization type, not as generic groups
- $this->setGenericUserGroups([
- 'software-catalog-users'
- ]);
-
- // No automatic organization admin groups - can be configured via settings
- $this->setOrganizationAdminGroups([]);
+ // Update the configuration with only truly generic groups (not role-specific).
+ // Role-specific groups are now assigned based on organization type, not as generic groups.
+ $this->setGenericUserGroups(
+ groups: [
+ 'software-catalog-users',
+ ]
+ );
- $this->setSuperUserGroups([
- 'admin', // Keep existing admin group
- 'software-catalog-admins'
- ]);
+ // No automatic organization admin groups - can be configured via settings.
+ $this->setOrganizationAdminGroups(groups: []);
- $this->logger->info('User group creation completed', [
- 'created_groups' => $createdGroups,
- 'existing_groups' => $existingGroups,
- 'total_required' => count($requiredGroups)
- ]);
+ $this->setSuperUserGroups(
+ groups: [
+ // Keep existing admin group.
+ 'admin',
+ 'software-catalog-admins',
+ ]
+ );
+ $this->logger->info(
+ 'User group creation completed',
+ [
+ 'created_groups' => $createdGroups,
+ 'existing_groups' => $existingGroups,
+ 'total_required' => count($requiredGroups),
+ ]
+ );
} catch (\Exception $e) {
- $this->logger->error('Failed to create required user groups: ' . $e->getMessage(), [
- 'exception' => $e
- ]);
- throw new \RuntimeException('Failed to create required user groups: ' . $e->getMessage());
- }
- }
+ $this->logger->error(
+ 'Failed to create required user groups: '.$e->getMessage(),
+ [
+ 'exception' => $e,
+ ]
+ );
+ throw new \RuntimeException('Failed to create required user groups: '.$e->getMessage());
+ }//end try
+ }//end createRequiredUserGroups()
/**
* Gets all available groups with their information
@@ -1594,27 +1785,27 @@ public function getAllGroups(): array
{
$groups = [];
- // Get group manager if possible
- if ($this->appManager->isInstalled('user_management')) {
+ // Get group manager if possible.
+ if ($this->appManager->isInstalled('user_management') === true) {
try {
$groupManager = \OC::$server->getGroupManager();
- $allGroups = $groupManager->search('');
+ $allGroups = $groupManager->search('');
foreach ($allGroups as $group) {
$groups[] = [
- 'id' => $group->getGID(),
+ 'id' => $group->getGID(),
'displayName' => $group->getDisplayName(),
- 'memberCount' => count($group->getUsers()),
- 'isGeneric' => in_array($group->getGID(), $this->getGenericUserGroups())
+ 'memberCount' => count($group->getUsers() === true),
+ 'isGeneric' => in_array($group->getGID(), $this->getGenericUserGroups()) === true,
];
}
} catch (\Exception $e) {
- $this->logger->error('Failed to get all groups: ' . $e->getMessage());
+ $this->logger->error('Failed to get all groups: '.$e->getMessage());
}
}
return $groups;
- }
+ }//end getAllGroups()
/**
* Gets email configuration settings
@@ -1626,67 +1817,70 @@ public function getEmailSettings(): array
$this->logger->debug('SoftwareCatalog: Loading email settings from configuration');
$settings = [
- 'enabled' => $this->config->getValueString($this->_appName, 'email_enabled', 'false') === 'true',
- 'senderEmail' => $this->config->getValueString($this->_appName, 'sender_email', 'noreply@softwarecatalogus.nl'),
- 'senderName' => $this->config->getValueString($this->_appName, 'sender_name', 'Software Catalogus'),
- 'testReceiverOverride' => $this->config->getValueString($this->_appName, 'test_receiver_override', ''),
+ 'enabled' => $this->config->getValueString($this->_appName, 'email_enabled', 'false') === 'true',
+ 'senderEmail' => $this->config->getValueString($this->_appName, 'sender_email', 'noreply@softwarecatalogus.nl'),
+ 'senderName' => $this->config->getValueString($this->_appName, 'sender_name', 'Software Catalogus'),
+ 'testReceiverOverride' => $this->config->getValueString($this->_appName, 'test_receiver_override', ''),
'organizationRegistrationEnabled' => $this->config->getValueString($this->_appName, 'email_org_registration_enabled', 'true') === 'true',
- 'organizationActivationEnabled' => $this->config->getValueString($this->_appName, 'email_org_activation_enabled', 'true') === 'true',
- 'userCreationEnabled' => $this->config->getValueString($this->_appName, 'email_user_creation_enabled', 'true') === 'true',
- 'userPasswordEnabled' => $this->config->getValueString($this->_appName, 'email_user_password_enabled', 'true') === 'true',
- 'userOrganisationEnabled' => $this->config->getValueString($this->_appName, 'email_user_organisation_enabled', 'true') === 'true',
-
- // Symfony Mailer transport configuration
- 'transportType' => $this->config->getValueString($this->_appName, 'email_transport_type', 'smtp'),
-
- // SMTP configuration
- 'smtpHost' => $this->config->getValueString($this->_appName, 'email_smtp_host', 'localhost'),
- 'smtpPort' => (int) $this->config->getValueString($this->_appName, 'email_smtp_port', '587'),
- 'smtpEncryption' => $this->config->getValueString($this->_appName, 'email_smtp_encryption', 'tls'),
- 'smtpUsername' => $this->config->getValueString($this->_appName, 'email_smtp_username', ''),
- 'smtpPassword' => $this->config->getValueString($this->_appName, 'email_smtp_password', ''),
-
- // SendGrid configuration
- 'sendgridApiKey' => $this->config->getValueString($this->_appName, 'email_sendgrid_api_key', ''),
-
- // Mailgun configuration
- 'mailgunApiKey' => $this->config->getValueString($this->_appName, 'email_mailgun_api_key', ''),
- 'mailgunDomain' => $this->config->getValueString($this->_appName, 'email_mailgun_domain', ''),
-
- // Postmark configuration
- 'postmarkApiKey' => $this->config->getValueString($this->_appName, 'email_postmark_api_key', ''),
-
- // Amazon SES configuration
- 'sesAccessKey' => $this->config->getValueString($this->_appName, 'email_ses_access_key', ''),
- 'sesSecretKey' => $this->config->getValueString($this->_appName, 'email_ses_secret_key', ''),
- 'sesRegion' => $this->config->getValueString($this->_appName, 'email_ses_region', 'us-east-1'),
-
- // Mailjet configuration
- 'mailjetApiKey' => $this->config->getValueString($this->_appName, 'email_mailjet_api_key', ''),
- 'mailjetSecretKey' => $this->config->getValueString($this->_appName, 'email_mailjet_secret_key', ''),
-
- // Templates
- 'templates' => [
- 'organization_registration' => $this->getEmailTemplate('organization_registration'),
- 'organization_activation' => $this->getEmailTemplate('organization_activation'),
- 'user_creation' => $this->getEmailTemplate('user_creation'),
- 'user_password' => $this->getEmailTemplate('user_password'),
- ]
+ 'organizationActivationEnabled' => $this->config->getValueString($this->_appName, 'email_org_activation_enabled', 'true') === 'true',
+ 'userCreationEnabled' => $this->config->getValueString($this->_appName, 'email_user_creation_enabled', 'true') === 'true',
+ 'userPasswordEnabled' => $this->config->getValueString($this->_appName, 'email_user_password_enabled', 'true') === 'true',
+ 'userOrganisationEnabled' => $this->config->getValueString($this->_appName, 'email_user_organisation_enabled', 'true') === 'true',
+
+ // Symfony Mailer transport configuration.
+ 'transportType' => $this->config->getValueString($this->_appName, 'email_transport_type', 'smtp'),
+
+ // SMTP configuration.
+ 'smtpHost' => $this->config->getValueString($this->_appName, 'email_smtp_host', 'localhost'),
+ 'smtpPort' => (int) $this->config->getValueString($this->_appName, 'email_smtp_port', '587'),
+ 'smtpEncryption' => $this->config->getValueString($this->_appName, 'email_smtp_encryption', 'tls'),
+ 'smtpUsername' => $this->config->getValueString($this->_appName, 'email_smtp_username', ''),
+ 'smtpPassword' => $this->config->getValueString($this->_appName, 'email_smtp_password', ''),
+
+ // SendGrid configuration.
+ 'sendgridApiKey' => $this->config->getValueString($this->_appName, 'email_sendgrid_api_key', ''),
+
+ // Mailgun configuration.
+ 'mailgunApiKey' => $this->config->getValueString($this->_appName, 'email_mailgun_api_key', ''),
+ 'mailgunDomain' => $this->config->getValueString($this->_appName, 'email_mailgun_domain', ''),
+
+ // Postmark configuration.
+ 'postmarkApiKey' => $this->config->getValueString($this->_appName, 'email_postmark_api_key', ''),
+
+ // Amazon SES configuration.
+ 'sesAccessKey' => $this->config->getValueString($this->_appName, 'email_ses_access_key', ''),
+ 'sesSecretKey' => $this->config->getValueString($this->_appName, 'email_ses_secret_key', ''),
+ 'sesRegion' => $this->config->getValueString($this->_appName, 'email_ses_region', 'us-east-1'),
+
+ // Mailjet configuration.
+ 'mailjetApiKey' => $this->config->getValueString($this->_appName, 'email_mailjet_api_key', ''),
+ 'mailjetSecretKey' => $this->config->getValueString($this->_appName, 'email_mailjet_secret_key', ''),
+
+ // Templates.
+ 'templates' => [
+ 'organization_registration' => $this->getEmailTemplate(templateName: 'organization_registration'),
+ 'organization_activation' => $this->getEmailTemplate(templateName: 'organization_activation'),
+ 'user_creation' => $this->getEmailTemplate(templateName: 'user_creation'),
+ 'user_password' => $this->getEmailTemplate(templateName: 'user_password'),
+ ],
];
- $this->logger->info('SoftwareCatalog: Email settings loaded from configuration', [
- 'enabled' => $settings['enabled'],
- 'transport_type' => $settings['transportType'],
- 'sender_email' => $settings['senderEmail'],
- 'has_mailjet_api_key' => !empty($settings['mailjetApiKey']),
- 'mailjet_api_key_length' => strlen($settings['mailjetApiKey']),
- 'has_mailjet_secret_key' => !empty($settings['mailjetSecretKey']),
- 'mailjet_secret_key_length' => strlen($settings['mailjetSecretKey']),
- 'test_receiver_override' => $settings['testReceiverOverride']
- ]);
+ $this->logger->info(
+ 'SoftwareCatalog: Email settings loaded from configuration',
+ [
+ 'enabled' => $settings['enabled'],
+ 'transport_type' => $settings['transportType'],
+ 'sender_email' => $settings['senderEmail'],
+ 'has_mailjet_api_key' => empty($settings['mailjetApiKey']) === false,
+ 'mailjet_api_key_length' => strlen($settings['mailjetApiKey']),
+ 'has_mailjet_secret_key' => empty($settings['mailjetSecretKey']) === false,
+ 'mailjet_secret_key_length' => strlen($settings['mailjetSecretKey']),
+ 'test_receiver_override' => $settings['testReceiverOverride'],
+ ]
+ );
return $settings;
- }
+ }//end getEmailSettings()
/**
* Updates email configuration settings
@@ -1698,54 +1892,58 @@ public function getEmailSettings(): array
public function updateEmailSettings(array $emailSettings): array
{
$allowedSettings = [
- 'enabled' => 'email_enabled',
- 'senderEmail' => 'sender_email',
- 'senderName' => 'sender_name',
- 'testReceiverOverride' => 'test_receiver_override',
+ 'enabled' => 'email_enabled',
+ 'senderEmail' => 'sender_email',
+ 'senderName' => 'sender_name',
+ 'testReceiverOverride' => 'test_receiver_override',
'organizationRegistrationEnabled' => 'email_org_registration_enabled',
- 'organizationActivationEnabled' => 'email_org_activation_enabled',
- 'userCreationEnabled' => 'email_user_creation_enabled',
- 'userPasswordEnabled' => 'email_user_password_enabled',
- 'userOrganisationEnabled' => 'email_user_organisation_enabled',
-
- // Symfony Mailer transport configuration
- 'transportType' => 'email_transport_type',
-
- // SMTP configuration
- 'smtpHost' => 'email_smtp_host',
- 'smtpPort' => 'email_smtp_port',
- 'smtpEncryption' => 'email_smtp_encryption',
- 'smtpUsername' => 'email_smtp_username',
- 'smtpPassword' => 'email_smtp_password',
-
- // SendGrid configuration
- 'sendgridApiKey' => 'email_sendgrid_api_key',
-
- // Mailgun configuration
- 'mailgunApiKey' => 'email_mailgun_api_key',
- 'mailgunDomain' => 'email_mailgun_domain',
-
- // Postmark configuration
- 'postmarkApiKey' => 'email_postmark_api_key',
-
- // Amazon SES configuration
- 'sesAccessKey' => 'email_ses_access_key',
- 'sesSecretKey' => 'email_ses_secret_key',
- 'sesRegion' => 'email_ses_region',
-
- // Mailjet configuration
- 'mailjetApiKey' => 'email_mailjet_api_key',
- 'mailjetSecretKey' => 'email_mailjet_secret_key',
+ 'organizationActivationEnabled' => 'email_org_activation_enabled',
+ 'userCreationEnabled' => 'email_user_creation_enabled',
+ 'userPasswordEnabled' => 'email_user_password_enabled',
+ 'userOrganisationEnabled' => 'email_user_organisation_enabled',
+
+ // Symfony Mailer transport configuration.
+ 'transportType' => 'email_transport_type',
+
+ // SMTP configuration.
+ 'smtpHost' => 'email_smtp_host',
+ 'smtpPort' => 'email_smtp_port',
+ 'smtpEncryption' => 'email_smtp_encryption',
+ 'smtpUsername' => 'email_smtp_username',
+ 'smtpPassword' => 'email_smtp_password',
+
+ // SendGrid configuration.
+ 'sendgridApiKey' => 'email_sendgrid_api_key',
+
+ // Mailgun configuration.
+ 'mailgunApiKey' => 'email_mailgun_api_key',
+ 'mailgunDomain' => 'email_mailgun_domain',
+
+ // Postmark configuration.
+ 'postmarkApiKey' => 'email_postmark_api_key',
+
+ // Amazon SES configuration.
+ 'sesAccessKey' => 'email_ses_access_key',
+ 'sesSecretKey' => 'email_ses_secret_key',
+ 'sesRegion' => 'email_ses_region',
+
+ // Mailjet configuration.
+ 'mailjetApiKey' => 'email_mailjet_api_key',
+ 'mailjetSecretKey' => 'email_mailjet_secret_key',
];
$updatedSettings = [];
foreach ($allowedSettings as $settingKey => $configKey) {
- if (array_key_exists($settingKey, $emailSettings)) {
+ if (array_key_exists($settingKey, $emailSettings) === true) {
$value = $emailSettings[$settingKey];
- // Convert boolean values to strings
- if (is_bool($value)) {
- $value = $value ? 'true' : 'false';
+ // Convert boolean values to strings.
+ if (is_bool($value) === true) {
+ if ($value === true) {
+ $value = 'true';
+ } else {
+ $value = 'false';
+ }
}
$this->config->setValueString($this->_appName, $configKey, (string) $value);
@@ -1756,12 +1954,12 @@ public function updateEmailSettings(array $emailSettings): array
$this->logger->info(
'Email settings updated successfully',
[
- 'updatedKeys' => array_keys($updatedSettings)
+ 'updatedKeys' => array_keys($updatedSettings),
]
);
return $updatedSettings;
- }
+ }//end updateEmailSettings()
/**
* Gets email template content for a specific template
@@ -1772,11 +1970,11 @@ public function updateEmailSettings(array $emailSettings): array
*/
public function getEmailTemplate(string $templateName): string
{
- $configKey = "email_template_{$templateName}";
- $defaultTemplate = $this->getDefaultEmailTemplate($templateName);
+ $configKey = "email_template_{$templateName}";
+ $defaultTemplate = $this->getDefaultEmailTemplate(templateName: $templateName);
return $this->config->getValueString($this->_appName, $configKey, $defaultTemplate);
- }
+ }//end getEmailTemplate()
/**
* Updates email template content
@@ -1795,21 +1993,21 @@ public function updateEmailTemplate(string $templateName, string $templateConten
$this->logger->info(
'Email template updated successfully',
[
- 'templateName' => $templateName
+ 'templateName' => $templateName,
]
);
return true;
} catch (\Exception $e) {
$this->logger->error(
- 'Failed to update email template: ' . $e->getMessage(),
+ 'Failed to update email template: '.$e->getMessage(),
[
- 'templateName' => $templateName
+ 'templateName' => $templateName,
]
);
return false;
- }
- }
+ }//end try
+ }//end updateEmailTemplate()
/**
* Gets default email template content
@@ -1835,7 +2033,7 @@ public function getDefaultEmailTemplate(string $templateName): string
Heeft u vragen? Neem dan contact met ons op.
Met vriendelijke groet, Het Software Catalogus Team
',
- 'organization_activation' => '
+ 'organization_activation' => '
Uw organisatie is geactiveerd!
Beste {{ organization.name }},
Goed nieuws! Uw organisatie is zojuist geactiveerd in de Software Catalogus.
@@ -1849,7 +2047,7 @@ public function getDefaultEmailTemplate(string $templateName): string
U kunt nu inloggen en gebruik maken van alle beschikbare functionaliteiten.
Met vriendelijke groet, Het Software Catalogus Team
',
- 'user_creation' => '
+ 'user_creation' => '
Welkom {{ user.name }}!
Beste {{ user.name }},
Er is een gebruikersaccount voor u aangemaakt in de Software Catalogus.
@@ -1863,7 +2061,7 @@ public function getDefaultEmailTemplate(string $templateName): string
Heeft u vragen over uw account? Neem dan contact met ons op.
Met vriendelijke groet, Het Software Catalogus Team
',
- 'user_password' => '
+ 'user_password' => '
Uw wachtwoord voor de Software Catalogus
Beste {{ user.name }},
Uw wachtwoord voor de Software Catalogus is aangepast.
@@ -1876,11 +2074,11 @@ public function getDefaultEmailTemplate(string $templateName): string
U kunt nu inloggen met uw nieuwe wachtwoord.
We raden u aan om uw wachtwoord te wijzigen na het eerste inloggen.
Met vriendelijke groet, Het Software Catalogus Team
- '
+ ',
];
return $templates[$templateName] ?? '';
- }
+ }//end getDefaultEmailTemplate()
/**
* Gets available email template variables for a specific template
@@ -1893,34 +2091,34 @@ public function getEmailTemplateVariables(string $templateName): array
{
$variables = [
'organization_registration' => [
- 'organization.name' => 'Organization name',
+ 'organization.name' => 'Organization name',
'organization.beoordeling' => 'Organization status (e.g., Actief)',
- 'organization.type' => 'Organization type (e.g., Leverancier)',
- 'organization.website' => 'Organization website',
+ 'organization.type' => 'Organization type (e.g., Leverancier)',
+ 'organization.website' => 'Organization website',
],
- 'organization_activation' => [
- 'organization.name' => 'Organization name',
+ 'organization_activation' => [
+ 'organization.name' => 'Organization name',
'organization.beoordeling' => 'Organization status (e.g., Actief)',
- 'organization.type' => 'Organization type',
- 'organization.website' => 'Organization website',
+ 'organization.type' => 'Organization type',
+ 'organization.website' => 'Organization website',
],
- 'user_creation' => [
- 'user.name' => 'User display name',
- 'user.email' => 'User email address',
- 'user.username' => 'Username',
+ 'user_creation' => [
+ 'user.name' => 'User display name',
+ 'user.email' => 'User email address',
+ 'user.username' => 'Username',
'user.organization.name' => 'Organization name (if applicable)',
],
- 'user_password' => [
- 'user.name' => 'User display name',
- 'user.email' => 'User email address',
- 'user.username' => 'Username',
- 'user.password' => 'Auto-generated password',
+ 'user_password' => [
+ 'user.name' => 'User display name',
+ 'user.email' => 'User email address',
+ 'user.username' => 'Username',
+ 'user.password' => 'Auto-generated password',
'user.organization.name' => 'Organization name (if applicable)',
- ]
+ ],
];
return $variables[$templateName] ?? [];
- }
+ }//end getEmailTemplateVariables()
/**
* Gets debug information for settings
@@ -1932,7 +2130,7 @@ public function getDebugInfo(): array
$debugInfo = [];
try {
- // Get current configuration values
+ // Get current configuration values.
$debugInfo['configuration'] = [];
$configKeys = [
'amef_organization_source',
@@ -1944,28 +2142,33 @@ public function getDebugInfo(): array
'voorzieningen_contactpersoon_source',
'voorzieningen_contactpersoon_register',
'voorzieningen_contactpersoon_schema',
- 'voorzieningen_register', // Sync service expects this key
+ 'voorzieningen_register',
+ // Sync service expects this key.
'organization_source',
'organization_register',
'organization_schema',
'contact_source',
'contact_register',
- 'contact_schema'
+ 'contact_schema',
];
foreach ($configKeys as $key) {
$value = $this->config->getValueString($this->_appName, $key, '');
- $debugInfo['configuration'][$key] = empty($value) ? '' : $value;
+ if (empty($value) === true) {
+ $debugInfo['configuration'][$key] = '';
+ } else {
+ $debugInfo['configuration'][$key] = $value;
+ }
}
- // Get group configurations
+ // Get group configurations.
$debugInfo['userGroups'] = [
- 'generic' => $this->getGenericUserGroups(),
+ 'generic' => $this->getGenericUserGroups(),
'organizationAdmin' => $this->getOrganizationAdminGroups(),
- 'superUser' => $this->getSuperUserGroups()
+ 'superUser' => $this->getSuperUserGroups(),
];
- // Get email settings (without sensitive data)
+ // Get email settings (without sensitive data).
$emailSettings = $this->getEmailSettings();
unset($emailSettings['smtpPassword']);
unset($emailSettings['sendgridApiKey']);
@@ -1975,14 +2178,14 @@ public function getDebugInfo(): array
unset($emailSettings['mailjetSecretKey']);
$debugInfo['emailSettings'] = $emailSettings;
- // Get OpenRegister status
+ // Get OpenRegister status.
$debugInfo['openRegister'] = [
- 'installed' => $this->isOpenRegisterInstalled(),
- 'enabled' => $this->isOpenRegisterEnabled(),
- 'availableRegisters' => []
+ 'installed' => $this->isOpenRegisterInstalled(),
+ 'enabled' => $this->isOpenRegisterEnabled(),
+ 'availableRegisters' => [],
];
- if ($debugInfo['openRegister']['installed'] && $debugInfo['openRegister']['enabled']) {
+ if ($debugInfo['openRegister']['installed'] === true && $debugInfo['openRegister']['enabled'] === true) {
try {
$registerService = $this->getRegisterService();
$debugInfo['openRegister']['availableRegisters'] = $registerService->findAll();
@@ -1990,13 +2193,12 @@ public function getDebugInfo(): array
$debugInfo['openRegister']['error'] = $e->getMessage();
}
}
-
} catch (\Exception $e) {
$debugInfo['error'] = $e->getMessage();
- }
+ }//end try
return $debugInfo;
- }
+ }//end getDebugInfo()
/**
* Sends a test email
@@ -2006,125 +2208,144 @@ public function getDebugInfo(): array
*
* @return array Result of the test email
*/
- public function sendTestEmail(string $email, array $emailSettings = []): array
+ public function sendTestEmail(string $email, array $emailSettings=[]): array
{
- // Validate email address first (business logic moved from controller)
- if (empty($email)) {
+ // Validate email address first (business logic moved from controller).
+ if (empty($email) === true) {
$this->logger->warning('SoftwareCatalog: Test email request missing email address');
return [
'success' => false,
- 'message' => 'Email address is required'
+ 'message' => 'Email address is required',
];
}
- $this->logger->info('SoftwareCatalog: Starting sendTestEmail process', [
- 'recipient' => $email,
- 'has_email_settings' => !empty($emailSettings)
- ]);
+ $this->logger->info(
+ 'SoftwareCatalog: Starting sendTestEmail process',
+ [
+ 'recipient' => $email,
+ 'has_email_settings' => empty($emailSettings) === false,
+ ]
+ );
try {
- // Ensure vendor autoloader is loaded
- include_once __DIR__ . '/../../vendor/autoload.php';
+ // Ensure vendor autoloader is loaded.
+ include_once __DIR__.'/../../vendor/autoload.php';
$this->logger->debug('SoftwareCatalog: Vendor autoloader loaded');
- // Use provided settings or fall back to stored settings
- if (empty($emailSettings)) {
+ // Use provided settings or fall back to stored settings.
+ if (empty($emailSettings) === true) {
$emailSettings = $this->getEmailSettings();
$this->logger->info('SoftwareCatalog: Loaded email settings from storage');
} else {
$this->logger->info('SoftwareCatalog: Using provided email settings');
}
- // Log the email configuration (without sensitive data)
- $this->logger->info('SoftwareCatalog: Email configuration', [
- 'enabled' => $emailSettings['enabled'] ?? false,
- 'transport_type' => $emailSettings['transportType'] ?? 'unknown',
- 'sender_email' => $emailSettings['senderEmail'] ?? 'not set',
- 'sender_name' => $emailSettings['senderName'] ?? 'not set',
- 'has_mailjet_api_key' => !empty($emailSettings['mailjetApiKey']),
- 'has_mailjet_secret_key' => !empty($emailSettings['mailjetSecretKey']),
- ]);
-
- // Check if email is enabled
- if (!($emailSettings['enabled'] ?? false)) {
+ // Log the email configuration (without sensitive data).
+ $this->logger->info(
+ 'SoftwareCatalog: Email configuration',
+ [
+ 'enabled' => $emailSettings['enabled'] ?? false,
+ 'transport_type' => $emailSettings['transportType'] ?? 'unknown',
+ 'sender_email' => $emailSettings['senderEmail'] ?? 'not set',
+ 'sender_name' => $emailSettings['senderName'] ?? 'not set',
+ 'has_mailjet_api_key' => empty($emailSettings['mailjetApiKey']) === false,
+ 'has_mailjet_secret_key' => empty($emailSettings['mailjetSecretKey']) === false,
+ ]
+ );
+
+ // Check if email is enabled.
+ if (($emailSettings['enabled'] ?? false) === false) {
$this->logger->warning('SoftwareCatalog: Email notifications are disabled');
return [
'success' => false,
- 'message' => 'Email notifications are disabled'
+ 'message' => 'Email notifications are disabled',
];
}
- // Use test receiver override if configured
+ // Use test receiver override if configured.
$recipient = $emailSettings['testReceiverOverride'] ?? $email;
- $this->logger->info('SoftwareCatalog: Final recipient determined', [
- 'original_recipient' => $email,
- 'final_recipient' => $recipient,
- 'using_override' => !empty($emailSettings['testReceiverOverride'])
- ]);
+ $this->logger->info(
+ 'SoftwareCatalog: Final recipient determined',
+ [
+ 'original_recipient' => $email,
+ 'final_recipient' => $recipient,
+ 'using_override' => empty($emailSettings['testReceiverOverride']) === false,
+ ]
+ );
- // Create transport based on configuration
+ // Create transport based on configuration.
$this->logger->info('SoftwareCatalog: Creating email transport');
- $transport = $this->createEmailTransport($emailSettings);
+ $transport = $this->createEmailTransport(emailSettings: $emailSettings);
$this->logger->info('SoftwareCatalog: Email transport created successfully');
$mailer = new Mailer($transport);
$this->logger->info('SoftwareCatalog: Mailer instance created');
- // Create test email
- $senderEmail = $emailSettings['senderEmail'] ?? 'noreply@softwarecatalogus.nl';
- $senderName = $emailSettings['senderName'] ?? 'Software Catalogus';
+ // Create test email.
+ $senderEmail = $emailSettings['senderEmail'] ?? 'noreply@softwarecatalogus.nl';
+ $senderName = $emailSettings['senderName'] ?? 'Software Catalogus';
$transportType = $emailSettings['transportType'] ?? 'smtp';
- $this->logger->info('SoftwareCatalog: Creating email message', [
- 'sender_email' => $senderEmail,
- 'sender_name' => $senderName,
- 'transport_type' => $transportType,
- 'recipient' => $recipient
- ]);
+ $this->logger->info(
+ 'SoftwareCatalog: Creating email message',
+ [
+ 'sender_email' => $senderEmail,
+ 'sender_name' => $senderName,
+ 'transport_type' => $transportType,
+ 'recipient' => $recipient,
+ ]
+ );
$email = (new Email())
->from(new Address($senderEmail, $senderName))
->to($recipient)
->subject('Software Catalogus - Test Email')
- ->html('
+ ->html(
+ '
Test Email - Software Catalogus
Dit is een test email van de Software Catalogus.
Als u deze email ontvangt, werkt het email systeem correct.
- Transport Type: ' . htmlspecialchars($transportType) . '
- Datum: ' . date('Y-m-d H:i:s') . '
+ Transport Type: '.htmlspecialchars($transportType).'
+ Datum: '.date('Y-m-d H:i:s').'
Met vriendelijke groet, Het Software Catalogus Team
- ');
+ '
+ );
$this->logger->info('SoftwareCatalog: Email message created, attempting to send');
- // Send the email
+ // Send the email.
$mailer->send($email);
- $this->logger->info('SoftwareCatalog: Email sent successfully via Symfony Mailer', [
- 'recipient' => $recipient,
- 'transport' => $transportType,
- 'sender' => $senderEmail
- ]);
+ $this->logger->info(
+ 'SoftwareCatalog: Email sent successfully via Symfony Mailer',
+ [
+ 'recipient' => $recipient,
+ 'transport' => $transportType,
+ 'sender' => $senderEmail,
+ ]
+ );
return [
'success' => true,
- 'message' => "Test email sent successfully to {$recipient} via {$transportType}"
+ 'message' => "Test email sent successfully to {$recipient} via {$transportType}",
];
-
} catch (\Exception $e) {
- $this->logger->error('SoftwareCatalog: Failed to send test email', [
- 'recipient' => $email,
- 'exception_class' => get_class($e),
- 'exception_message' => $e->getMessage(),
- 'exception_code' => $e->getCode(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'SoftwareCatalog: Failed to send test email',
+ [
+ 'recipient' => $email,
+ 'exception_class' => get_class($e),
+ 'exception_message' => $e->getMessage(),
+ 'exception_code' => $e->getCode(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return [
'success' => false,
- 'message' => 'Failed to send test email: ' . $e->getMessage()
+ 'message' => 'Failed to send test email: '.$e->getMessage(),
];
- }
- }
+ }//end try
+ }//end sendTestEmail()
/**
* Test email connection without sending an actual email
@@ -2133,96 +2354,108 @@ public function sendTestEmail(string $email, array $emailSettings = []): array
*
* @return array Result of the connection test
*/
- public function testEmailConnection(array $emailSettings = []): array
+ public function testEmailConnection(array $emailSettings=[]): array
{
- $this->logger->info('SoftwareCatalog: Starting email connection test', [
- 'has_email_settings' => !empty($emailSettings)
- ]);
+ $this->logger->info(
+ 'SoftwareCatalog: Starting email connection test',
+ [
+ 'has_email_settings' => empty($emailSettings) === false,
+ ]
+ );
try {
- // Ensure vendor autoloader is loaded
- include_once __DIR__ . '/../../vendor/autoload.php';
+ // Ensure vendor autoloader is loaded.
+ include_once __DIR__.'/../../vendor/autoload.php';
$this->logger->debug('SoftwareCatalog: Vendor autoloader loaded');
- // Use provided settings or fall back to stored settings
- if (empty($emailSettings)) {
+ // Use provided settings or fall back to stored settings.
+ if (empty($emailSettings) === true) {
$emailSettings = $this->getEmailSettings();
$this->logger->info('SoftwareCatalog: Loaded email settings from storage');
} else {
$this->logger->info('SoftwareCatalog: Using provided email settings');
}
- // Log the email configuration (without sensitive data)
- $this->logger->info('SoftwareCatalog: Email configuration for connection test', [
- 'enabled' => $emailSettings['enabled'] ?? false,
- 'transport_type' => $emailSettings['transportType'] ?? 'unknown',
- 'sender_email' => $emailSettings['senderEmail'] ?? 'not set',
- 'sender_name' => $emailSettings['senderName'] ?? 'not set',
- 'has_credentials' => $this->hasValidCredentials($emailSettings)
- ]);
-
- // Check if email is enabled
- if (!($emailSettings['enabled'] ?? false)) {
+ // Log the email configuration (without sensitive data).
+ $this->logger->info(
+ 'SoftwareCatalog: Email configuration for connection test',
+ [
+ 'enabled' => $emailSettings['enabled'] ?? false,
+ 'transport_type' => $emailSettings['transportType'] ?? 'unknown',
+ 'sender_email' => $emailSettings['senderEmail'] ?? 'not set',
+ 'sender_name' => $emailSettings['senderName'] ?? 'not set',
+ 'has_credentials' => $this->hasValidCredentials(emailSettings: $emailSettings),
+ ]
+ );
+
+ // Check if email is enabled.
+ if (($emailSettings['enabled'] ?? false) === false) {
$this->logger->warning('SoftwareCatalog: Email notifications are disabled');
return [
'success' => false,
- 'message' => 'Email notifications are disabled'
+ 'message' => 'Email notifications are disabled',
];
}
- // Validate basic settings
+ // Validate basic settings.
$transportType = $emailSettings['transportType'] ?? 'smtp';
- $senderEmail = $emailSettings['senderEmail'] ?? '';
+ $senderEmail = $emailSettings['senderEmail'] ?? '';
- if (empty($senderEmail)) {
+ if (empty($senderEmail) === true) {
return [
'success' => false,
- 'message' => 'Sender email address is required'
+ 'message' => 'Sender email address is required',
];
}
- // Create transport based on configuration (this tests the connection)
+ // Create transport based on configuration (this tests the connection).
$this->logger->info('SoftwareCatalog: Creating email transport for connection test');
- $transport = $this->createEmailTransport($emailSettings);
+ $transport = $this->createEmailTransport(emailSettings: $emailSettings);
$this->logger->info('SoftwareCatalog: Email transport created successfully');
- // Test the connection by creating a mailer instance
+ // Test the connection by creating a mailer instance.
$mailer = new Mailer($transport);
$this->logger->info('SoftwareCatalog: Mailer instance created for connection test');
- // For some transports, we can test the connection more directly
- $connectionDetails = $this->getConnectionDetails($emailSettings);
+ // For some transports, we can test the connection more directly.
+ $connectionDetails = $this->getConnectionDetails(emailSettings: $emailSettings);
- $this->logger->info('SoftwareCatalog: Email connection test completed successfully', [
- 'transport' => $transportType,
- 'sender' => $senderEmail
- ]);
+ $this->logger->info(
+ 'SoftwareCatalog: Email connection test completed successfully',
+ [
+ 'transport' => $transportType,
+ 'sender' => $senderEmail,
+ ]
+ );
return [
'success' => true,
'message' => "Email connection test successful for {$transportType}",
- 'details' => $connectionDetails
+ 'details' => $connectionDetails,
];
-
} catch (\Exception $e) {
- $this->logger->error('SoftwareCatalog: Email connection test failed', [
- 'exception_class' => get_class($e),
- 'exception_message' => $e->getMessage(),
- 'exception_code' => $e->getCode(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'SoftwareCatalog: Email connection test failed',
+ [
+ 'exception_class' => get_class($e),
+ 'exception_message' => $e->getMessage(),
+ 'exception_code' => $e->getCode(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return [
'success' => false,
- 'message' => 'Email connection test failed: ' . $e->getMessage()
+ 'message' => 'Email connection test failed: '.$e->getMessage(),
];
- }
- }
+ }//end try
+ }//end testEmailConnection()
/**
* Check if email settings have valid credentials for the transport type
*
- * @param array $emailSettings Email settings
- * @return bool True if credentials are present
+ * @param array $emailSettings Email settings.
+ *
+ * @return bool True if credentials are present.
*/
private function hasValidCredentials(array $emailSettings): bool
{
@@ -2230,27 +2463,28 @@ private function hasValidCredentials(array $emailSettings): bool
switch ($transportType) {
case 'smtp':
- return !empty($emailSettings['smtpHost']) && !empty($emailSettings['smtpPort']);
+ return empty($emailSettings['smtpHost']) === false && empty($emailSettings['smtpPort']) === false;
case 'mailjet':
- return !empty($emailSettings['mailjetApiKey']) && !empty($emailSettings['mailjetSecretKey']);
+ return empty($emailSettings['mailjetApiKey']) === false && empty($emailSettings['mailjetSecretKey']) === false;
case 'sendgrid':
- return !empty($emailSettings['sendgridApiKey']);
+ return empty($emailSettings['sendgridApiKey']) === false;
case 'mailgun':
- return !empty($emailSettings['mailgunApiKey']) && !empty($emailSettings['mailgunDomain']);
+ return empty($emailSettings['mailgunApiKey']) === false && empty($emailSettings['mailgunDomain']) === false;
case 'postmark':
- return !empty($emailSettings['postmarkApiKey']);
+ return empty($emailSettings['postmarkApiKey']) === false;
case 'ses':
- return !empty($emailSettings['sesAccessKey']) && !empty($emailSettings['sesSecretKey']);
+ return empty($emailSettings['sesAccessKey']) === false && empty($emailSettings['sesSecretKey']) === false;
default:
return false;
}
- }
+ }//end hasValidCredentials()
/**
* Get connection details for the email transport
*
- * @param array $emailSettings Email settings
- * @return array Connection details
+ * @param array $emailSettings Email settings.
+ *
+ * @return array Connection details.
*/
private function getConnectionDetails(array $emailSettings): array
{
@@ -2258,100 +2492,120 @@ private function getConnectionDetails(array $emailSettings): array
switch ($transportType) {
case 'smtp':
+ if (empty($emailSettings['smtpUsername']) === false) {
+ $usernameValue = '***';
+ } else {
+ $usernameValue = 'none';
+ }
return [
- 'type' => 'SMTP',
- 'host' => $emailSettings['smtpHost'] ?? '',
- 'port' => $emailSettings['smtpPort'] ?? '',
+ 'type' => 'SMTP',
+ 'host' => $emailSettings['smtpHost'] ?? '',
+ 'port' => $emailSettings['smtpPort'] ?? '',
'encryption' => $emailSettings['smtpEncryption'] ?? 'none',
- 'username' => !empty($emailSettings['smtpUsername']) ? '***' : 'none'
+ 'username' => $usernameValue,
];
case 'mailjet':
return [
- 'type' => 'Mailjet API',
- 'has_api_key' => !empty($emailSettings['mailjetApiKey']),
- 'has_secret_key' => !empty($emailSettings['mailjetSecretKey'])
+ 'type' => 'Mailjet API',
+ 'has_api_key' => empty($emailSettings['mailjetApiKey']) === false,
+ 'has_secret_key' => empty($emailSettings['mailjetSecretKey']) === false,
];
case 'sendgrid':
return [
- 'type' => 'SendGrid API',
- 'has_api_key' => !empty($emailSettings['sendgridApiKey'])
+ 'type' => 'SendGrid API',
+ 'has_api_key' => empty($emailSettings['sendgridApiKey']) === false,
];
case 'mailgun':
return [
- 'type' => 'Mailgun API',
- 'has_api_key' => !empty($emailSettings['mailgunApiKey']),
- 'domain' => $emailSettings['mailgunDomain'] ?? ''
+ 'type' => 'Mailgun API',
+ 'has_api_key' => empty($emailSettings['mailgunApiKey']) === false,
+ 'domain' => $emailSettings['mailgunDomain'] ?? '',
];
case 'postmark':
return [
- 'type' => 'Postmark API',
- 'has_api_key' => !empty($emailSettings['postmarkApiKey'])
+ 'type' => 'Postmark API',
+ 'has_api_key' => empty($emailSettings['postmarkApiKey']) === false,
];
case 'ses':
return [
- 'type' => 'Amazon SES',
- 'has_access_key' => !empty($emailSettings['sesAccessKey']),
- 'has_secret_key' => !empty($emailSettings['sesSecretKey']),
- 'region' => $emailSettings['sesRegion'] ?? 'us-east-1'
+ 'type' => 'Amazon SES',
+ 'has_access_key' => empty($emailSettings['sesAccessKey']) === false,
+ 'has_secret_key' => empty($emailSettings['sesSecretKey']) === false,
+ 'region' => $emailSettings['sesRegion'] ?? 'us-east-1',
];
default:
return ['type' => $transportType];
- }
- }
+ }//end switch
+ }//end getConnectionDetails()
/**
* Creates an email transport based on configuration
*
- * @param array $emailSettings Email settings
+ * @param array $emailSettings Email settings.
+ *
* @return \Symfony\Component\Mailer\Transport\TransportInterface
- * @throws \Exception If transport configuration is invalid
+ *
+ * @throws \Exception If transport configuration is invalid.
*/
private function createEmailTransport(array $emailSettings): \Symfony\Component\Mailer\Transport\TransportInterface
{
$transportType = $emailSettings['transportType'] ?? 'smtp';
- $this->logger->info('SoftwareCatalog: Creating transport', [
- 'transport_type' => $transportType
- ]);
+ $this->logger->info(
+ 'SoftwareCatalog: Creating transport',
+ [
+ 'transport_type' => $transportType,
+ ]
+ );
switch ($transportType) {
case 'mailjet':
$this->logger->info('SoftwareCatalog: Creating Mailjet transport');
- return $this->createMailjetTransport($emailSettings);
+ return $this->createMailjetTransport(settings: $emailSettings);
case 'smtp':
$this->logger->info('SoftwareCatalog: Creating SMTP transport');
- return $this->createSmtpTransport($emailSettings);
+ return $this->createSmtpTransport(settings: $emailSettings);
default:
- $this->logger->error('SoftwareCatalog: Unsupported transport type', [
- 'transport_type' => $transportType
- ]);
+ $this->logger->error(
+ 'SoftwareCatalog: Unsupported transport type',
+ [
+ 'transport_type' => $transportType,
+ ]
+ );
throw new \InvalidArgumentException("Unsupported transport type: {$transportType}");
}
- }
+ }//end createEmailTransport()
/**
* Creates a Mailjet transport
*
- * @param array $settings Email settings
+ * @param array $settings Email settings.
+ *
* @return \Symfony\Component\Mailer\Transport\TransportInterface
*/
private function createMailjetTransport(array $settings): \Symfony\Component\Mailer\Transport\TransportInterface
{
- $apiKey = $settings['mailjetApiKey'] ?? '';
+ $apiKey = $settings['mailjetApiKey'] ?? '';
$secretKey = $settings['mailjetSecretKey'] ?? '';
- $this->logger->info('SoftwareCatalog: Mailjet transport configuration', [
- 'has_api_key' => !empty($apiKey),
- 'api_key_length' => strlen($apiKey),
- 'has_secret_key' => !empty($secretKey),
- 'secret_key_length' => strlen($secretKey)
- ]);
-
- if (empty($apiKey) || empty($secretKey)) {
- $this->logger->error('SoftwareCatalog: Mailjet API key and secret key are required', [
- 'api_key_empty' => empty($apiKey),
- 'secret_key_empty' => empty($secretKey)
- ]);
+ $this->logger->info(
+ 'SoftwareCatalog: Mailjet transport configuration',
+ [
+ 'has_api_key' => empty($apiKey) === false,
+ 'api_key_length' => strlen($apiKey),
+ 'has_secret_key' => empty($secretKey) === false,
+ 'secret_key_length' => strlen($secretKey),
+ ]
+ );
+
+ if (empty($apiKey) === true || empty($secretKey) === true) {
+ $this->logger->error(
+ 'SoftwareCatalog: Mailjet API key and secret key are required',
+ [
+ 'api_key_empty' => empty($apiKey) === true,
+ 'secret_key_empty' => empty($secretKey) === true,
+ ]
+ );
throw new \InvalidArgumentException('Mailjet API key and secret key are required');
}
@@ -2361,46 +2615,59 @@ private function createMailjetTransport(array $settings): \Symfony\Component\Mai
urlencode($secretKey)
);
- $this->logger->info('SoftwareCatalog: Creating Mailjet transport with DSN', [
- 'dsn_pattern' => 'mailjet+api://***:***@default'
- ]);
+ $this->logger->info(
+ 'SoftwareCatalog: Creating Mailjet transport with DSN',
+ [
+ 'dsn_pattern' => 'mailjet+api://***:***@default',
+ ]
+ );
try {
$transport = Transport::fromDsn($dsn);
- $this->logger->info('SoftwareCatalog: Mailjet transport created successfully', [
- 'transport_class' => get_class($transport)
- ]);
+ $this->logger->info(
+ 'SoftwareCatalog: Mailjet transport created successfully',
+ [
+ 'transport_class' => get_class($transport),
+ ]
+ );
return $transport;
} catch (\Exception $e) {
- $this->logger->error('SoftwareCatalog: Failed to create Mailjet transport', [
- 'exception_class' => get_class($e),
- 'exception_message' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'SoftwareCatalog: Failed to create Mailjet transport',
+ [
+ 'exception_class' => get_class($e),
+ 'exception_message' => $e->getMessage(),
+ ]
+ );
throw $e;
}
- }
+ }//end createMailjetTransport()
/**
* Creates an SMTP transport
*
- * @param array $settings Email settings
+ * @param array $settings Email settings.
+ *
* @return \Symfony\Component\Mailer\Transport\TransportInterface
*/
private function createSmtpTransport(array $settings): \Symfony\Component\Mailer\Transport\TransportInterface
{
- $host = $settings['smtpHost'] ?? 'localhost';
- $port = $settings['smtpPort'] ?? 587;
+ $host = $settings['smtpHost'] ?? 'localhost';
+ $port = $settings['smtpPort'] ?? 587;
$encryption = $settings['smtpEncryption'] ?? 'tls';
- $username = $settings['smtpUsername'] ?? '';
- $password = $settings['smtpPassword'] ?? '';
+ $username = $settings['smtpUsername'] ?? '';
+ $password = $settings['smtpPassword'] ?? '';
- $this->logger->info('SoftwareCatalog: SMTP transport configuration', [
- 'host' => $host,
- 'port' => $port,
- 'encryption' => $encryption,
- 'has_username' => !empty($username),
- 'has_password' => !empty($password)
- ]);
+ $this->logger->info(
+ 'SoftwareCatalog: SMTP transport configuration',
+ [
+ 'host' => $host,
+ 'port' => $port,
+ 'encryption' => $encryption,
+ 'has_username' => empty($username) === false,
+ 'has_password' => empty($password) === false,
+ ]
+ );
$dsn = sprintf(
'smtp://%s:%s@%s:%d',
@@ -2410,28 +2677,45 @@ private function createSmtpTransport(array $settings): \Symfony\Component\Mailer
$port
);
- if ($encryption && $encryption !== 'none') {
- $dsn .= '?encryption=' . $encryption;
+ if ($encryption !== false && $encryption !== 'none') {
+ $dsn .= '?encryption='.$encryption;
+ }
+
+ if (empty($encryption) === false && $encryption !== 'none') {
+ $encSuffix = '?encryption='.$encryption;
+ } else {
+ $encSuffix = '';
}
- $this->logger->info('SoftwareCatalog: Creating SMTP transport with DSN', [
- 'dsn_pattern' => sprintf('smtp://***:***@%s:%d%s', $host, $port, $encryption && $encryption !== 'none' ? '?encryption=' . $encryption : '')
- ]);
+ $dsnPattern = sprintf('smtp://***:***@%s:%d%s', $host, $port, $encSuffix);
+
+ $this->logger->info(
+ 'SoftwareCatalog: Creating SMTP transport with DSN',
+ [
+ 'dsn_pattern' => $dsnPattern,
+ ]
+ );
try {
$transport = Transport::fromDsn($dsn);
- $this->logger->info('SoftwareCatalog: SMTP transport created successfully', [
- 'transport_class' => get_class($transport)
- ]);
+ $this->logger->info(
+ 'SoftwareCatalog: SMTP transport created successfully',
+ [
+ 'transport_class' => get_class($transport),
+ ]
+ );
return $transport;
} catch (\Exception $e) {
- $this->logger->error('SoftwareCatalog: Failed to create SMTP transport', [
- 'exception_class' => get_class($e),
- 'exception_message' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'SoftwareCatalog: Failed to create SMTP transport',
+ [
+ 'exception_class' => get_class($e),
+ 'exception_message' => $e->getMessage(),
+ ]
+ );
throw $e;
}
- }
+ }//end createSmtpTransport()
/**
* Check if settings should be loaded based on version comparison.
@@ -2444,50 +2728,61 @@ private function createSmtpTransport(array $settings): \Symfony\Component\Mailer
private function shouldLoadSettings(): bool
{
try {
- // Get the current app version
+ // Get the current app version.
$currentAppVersion = $this->appManager->getAppVersion(\OCA\SoftwareCatalog\AppInfo\Application::APP_ID);
- $this->logger->info('SettingsService: Checking if settings should be loaded', [
- 'current_app_version' => $currentAppVersion
- ]);
+ $this->logger->info(
+ 'SettingsService: Checking if settings should be loaded',
+ [
+ 'current_app_version' => $currentAppVersion,
+ ]
+ );
- // Get the configuration service to check stored version
+ // Get the configuration service to check stored version.
$configurationService = $this->getConfigurationService();
- $storedVersion = $configurationService->getConfiguredAppVersion(\OCA\SoftwareCatalog\AppInfo\Application::APP_ID);
+ $storedVersion = $configurationService->getConfiguredAppVersion(\OCA\SoftwareCatalog\AppInfo\Application::APP_ID);
- $this->logger->info('SettingsService: Version comparison details', [
- 'current_app_version' => $currentAppVersion,
- 'stored_config_version' => $storedVersion,
- 'stored_version_is_null' => $storedVersion === null
- ]);
+ $this->logger->info(
+ 'SettingsService: Version comparison details',
+ [
+ 'current_app_version' => $currentAppVersion,
+ 'stored_config_version' => $storedVersion,
+ 'stored_version_is_null' => $storedVersion === null,
+ ]
+ );
- // If no stored version exists, we need to load settings
+ // If no stored version exists, we need to load settings.
if ($storedVersion === null) {
$this->logger->info('SettingsService: No stored version found, settings should be loaded');
return true;
}
- // Compare versions using semantic versioning
- // Load settings if current version is newer than stored version
+ // Compare versions using semantic versioning.
+ // Load settings if current version is newer than stored version.
$shouldLoad = version_compare($currentAppVersion, $storedVersion, '>');
- $this->logger->info('SettingsService: Version comparison result', [
- 'current_version' => $currentAppVersion,
- 'stored_version' => $storedVersion,
- 'should_load' => $shouldLoad,
- 'version_compare_result' => version_compare($currentAppVersion, $storedVersion)
- ]);
+ $this->logger->info(
+ 'SettingsService: Version comparison result',
+ [
+ 'current_version' => $currentAppVersion,
+ 'stored_version' => $storedVersion,
+ 'should_load' => $shouldLoad,
+ 'version_compare_result' => version_compare($currentAppVersion, $storedVersion),
+ ]
+ );
return $shouldLoad;
-
} catch (\Exception $e) {
- // If we can't determine versions, err on the side of loading settings
- $this->logger->warning('Failed to check if settings should be loaded: ' . $e->getMessage(), [
- 'exception' => $e
- ]);
+ // If we can't determine versions, err on the side of loading settings.
+ $this->logger->warning(
+ 'Failed to check if settings should be loaded: '.$e->getMessage(),
+ [
+ 'exception' => $e,
+ ]
+ );
return true;
- }
- }
+ }//end try
+ }//end shouldLoadSettings()
/**
* Get version information for the app and configuration.
@@ -2501,60 +2796,75 @@ private function shouldLoadSettings(): bool
public function getVersionInfo(): array
{
try {
- // Get the current app version
+ // Get the current app version.
$currentAppVersion = $this->appManager->getAppVersion(\OCA\SoftwareCatalog\AppInfo\Application::APP_ID);
- $this->logger->debug('SettingsService: Getting version information', [
- 'current_app_version' => $currentAppVersion
- ]);
+ $this->logger->debug(
+ 'SettingsService: Getting version information',
+ [
+ 'current_app_version' => $currentAppVersion,
+ ]
+ );
- // Get the configuration service to check stored version
+ // Get the configuration service to check stored version.
$configurationService = $this->getConfigurationService();
- $storedConfigVersion = null;
+ $storedConfigVersion = null;
try {
$storedConfigVersion = $configurationService->getConfiguredAppVersion(\OCA\SoftwareCatalog\AppInfo\Application::APP_ID);
} catch (\Exception $e) {
- $this->logger->warning('SettingsService: Could not retrieve stored configuration version', [
- 'exception_message' => $e->getMessage()
- ]);
- // Continue with null stored version
+ $this->logger->warning(
+ 'SettingsService: Could not retrieve stored configuration version',
+ [
+ 'exception_message' => $e->getMessage(),
+ ]
+ );
+ // Continue with null stored version.
}
- // Determine if versions match
+ // Determine if versions match.
$versionsMatch = $storedConfigVersion !== null &&
version_compare($currentAppVersion, $storedConfigVersion, '=');
$needsUpdate = $storedConfigVersion === null ||
version_compare($currentAppVersion, $storedConfigVersion, '>');
- // Check OpenRegister status
+ // Check OpenRegister status.
$openRegisterInstalled = $this->isOpenRegisterInstalled();
- $openRegisterEnabled = $openRegisterInstalled && $this->isOpenRegisterEnabled();
+ $openRegisterEnabled = $openRegisterInstalled && $this->isOpenRegisterEnabled();
+
+ if ($storedConfigVersion !== null) {
+ $versionComparisonValue = version_compare($currentAppVersion, $storedConfigVersion);
+ } else {
+ $versionComparisonValue = null;
+ }
$versionInfo = [
- 'appName' => 'SoftwareCatalog',
- 'appVersion' => $currentAppVersion,
- 'configuredVersion' => $storedConfigVersion,
- 'versionsMatch' => $versionsMatch,
- 'needsUpdate' => $needsUpdate,
- 'versionComparison' => $storedConfigVersion !== null ? version_compare($currentAppVersion, $storedConfigVersion) : null,
- 'isFullyConfigured' => $this->isFullyConfigured(),
- 'autoConfigCompleted' => $this->config->getValueString($this->_appName, 'auto_config_completed', 'false') === 'true',
+ 'appName' => 'SoftwareCatalog',
+ 'appVersion' => $currentAppVersion,
+ 'configuredVersion' => $storedConfigVersion,
+ 'versionsMatch' => $versionsMatch,
+ 'needsUpdate' => $needsUpdate,
+ 'versionComparison' => $versionComparisonValue,
+ 'isFullyConfigured' => $this->isFullyConfigured(),
+ 'autoConfigCompleted' => $this->config->getValueString($this->_appName, 'auto_config_completed', 'false') === 'true',
'openRegisterInstalled' => $openRegisterInstalled,
- 'openRegisterEnabled' => $openRegisterEnabled
+ 'openRegisterEnabled' => $openRegisterEnabled,
];
$this->logger->info('SettingsService: Version information compiled', $versionInfo);
return $versionInfo;
} catch (\Exception $e) {
- $this->logger->error('SettingsService: Failed to get version information', [
- 'exception' => $e
- ]);
- throw new \RuntimeException('Failed to get version information: ' . $e->getMessage());
- }
- }
+ $this->logger->error(
+ 'SettingsService: Failed to get version information',
+ [
+ 'exception' => $e,
+ ]
+ );
+ throw new \RuntimeException('Failed to get version information: '.$e->getMessage());
+ }//end try
+ }//end getVersionInfo()
/**
* Forces a complete configuration update regardless of version checks
@@ -2569,63 +2879,74 @@ public function forceUpdate(): array
try {
$this->logger->info('SettingsService: Starting force update');
- // Reset auto-configuration flag
+ // Reset auto-configuration flag.
$this->config->setValueString($this->_appName, 'auto_config_completed', 'false');
- // Perform forced import
- $importResult = $this->manualImport(true);
+ // Perform forced import.
+ $importResult = $this->manualImport(forceImport: true);
- if (!$importResult['success']) {
+ if ($importResult['success'] === false) {
return [
- 'success' => false,
- 'message' => 'Force update failed during import: ' . ($importResult['message'] ?? 'Unknown error'),
- 'importResult' => $importResult
+ 'success' => false,
+ 'message' => 'Force update failed during import: '.($importResult['message'] ?? 'Unknown error'),
+ 'importResult' => $importResult,
];
}
- // Verify configuration after force update
- $finalVersionInfo = $this->getVersionInfo();
+ // Verify configuration after force update.
+ $finalVersionInfo = $this->getVersionInfo();
$finalConfigStatus = $this->getConfigurationStatus();
- // For force update, if import succeeded, consider it successful
- // Version matching is less critical since we forced the update
- $success = $importResult['success'] && ($finalVersionInfo['isFullyConfigured'] || $finalVersionInfo['versionsMatch']);
+ // For force update, if import succeeded, consider it successful.
+ // Version matching is less critical since we forced the update.
+ $success = $importResult['success'] && ($finalVersionInfo['isFullyConfigured'] !== false || $finalVersionInfo['versionsMatch'] === true);
+
+ $this->logger->info(
+ 'SettingsService: Force update completed',
+ [
+ 'success' => $success,
+ 'import_success' => $importResult['success'],
+ 'final_version_info' => $finalVersionInfo,
+ 'final_config_status' => $finalConfigStatus,
+ ]
+ );
- $this->logger->info('SettingsService: Force update completed', [
- 'success' => $success,
- 'import_success' => $importResult['success'],
- 'final_version_info' => $finalVersionInfo,
- 'final_config_status' => $finalConfigStatus
- ]);
+ // Return concise response to avoid serialization issues with large nested structures.
+ if ($success === true) {
+ $messageValue = 'Force update completed successfully';
+ } else {
+ $messageValue = 'Force update completed but configuration needs attention';
+ }
- // Return concise response to avoid serialization issues with large nested structures
return [
- 'success' => $success,
- 'message' => $success ? 'Force update completed successfully' : 'Force update completed but configuration needs attention',
- 'importSuccess' => $importResult['success'] ?? false,
- 'importMessage' => $importResult['message'] ?? '',
- 'finalVersionInfo' => [
- 'appVersion' => $finalVersionInfo['appVersion'] ?? null,
+ 'success' => $success,
+ 'message' => $messageValue,
+ 'importSuccess' => $importResult['success'] ?? false,
+ 'importMessage' => $importResult['message'] ?? '',
+ 'finalVersionInfo' => [
+ 'appVersion' => $finalVersionInfo['appVersion'] ?? null,
'configuredVersion' => $finalVersionInfo['configuredVersion'] ?? null,
- 'versionsMatch' => $finalVersionInfo['versionsMatch'] ?? false,
- 'needsUpdate' => $finalVersionInfo['needsUpdate'] ?? false,
- 'isFullyConfigured' => $finalVersionInfo['isFullyConfigured'] ?? false
+ 'versionsMatch' => $finalVersionInfo['versionsMatch'] ?? false,
+ 'needsUpdate' => $finalVersionInfo['needsUpdate'] ?? false,
+ 'isFullyConfigured' => $finalVersionInfo['isFullyConfigured'] ?? false,
],
- 'finalConfigStatus' => $finalConfigStatus
+ 'finalConfigStatus' => $finalConfigStatus,
];
-
} catch (\Exception $e) {
- $this->logger->error('SettingsService: Force update failed', [
- 'exception_message' => $e->getMessage(),
- 'exception' => $e
- ]);
+ $this->logger->error(
+ 'SettingsService: Force update failed',
+ [
+ 'exception_message' => $e->getMessage(),
+ 'exception' => $e,
+ ]
+ );
return [
'success' => false,
- 'message' => 'Force update failed: ' . $e->getMessage(),
- 'error' => $e->getMessage()
+ 'message' => 'Force update failed: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
];
- }
- }
+ }//end try
+ }//end forceUpdate()
/**
* Resets the auto-configuration to allow it to run again
@@ -2637,20 +2958,23 @@ public function forceUpdate(): array
*
* @return array The reset results
*/
- public function resetAutoConfiguration(bool $resetConfiguration = false): array
+ public function resetAutoConfiguration(bool $resetConfiguration=false): array
{
try {
- $this->logger->info('Resetting auto-configuration', [
- 'reset_configuration' => $resetConfiguration
- ]);
+ $this->logger->info(
+ 'Resetting auto-configuration',
+ [
+ 'reset_configuration' => $resetConfiguration,
+ ]
+ );
- // Reset the auto-configuration completion flag
+ // Reset the auto-configuration completion flag.
$this->config->setValueString($this->_appName, 'auto_config_completed', 'false');
$resetItems = ['auto_config_completed_flag'];
- if ($resetConfiguration) {
- // Reset schema and register configurations
+ if (empty($resetConfiguration) === false) {
+ // Reset schema and register configurations.
$configKeysToReset = [
'voorzieningen_organisatie_source',
'voorzieningen_organisatie_register',
@@ -2663,7 +2987,7 @@ public function resetAutoConfiguration(bool $resetConfiguration = false): array
'organization_schema',
'contact_source',
'contact_register',
- 'contact_schema'
+ 'contact_schema',
];
foreach ($configKeysToReset as $key) {
@@ -2671,27 +2995,29 @@ public function resetAutoConfiguration(bool $resetConfiguration = false): array
}
$resetItems[] = 'schema_register_configurations';
- }
+ }//end if
- $this->logger->info('Auto-configuration reset completed', [
- 'reset_items' => $resetItems
- ]);
+ $this->logger->info(
+ 'Auto-configuration reset completed',
+ [
+ 'reset_items' => $resetItems,
+ ]
+ );
return [
- 'success' => true,
- 'message' => 'Auto-configuration reset successfully',
- 'reset_items' => $resetItems
+ 'success' => true,
+ 'message' => 'Auto-configuration reset successfully',
+ 'reset_items' => $resetItems,
];
-
} catch (\Exception $e) {
- $this->logger->error('Failed to reset auto-configuration: ' . $e->getMessage());
+ $this->logger->error('Failed to reset auto-configuration: '.$e->getMessage());
return [
'success' => false,
- 'message' => 'Failed to reset auto-configuration: ' . $e->getMessage(),
- 'error' => $e->getMessage()
+ 'message' => 'Failed to reset auto-configuration: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
];
- }
- }
+ }//end try
+ }//end resetAutoConfiguration()
/**
* Manually trigger configuration import from JSON.
@@ -2703,103 +3029,127 @@ public function resetAutoConfiguration(bool $resetConfiguration = false): array
*
* @return array The import results with success/error information.
*/
- public function manualImport(bool $forceImport = false): array
+ public function manualImport(bool $forceImport=false): array
{
try {
- $this->logger->info('SettingsService: Starting manual import', [
- 'force_import' => $forceImport
- ]);
+ $this->logger->info(
+ 'SettingsService: Starting manual import',
+ [
+ 'force_import' => $forceImport,
+ ]
+ );
- // Get version info first
+ // Get version info first.
$versionInfo = $this->getVersionInfo();
$this->logger->info('SettingsService: Pre-import version info', $versionInfo);
- // Check if import is needed (unless forced)
- if (!$forceImport && $versionInfo['versionsMatch'] && $versionInfo['isFullyConfigured']) {
+ // Check if import is needed (unless forced).
+ if ($forceImport === null && $versionInfo['versionsMatch'] === true && $versionInfo['isFullyConfigured'] === true) {
$this->logger->info('SettingsService: Import not needed - versions match and fully configured');
return [
- 'success' => false,
- 'message' => 'Configuration is already up to date. Use force import if you want to reimport.',
- 'versionInfo' => $versionInfo
+ 'success' => false,
+ 'message' => 'Configuration is already up to date. Use force import if you want to reimport.',
+ 'versionInfo' => $versionInfo,
];
}
- // If force import is requested or auto-config not completed, reset auto-configuration flag
- if ($forceImport || !$versionInfo['autoConfigCompleted']) {
+ // If force import is requested or auto-config not completed, reset auto-configuration flag.
+ if ($forceImport === true || $versionInfo['autoConfigCompleted'] === false) {
$this->config->setValueString($this->_appName, 'auto_config_completed', 'false');
- $this->logger->info('SettingsService: Reset auto-configuration flag', [
- 'reason' => $forceImport ? 'force_import' : 'auto_config_not_completed'
- ]);
+ if ($forceImport === true) {
+ $reasonValue = 'force_import';
+ } else {
+ $reasonValue = 'auto_config_not_completed';
+ }
+
+ $this->logger->info(
+ 'SettingsService: Reset auto-configuration flag',
+ [
+ 'reason' => $reasonValue,
+ ]
+ );
}
- // Perform the import
+ // Perform the import.
$this->logger->info('SettingsService: Starting settings import');
- $importResult = $this->loadSettings($forceImport);
- $this->logger->info('SettingsService: Settings import completed', [
- 'import_result' => $importResult
- ]);
+ $importResult = $this->loadSettings(force: $forceImport);
+ $this->logger->info(
+ 'SettingsService: Settings import completed',
+ [
+ 'import_result' => $importResult,
+ ]
+ );
- // Auto-configure after successful import
+ // Auto-configure after successful import.
$autoConfigResult = null;
try {
$this->logger->info('SettingsService: Starting auto-configuration after import');
$autoConfigResult = $this->autoConfigureAfterImport();
- if (!empty($autoConfigResult)) {
+ if (empty($autoConfigResult) === false) {
$this->logger->info('SettingsService: Updating settings with auto-configuration result');
- $this->updateSettings($autoConfigResult);
- $this->logger->info('SettingsService: Auto-configuration completed after import', [
- 'configuration' => array_keys($autoConfigResult)
- ]);
+ $this->updateSettings(data: $autoConfigResult);
+ $this->logger->info(
+ 'SettingsService: Auto-configuration completed after import',
+ [
+ 'configuration' => array_keys($autoConfigResult),
+ ]
+ );
} else {
$this->logger->info('SettingsService: Auto-configuration yielded no results');
}
} catch (\Exception $e) {
- $this->logger->warning('SettingsService: Auto-configuration failed after import', [
- 'exception_message' => $e->getMessage(),
- 'exception' => $e
- ]);
- // Don't fail the entire import if auto-configuration fails
- }
-
- // Wait a moment for any async operations to complete
- usleep(100000); // 0.1 seconds
+ $this->logger->warning(
+ 'SettingsService: Auto-configuration failed after import',
+ [
+ 'exception_message' => $e->getMessage(),
+ 'exception' => $e,
+ ]
+ );
+ // Don't fail the entire import if auto-configuration fails.
+ }//end try
- // Get updated version info - this should now reflect the changes
+ // Wait a moment for any async operations to complete.
+ usleep(100000);
+ // 0.1 seconds.
+ // Get updated version info - this should now reflect the changes.
$this->logger->info('SettingsService: Getting updated version info after import');
$updatedVersionInfo = $this->getVersionInfo();
$this->logger->info('SettingsService: Post-import version info', $updatedVersionInfo);
$message = 'Configuration imported successfully';
- if (!empty($autoConfigResult)) {
+ if (empty($autoConfigResult) === false) {
$message .= ' and auto-configured';
}
- if ($forceImport) {
+
+ if (empty($forceImport) === false) {
$message .= ' (forced import)';
}
return [
- 'success' => true,
- 'message' => $message,
- 'importResult' => $importResult,
- 'autoConfigResult' => $autoConfigResult,
- 'versionInfo' => $updatedVersionInfo,
- 'configurationStatus' => $this->getConfigurationStatus()
+ 'success' => true,
+ 'message' => $message,
+ 'importResult' => $importResult,
+ 'autoConfigResult' => $autoConfigResult,
+ 'versionInfo' => $updatedVersionInfo,
+ 'configurationStatus' => $this->getConfigurationStatus(),
];
-
} catch (\Exception $e) {
- $this->logger->error('SettingsService: Manual import failed', [
- 'exception_message' => $e->getMessage(),
- 'exception' => $e
- ]);
+ $this->logger->error(
+ 'SettingsService: Manual import failed',
+ [
+ 'exception_message' => $e->getMessage(),
+ 'exception' => $e,
+ ]
+ );
return [
- 'success' => false,
- 'message' => 'Import failed: ' . $e->getMessage(),
- 'error' => $e->getMessage(),
- 'versionInfo' => $this->getVersionInfo()
+ 'success' => false,
+ 'message' => 'Import failed: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
+ 'versionInfo' => $this->getVersionInfo(),
];
- }
- }
+ }//end try
+ }//end manualImport()
/**
* Perform consolidated auto-configuration with clean separation of concerns
@@ -2810,86 +3160,94 @@ public function manualImport(bool $forceImport = false): array
* 3. AMEF register configuration
* 4. User groups configuration
*
- * @param bool $force Whether to force configuration loading
+ * @param bool $force Whether to force configuration loading.
+ *
* @return array Consolidated configuration results
*/
- public function performConsolidatedAutoConfiguration(bool $force = false): array
+ public function performConsolidatedAutoConfiguration(bool $force=false): array
{
- $this->logger->info('SettingsService: Starting consolidated auto-configuration', [
- 'force' => $force
- ]);
+ $this->logger->info(
+ 'SettingsService: Starting consolidated auto-configuration',
+ [
+ 'force' => $force,
+ ]
+ );
$results = [
- 'success' => true,
- 'message' => 'Auto-configuration completed successfully',
- 'steps' => [],
- 'errors' => [],
+ 'success' => true,
+ 'message' => 'Auto-configuration completed successfully',
+ 'steps' => [],
+ 'errors' => [],
'timestamp' => time(),
- 'force' => $force
+ 'force' => $force,
];
- // Step 1: Load configuration files
+ // Step 1: Load configuration files.
$this->logger->info('SettingsService: Step 1 - Loading configuration');
- $configResult = $this->loadConfiguration($force);
+ $configResult = $this->loadConfiguration(force: $force);
$results['steps']['configurationLoad'] = $configResult;
- $this->addStepResult($results, $configResult, 'Configuration loading');
+ $this->addStepResult(results: $results, stepResult: $configResult, stepName: 'Configuration loading');
- // Step 2: Configure Voorzieningen (Dutch register system)
+ // Step 2: Configure Voorzieningen (Dutch register system).
$this->logger->info('SettingsService: Step 2 - Configuring Voorzieningen');
$voorzieningenResult = $this->configureVoorzieningen();
$results['steps']['voorzieningenConfiguration'] = $voorzieningenResult;
- $this->addStepResult($results, $voorzieningenResult, 'Voorzieningen configuration');
+ $this->addStepResult(results: $results, stepResult: $voorzieningenResult, stepName: 'Voorzieningen configuration');
- // Step 3: Configure AMEF (ArchiMate/English register system)
+ // Step 3: Configure AMEF (ArchiMate/English register system).
$this->logger->info('SettingsService: Step 3 - Configuring AMEF');
$amefResult = $this->configureAmef();
$results['steps']['amefConfiguration'] = $amefResult;
- $this->addStepResult($results, $amefResult, 'AMEF configuration');
+ $this->addStepResult(results: $results, stepResult: $amefResult, stepName: 'AMEF configuration');
- // Step 4: Configure User Groups
+ // Step 4: Configure User Groups.
$this->logger->info('SettingsService: Step 4 - Configuring User Groups');
$groupsResult = $this->configureGroups();
$results['steps']['groupsConfiguration'] = $groupsResult;
- $this->addStepResult($results, $groupsResult, 'User groups configuration');
+ $this->addStepResult(results: $results, stepResult: $groupsResult, stepName: 'User groups configuration');
- // Determine overall success
- $results['success'] = empty($results['errors']);
- if (!$results['success']) {
+ // Determine overall success.
+ $results['success'] = empty($results['errors']) === true;
+ if ($results['success'] === false) {
$results['message'] = 'Auto-configuration completed with some issues';
}
- $this->logger->info('SettingsService: Consolidated auto-configuration completed', [
- 'success' => $results['success'],
- 'errors_count' => count($results['errors'])
- ]);
+ $this->logger->info(
+ 'SettingsService: Consolidated auto-configuration completed',
+ [
+ 'success' => $results['success'],
+ 'errors_count' => count($results['errors']),
+ ]
+ );
return $results;
- }
+ }//end performConsolidatedAutoConfiguration()
/**
* Load configuration files
*
- * @param bool $force Whether to force reload regardless of version
+ * @param bool $force Whether to force reload regardless of version.
+ *
* @return array Configuration loading result
*/
private function loadConfiguration(bool $force): array
{
try {
- $importResult = $this->manualImport($force);
+ $importResult = $this->manualImport(forceImport: $force);
return [
'success' => $importResult['success'],
'message' => $importResult['message'] ?? 'Configuration loaded',
- 'details' => $importResult
+ 'details' => $importResult,
];
} catch (\Exception $e) {
return [
'success' => false,
- 'message' => 'Configuration loading failed: ' . $e->getMessage(),
- 'error' => $e->getMessage()
+ 'message' => 'Configuration loading failed: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
];
}
- }
+ }//end loadConfiguration()
/**
* Configure Voorzieningen register and schemas
@@ -2909,27 +3267,30 @@ private function configureVoorzieningen(): array
try {
$registerService = $this->getRegisterService();
- $registers = $registerService->findAll();
+ $registers = $registerService->findAll();
} catch (\TypeError | \Exception $e) {
- $this->logger->warning('OpenRegister RegisterService->findAll() failed in configureVoorzieningen', [
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine()
- ]);
+ $this->logger->warning(
+ 'OpenRegister RegisterService->findAll() failed in configureVoorzieningen',
+ [
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]
+ );
return [
'success' => false,
- 'message' => 'Failed to retrieve registers: ' . $e->getMessage(),
+ 'message' => 'Failed to retrieve registers: '.$e->getMessage(),
];
}
- if (empty($registers)) {
+ if (empty($registers) === true) {
return [
'success' => false,
'message' => 'No registers available',
];
}
- // Get schema mapper to fetch schema details if needed
+ // Get schema mapper to fetch schema details if needed.
$schemaMapper = null;
try {
$schemaMapper = $this->container->get(\OCA\OpenRegister\Db\SchemaMapper::class);
@@ -2937,31 +3298,44 @@ private function configureVoorzieningen(): array
$this->logger->warning('SchemaMapper not available for Voorzieningen detection', ['error' => $e->getMessage()]);
}
- // Find the voorzieningen register by slug OR by presence of expected schema slugs
+ // Find the voorzieningen register by slug OR by presence of expected schema slugs.
$targetRegister = null;
- $expectedSlugs = [
- 'sector', 'suite', 'dienst', 'kwetsbaarheid', 'contactpersoon', 'organisatie',
- 'gebruik', 'contract', 'koppeling', 'beoordeeling', 'module', 'compliancy', 'moduleversie', 'moduleVersie'
+ $expectedSlugs = [
+ 'sector',
+ 'suite',
+ 'dienst',
+ 'kwetsbaarheid',
+ 'contactpersoon',
+ 'organisatie',
+ 'gebruik',
+ 'contract',
+ 'koppeling',
+ 'beoordeeling',
+ 'module',
+ 'compliancy',
+ 'moduleversie',
+ 'moduleVersie',
];
foreach ($registers as $register) {
- // Convert Register entity to array if needed
+ // Convert Register entity to array if needed.
if ($register instanceof \OCA\OpenRegister\Db\Register) {
$register = $register->jsonSerialize();
}
+
$slug = strtolower($register['slug'] ?? '');
if ($slug === 'voorzieningen') {
- // Fetch full schema details for the register
- $schemas = $register['schemas'] ?? [];
+ // Fetch full schema details for the register.
+ $schemas = $register['schemas'] ?? [];
$schemaDetails = [];
foreach ($schemas as $schema) {
- if (is_array($schema) && isset($schema['slug'])) {
- // Schema is already a full object
+ if (is_array($schema) === true && isset($schema['slug']) === true) {
+ // Schema is already a full object.
$schemaDetails[] = $schema;
- } elseif ((is_int($schema) || is_numeric($schema)) && $schemaMapper !== null) {
- // Schema is an ID - fetch details using SchemaMapper
+ } else if ((is_int($schema) === true || is_numeric($schema) === true) && $schemaMapper !== null) {
+ // Schema is an ID - fetch details using SchemaMapper.
try {
- $schemaEntity = $schemaMapper->find((int)$schema);
+ $schemaEntity = $schemaMapper->find((int) $schema);
if ($schemaEntity !== null) {
$schemaDetails[] = $schemaEntity->jsonSerialize();
}
@@ -2970,50 +3344,55 @@ private function configureVoorzieningen(): array
}
}
}
+
$register['schemas'] = $schemaDetails;
- $targetRegister = $register;
+ $targetRegister = $register;
break;
- }
- // Heuristic: count matching schemas
+ }//end if
+
+ // Heuristic: count matching schemas.
$schemaSlugs = [];
foreach (($register['schemas'] ?? []) as $schema) {
- if (is_array($schema) && isset($schema['slug'])) {
+ if (is_array($schema) === true && isset($schema['slug']) === true) {
$schemaSlugs[] = strtolower($schema['slug']);
- } elseif ((is_int($schema) || is_numeric($schema)) && $schemaMapper !== null) {
+ } else if ((is_int($schema) === true || is_numeric($schema) === true) && $schemaMapper !== null) {
try {
- $schemaEntity = $schemaMapper->find((int)$schema);
+ $schemaEntity = $schemaMapper->find((int) $schema);
if ($schemaEntity !== null) {
- $schemaArray = $schemaEntity->jsonSerialize();
+ $schemaArray = $schemaEntity->jsonSerialize();
$schemaSlugs[] = strtolower($schemaArray['slug'] ?? '');
}
} catch (\Exception $e) {
- // Skip schemas that can't be fetched
+ // Skip schemas that can't be fetched.
}
}
}
+
$matches = array_intersect($expectedSlugs, $schemaSlugs);
- if (count($matches) >= 6) { // good confidence
- // Fetch full schema details
- $schemas = $register['schemas'] ?? [];
+ if (count($matches) >= 6) {
+ // Good confidence.
+ // Fetch full schema details.
+ $schemas = $register['schemas'] ?? [];
$schemaDetails = [];
foreach ($schemas as $schema) {
- if (is_array($schema) && isset($schema['slug'])) {
+ if (is_array($schema) === true && isset($schema['slug']) === true) {
$schemaDetails[] = $schema;
- } elseif ((is_int($schema) || is_numeric($schema)) && $schemaMapper !== null) {
+ } else if ((is_int($schema) === true || is_numeric($schema) === true) && $schemaMapper !== null) {
try {
- $schemaEntity = $schemaMapper->find((int)$schema);
+ $schemaEntity = $schemaMapper->find((int) $schema);
if ($schemaEntity !== null) {
$schemaDetails[] = $schemaEntity->jsonSerialize();
}
} catch (\Exception $e) {
- // Skip
+ // Skip.
}
}
}
+
$register['schemas'] = $schemaDetails;
- $targetRegister = $register;
- }
- }
+ $targetRegister = $register;
+ }//end if
+ }//end foreach
if ($targetRegister === null) {
return [
@@ -3022,89 +3401,114 @@ private function configureVoorzieningen(): array
];
}
- // Map schema slugs to configuration keys based on actual register schemas
+ // Map schema slugs to configuration keys based on actual register schemas.
$slugToKey = [
- 'organisatie' => 'organisatie_schema',
+ 'organisatie' => 'organisatie_schema',
'contactpersoon' => 'contactpersoon_schema',
- 'suite' => 'suite_schema',
- 'dienst' => 'dienst_schema',
- 'kwetsbaarheid' => 'kwetsbaarheid_schema',
- 'gebruik' => 'gebruik_schema',
- 'contract' => 'contract_schema',
- 'koppeling' => 'koppeling_schema',
- 'beoordeeling' => 'beoordeeling_schema',
- 'module' => 'module_schema',
- 'compliancy' => 'compliancy_schema',
- 'moduleversie' => 'moduleVersie_schema', // Handle both moduleversie and moduleVersie
- 'moduleVersie' => 'moduleVersie_schema',
- 'sector' => 'sector_schema',
+ 'suite' => 'suite_schema',
+ 'dienst' => 'dienst_schema',
+ 'kwetsbaarheid' => 'kwetsbaarheid_schema',
+ 'gebruik' => 'gebruik_schema',
+ 'contract' => 'contract_schema',
+ 'koppeling' => 'koppeling_schema',
+ 'beoordeeling' => 'beoordeeling_schema',
+ 'module' => 'module_schema',
+ 'compliancy' => 'compliancy_schema',
+ 'moduleversie' => 'moduleVersie_schema',
+ // Handle both moduleversie and moduleVersie.
+ 'moduleVersie' => 'moduleVersie_schema',
+ 'sector' => 'sector_schema',
];
- $config = [ 'register' => (string)($targetRegister['id'] ?? '') ];
+ $config = [ 'register' => (string) ($targetRegister['id'] ?? '') ];
- $this->logger->info('DEBUG: About to process schemas', [
- 'register_id' => $targetRegister['id'],
- 'schemas_count' => count($targetRegister['schemas'] ?? []),
- 'slugToKey_map' => $slugToKey
- ]);
+ $this->logger->info(
+ 'DEBUG: About to process schemas',
+ [
+ 'register_id' => $targetRegister['id'],
+ 'schemas_count' => count($targetRegister['schemas'] ?? []),
+ 'slugToKey_map' => $slugToKey,
+ ]
+ );
foreach (($targetRegister['schemas'] ?? []) as $schema) {
- $originalSlug = $schema['slug'] ?? '';
+ $originalSlug = $schema['slug'] ?? '';
$lowercaseSlug = strtolower($originalSlug);
- $this->logger->info('DEBUG: Processing schema', [
- 'original_slug' => $originalSlug,
- 'lowercase_slug' => $lowercaseSlug,
- 'schema_id' => $schema['id'] ?? 'NO_ID',
- 'has_mapping_original' => isset($slugToKey[$originalSlug]) ? 'YES' : 'NO',
- 'has_mapping_lowercase' => isset($slugToKey[$lowercaseSlug]) ? 'YES' : 'NO'
- ]);
+ if (isset($slugToKey[$originalSlug]) === true) {
+ $hasMappingOriginalValue = 'YES';
+ } else {
+ $hasMappingOriginalValue = 'NO';
+ }
+
+ if (isset($slugToKey[$lowercaseSlug]) === true) {
+ $hasMappingLowercaseValue = 'YES';
+ } else {
+ $hasMappingLowercaseValue = 'NO';
+ }
+
+ $this->logger->info(
+ 'DEBUG: Processing schema',
+ [
+ 'original_slug' => $originalSlug,
+ 'lowercase_slug' => $lowercaseSlug,
+ 'schema_id' => $schema['id'] ?? 'NO_ID',
+ 'has_mapping_original' => $hasMappingOriginalValue,
+ 'has_mapping_lowercase' => $hasMappingLowercaseValue,
+ ]
+ );
$mappingKey = null;
- $usedSlug = null;
+ $usedSlug = null;
- // Try original case first, then lowercase
- if (isset($slugToKey[$originalSlug])) {
+ // Try original case first, then lowercase.
+ if (isset($slugToKey[$originalSlug]) === true) {
$mappingKey = $slugToKey[$originalSlug];
- $usedSlug = $originalSlug;
- } elseif (isset($slugToKey[$lowercaseSlug])) {
+ $usedSlug = $originalSlug;
+ } else if (isset($slugToKey[$lowercaseSlug]) === true) {
$mappingKey = $slugToKey[$lowercaseSlug];
- $usedSlug = $lowercaseSlug;
+ $usedSlug = $lowercaseSlug;
}
if ($mappingKey !== null) {
- $config[$mappingKey] = (string)$schema['id'];
- $this->logger->info('DEBUG: Mapped schema successfully', [
- 'used_slug' => $usedSlug,
- 'config_key' => $mappingKey,
- 'schema_id' => $schema['id']
- ]);
+ $config[$mappingKey] = (string) $schema['id'];
+ $this->logger->info(
+ 'DEBUG: Mapped schema successfully',
+ [
+ 'used_slug' => $usedSlug,
+ 'config_key' => $mappingKey,
+ 'schema_id' => $schema['id'],
+ ]
+ );
} else {
- $this->logger->debug('DEBUG: No mapping found for schema slug', [
- 'original_slug' => $originalSlug,
- 'lowercase_slug' => $lowercaseSlug
- ]);
+ $this->logger->debug(
+ 'DEBUG: No mapping found for schema slug',
+ [
+ 'original_slug' => $originalSlug,
+ 'lowercase_slug' => $lowercaseSlug,
+ ]
+ );
}
- }
+ }//end foreach
$this->logger->info('DEBUG: Final config before persist', ['config' => $config]);
- // Persist normalized config
- $this->setVoorzieningenConfig($config);
+ // Persist normalized config.
+ $this->setVoorzieningenConfig(config: $config);
return [
- 'success' => true,
- 'message' => 'Voorzieningen configured successfully',
+ 'success' => true,
+ 'message' => 'Voorzieningen configured successfully',
'configured' => $config,
];
} catch (\Exception $e) {
return [
'success' => false,
- 'message' => 'Voorzieningen configuration failed: ' . $e->getMessage(),
- 'error' => $e->getMessage(),
+ 'message' => 'Voorzieningen configuration failed: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
];
- }
- }
+ }//end try
+ }//end configureVoorzieningen()
/**
* Configure AMEF register and schemas
@@ -3114,7 +3518,7 @@ private function configureVoorzieningen(): array
private function configureAmef(): array
{
try {
- // Get available registers
+ // Get available registers.
$objectService = $this->getObjectService();
if ($objectService === null) {
return [
@@ -3125,53 +3529,61 @@ private function configureAmef(): array
try {
$registerService = $this->getRegisterService();
- $registers = $registerService->findAll();
+ $registers = $registerService->findAll();
} catch (\TypeError | \Exception $e) {
- $this->logger->warning('OpenRegister RegisterService->findAll() failed in configureAmef', [
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine()
- ]);
+ $this->logger->warning(
+ 'OpenRegister RegisterService->findAll() failed in configureAmef',
+ [
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]
+ );
return [
'success' => false,
- 'message' => 'Failed to retrieve registers: ' . $e->getMessage(),
+ 'message' => 'Failed to retrieve registers: '.$e->getMessage(),
];
}
- if (empty($registers)) {
+ if (empty($registers) === true) {
return [
'success' => false,
'message' => 'No registers available',
];
}
- // Detect AMEF register by presence of core AMEF schemas (not by slug)
- $candidate = null;
+ // Detect AMEF register by presence of core AMEF schemas (not by slug).
+ $candidate = null;
$amefCoreSlugs = ['model', 'element', 'relation', 'view', 'organization', 'property', 'property-definition'];
- // Convert all registers to arrays first
- $registers = array_map(function($register) {
- return ($register instanceof \OCA\OpenRegister\Db\Register)
- ? $register->jsonSerialize()
- : (array)$register;
- }, $registers);
+ // Convert all registers to arrays first.
+ $registers = array_map(
+ function ($register) {
+ if (($register instanceof \OCA\OpenRegister\Db\Register)) {
+ return $register->jsonSerialize();
+ } else {
+ return (array) $register;
+ }
+ },
+ $registers
+ );
- // Collect all schema IDs for batch fetch
+ // Collect all schema IDs for batch fetch.
$allSchemaIds = [];
foreach ($registers as $register) {
foreach (($register['schemas'] ?? []) as $schema) {
- if (is_int($schema) || is_numeric($schema)) {
- $allSchemaIds[] = (int)$schema;
+ if (is_int($schema) === true || is_numeric($schema) === true) {
+ $allSchemaIds[] = (int) $schema;
}
}
}
- // Batch fetch all schemas in one query
+ // Batch fetch all schemas in one query.
$schemaMap = [];
- if (!empty($allSchemaIds)) {
+ if (empty($allSchemaIds) === false) {
try {
$schemaMapper = $this->container->get(\OCA\OpenRegister\Db\SchemaMapper::class);
- $schemas = $schemaMapper->findMultipleOptimized(array_unique($allSchemaIds));
+ $schemas = $schemaMapper->findMultipleOptimized(array_unique($allSchemaIds));
foreach ($schemas as $schema) {
$schemaMap[$schema->getId()] = $schema->jsonSerialize();
}
@@ -3181,40 +3593,46 @@ private function configureAmef(): array
}
foreach ($registers as $register) {
- // Handle schemas - they might be IDs (integers) or full objects
- $schemas = $register['schemas'] ?? [];
- $schemaSlugs = [];
+ // Handle schemas - they might be IDs (integers) or full objects.
+ $schemas = $register['schemas'] ?? [];
+ $schemaSlugs = [];
$schemaDetails = [];
foreach ($schemas as $schema) {
- if (is_array($schema) && isset($schema['slug'])) {
- // Schema is already a full object
- $schemaSlugs[] = strtolower($schema['slug']);
+ if (is_array($schema) === true && isset($schema['slug']) === true) {
+ // Schema is already a full object.
+ $schemaSlugs[] = strtolower($schema['slug']);
$schemaDetails[] = $schema;
- } elseif (is_int($schema) || is_numeric($schema)) {
- // Schema is an ID - get from pre-fetched map
- if (isset($schemaMap[(int)$schema])) {
- $schemaArray = $schemaMap[(int)$schema];
- $schemaSlugs[] = strtolower($schemaArray['slug'] ?? '');
+ } else if (is_int($schema) === true || is_numeric($schema) === true) {
+ // Schema is an ID - get from pre-fetched map.
+ if (isset($schemaMap[(int) $schema]) === true) {
+ $schemaArray = $schemaMap[(int) $schema];
+ $schemaSlugs[] = strtolower($schemaArray['slug'] ?? '');
$schemaDetails[] = $schemaArray;
}
}
}
- // Store schema details back for later use
+ // Store schema details back for later use.
$register['schemas'] = $schemaDetails;
$matches = array_intersect($amefCoreSlugs, $schemaSlugs);
- if (count($matches) >= 3) { // threshold: at least model + 2 others
+ if (count($matches) >= 3) {
+ // Threshold: at least model + 2 others.
$candidate = $register;
- // prefer the register with most matches
- if (!isset($bestCount) || count($matches) > $bestCount) {
- $best = $register;
+ // Prefer the register with most matches.
+ if (isset($bestCount) === false || count($matches) > $bestCount) {
+ $best = $register;
$bestCount = count($matches);
}
}
+ }//end foreach
+
+ if (isset($best) === true) {
+ $targetRegister = $best;
+ } else {
+ $targetRegister = $candidate;
}
- $targetRegister = isset($best) ? $best : $candidate;
if ($targetRegister === null) {
return [
@@ -3224,43 +3642,48 @@ private function configureAmef(): array
}
$config = [
- 'register' => (string)($targetRegister['id'] ?? ''),
- // Initialize all known keys with empty strings to provide a stable shape
- 'organization_schema' => '',
- 'element_schema' => '',
- 'relation_schema' => '',
- 'view_schema' => '',
- 'model_schema' => '',
+ 'register' => (string) ($targetRegister['id'] ?? ''),
+ // Initialize all known keys with empty strings to provide a stable shape.
+ 'organization_schema' => '',
+ 'element_schema' => '',
+ 'relation_schema' => '',
+ 'view_schema' => '',
+ 'model_schema' => '',
'property_definition_schema' => '',
];
foreach (($targetRegister['schemas'] ?? []) as $schema) {
- $slug = strtolower($schema['slug'] ?? '');
+ $slug = strtolower($schema['slug'] ?? '');
$allowed = ['organization','element','relation','view','model','property-definition'];
- if (in_array($slug, $allowed, true)) {
- // Handle property-definition schema with underscore in config key
- $configKey = $slug === 'property-definition' ? 'property_definition_schema' : $slug . '_schema';
- $config[$configKey] = (string)$schema['id'];
+ if (in_array($slug, $allowed, true) === true) {
+ // Handle property-definition schema with underscore in config key.
+ if ($slug === 'property-definition') {
+ $configKey = 'property_definition_schema';
+ } else {
+ $configKey = $slug.'_schema';
+ }
+
+ $config[$configKey] = (string) $schema['id'];
}
}
- // Persist consolidated AMEF config JSON
- $this->setAmefConfig($config);
+ // Persist consolidated AMEF config JSON.
+ $this->setAmefConfig(config: $config);
return [
- 'success' => true,
- 'message' => 'AMEF configuration completed successfully',
+ 'success' => true,
+ 'message' => 'AMEF configuration completed successfully',
'configured' => $config,
- 'errors' => [],
+ 'errors' => [],
];
} catch (\Exception $e) {
return [
'success' => false,
- 'message' => 'AMEF configuration failed: ' . $e->getMessage(),
- 'error' => $e->getMessage(),
+ 'message' => 'AMEF configuration failed: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
];
- }
- }
+ }//end try
+ }//end configureAmef()
/**
* Configure required user groups
@@ -3270,44 +3693,48 @@ private function configureAmef(): array
private function configureGroups(): array
{
try {
- // Call the method to create required user groups
+ // Call the method to create required user groups.
$result = $this->createAndConfigureUserGroups();
return [
- 'success' => $result['success'],
- 'message' => $result['message'],
- 'created' => $result['created'] ?? [],
+ 'success' => $result['success'],
+ 'message' => $result['message'],
+ 'created' => $result['created'] ?? [],
'existing' => $result['existing'] ?? [],
- 'total' => $result['total'] ?? 0
+ 'total' => $result['total'] ?? 0,
];
} catch (\Exception $e) {
return [
'success' => false,
- 'message' => 'User groups configuration failed: ' . $e->getMessage(),
- 'error' => $e->getMessage()
+ 'message' => 'User groups configuration failed: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
];
}
- }
+ }//end configureGroups()
/**
* Add step result to overall results and handle errors
*
- * @param array &$results The results array (passed by reference)
- * @param array $stepResult The result of a configuration step
- * @param string $stepName The name of the step for error reporting
+ * @param array $results The results array (passed by reference).
+ * @param array $stepResult The result of a configuration step.
+ * @param string $stepName The name of the step for error reporting.
+ *
* @return void
*/
private function addStepResult(array &$results, array $stepResult, string $stepName): void
{
- if (!$stepResult['success']) {
- $results['errors'][] = $stepName . ' failed: ' . ($stepResult['message'] ?? 'Unknown error');
- $this->logger->warning("SettingsService: {$stepName} failed", [
- 'error' => $stepResult['message'] ?? 'Unknown error'
- ]);
+ if ($stepResult['success'] === false) {
+ $results['errors'][] = $stepName.' failed: '.($stepResult['message'] ?? 'Unknown error');
+ $this->logger->warning(
+ "SettingsService: {$stepName} failed",
+ [
+ 'error' => $stepResult['message'] ?? 'Unknown error',
+ ]
+ );
} else {
$this->logger->info("SettingsService: {$stepName} successful");
}
- }
+ }//end addStepResult()
/**
* Get consolidated configuration as JSON objects
@@ -3316,31 +3743,31 @@ private function addStepResult(array &$results, array $stepResult, string $stepN
*/
public function getConsolidatedConfiguration(): array
{
- // Get email config and include templates
+ // Get email config and include templates.
$emailConfig = $this->getEmailConfig();
$emailConfig['templates'] = [
- 'organization_registration' => $this->getEmailTemplate('organization_registration'),
- 'organization_activation' => $this->getEmailTemplate('organization_activation'),
- 'user_creation' => $this->getEmailTemplate('user_creation'),
- 'user_password' => $this->getEmailTemplate('user_password'),
+ 'organization_registration' => $this->getEmailTemplate(templateName: 'organization_registration'),
+ 'organization_activation' => $this->getEmailTemplate(templateName: 'organization_activation'),
+ 'user_creation' => $this->getEmailTemplate(templateName: 'user_creation'),
+ 'user_password' => $this->getEmailTemplate(templateName: 'user_password'),
];
- // Get Voorzieningen and AMEF configs (without object counts for performance)
+ // Get Voorzieningen and AMEF configs (without object counts for performance).
$voorzieningenConfig = $this->getVoorzieningenConfig();
- $amefConfig = $this->getAmefConfig();
+ $amefConfig = $this->getAmefConfig();
return [
'voorzieningen' => $voorzieningenConfig,
- 'amef' => $amefConfig,
- 'email' => $emailConfig,
- 'archimate' => $this->getArchiMateStatus(),
- 'userGroups' => [
- 'generic' => $this->getGenericUserGroups(),
+ 'amef' => $amefConfig,
+ 'email' => $emailConfig,
+ 'archimate' => $this->getArchiMateStatus(),
+ 'userGroups' => [
+ 'generic' => $this->getGenericUserGroups(),
'organizationAdmin' => $this->getOrganizationAdminGroups(),
- 'superUser' => $this->getSuperUserGroups()
- ]
+ 'superUser' => $this->getSuperUserGroups(),
+ ],
];
- }
+ }//end getConsolidatedConfiguration()
/**
* Get Voorzieningen configuration as JSON object
@@ -3349,39 +3776,40 @@ public function getConsolidatedConfiguration(): array
*/
public function getVoorzieningenConfig(): array
{
- $config = $this->config->getValueString($this->_appName, 'voorzieningen_config', '{}');
+ $config = $this->config->getValueString($this->_appName, 'voorzieningen_config', '{}');
$decoded = json_decode($config, true);
- // Backward compatibility: build minimal structure from legacy scalar keys
- if (!is_array($decoded)) {
+ // Backward compatibility: build minimal structure from legacy scalar keys.
+ if (is_array($decoded) === false) {
$decoded = [
- 'register' => $this->config->getValueString($this->_appName, 'voorzieningen_register', ''),
- 'organisatie_schema' => $this->config->getValueString($this->_appName, 'voorzieningen_organisatie_schema', ''),
+ 'register' => $this->config->getValueString($this->_appName, 'voorzieningen_register', ''),
+ 'organisatie_schema' => $this->config->getValueString($this->_appName, 'voorzieningen_organisatie_schema', ''),
'contactpersoon_schema' => $this->config->getValueString($this->_appName, 'voorzieningen_contactpersoon_schema', ''),
];
}
- // Normalize to the new, clean structure: no *_source or *_register keys,
- // include all known schema keys, and accept legacy 'voorzieningen_*_schema' fallbacks
- return $this->normalizeVoorzieningenConfig($decoded);
- }
+ // Normalize to the new, clean structure: no *_source or *_register keys,.
+ // include all known schema keys, and accept legacy 'voorzieningen_*_schema' fallbacks.
+ return $this->normalizeVoorzieningenConfig(input: $decoded);
+ }//end getVoorzieningenConfig()
/**
* Set Voorzieningen configuration as JSON object
*
- * @param array $config The voorzieningen configuration
+ * @param array $config The voorzieningen configuration.
+ *
* @return void
*/
public function setVoorzieningenConfig(array $config): void
{
- // Clear cache since voorzieningen config affects schema and register IDs
+ // Clear cache since voorzieningen config affects schema and register IDs.
$this->clearConfigurationCache();
- // Persist only normalized structure
- $normalized = $this->normalizeVoorzieningenConfig($config);
+ // Persist only normalized structure.
+ $normalized = $this->normalizeVoorzieningenConfig(input: $config);
$jsonConfig = json_encode($normalized, JSON_PRETTY_PRINT);
$this->config->setValueString($this->_appName, 'voorzieningen_config', $jsonConfig);
- }
+ }//end setVoorzieningenConfig()
/**
* Normalize voorzieningen configuration to the new, clean format.
@@ -3389,17 +3817,22 @@ public function setVoorzieningenConfig(array $config): void
* - Drop any '*_source' and '*_register' keys
* - Ensure all known schema keys are present (null if missing)
*
- * @param array $input Raw/legacy configuration
- * @return array Normalized configuration
+ * @param array $input Raw/legacy configuration.
+ *
+ * @return array Normalized configuration.
*/
private function normalizeVoorzieningenConfig(array $input): array
{
$normalized = [];
- // Register id
- $normalized['register'] = isset($input['register']) ? (string)$input['register'] : '';
+ // Register id.
+ if (isset($input['register']) === true) {
+ $normalized['register'] = (string) $input['register'];
+ } else {
+ $normalized['register'] = '';
+ }
- // Known schema keys to support - updated to match actual schemas from register
+ // Known schema keys to support - updated to match actual schemas from register.
$schemaKeys = [
'organisatie_schema',
'contactpersoon_schema',
@@ -3416,18 +3849,22 @@ private function normalizeVoorzieningenConfig(array $input): array
'sector_schema',
];
- // Copy any present schema keys; ignore sources/registers
+ // Copy any present schema keys; ignore sources/registers.
foreach ($schemaKeys as $key) {
- if (array_key_exists($key, $input)) {
- $normalized[$key] = $input[$key] === null ? '' : (string)$input[$key];
+ if (array_key_exists($key, $input) === true) {
+ if ($input[$key] === null) {
+ $normalized[$key] = '';
+ } else {
+ $normalized[$key] = (string) $input[$key];
+ }
} else {
- // Accept legacy keys that might be nested under 'voorzieningen_*_schema'
+ // Accept legacy keys that might be nested under 'voorzieningen_*_schema'.
$normalized[$key] = '';
}
}
return $normalized;
- }
+ }//end normalizeVoorzieningenConfig()
/**
* Gets AMEF configuration using ArchiMateService to avoid code duplication
@@ -3440,46 +3877,49 @@ private function normalizeVoorzieningenConfig(array $input): array
public function getAmefConfig(): array
{
try {
- // Get ArchiMateService from container to avoid circular dependency
+ // Get ArchiMateService from container to avoid circular dependency.
$archiMateService = $this->container->get(\OCA\SoftwareCatalog\Service\ArchiMateService::class);
- // Use reflection to access the private getAmefConfig method
+ // Use reflection to access the private getAmefConfig method.
$reflection = new \ReflectionClass($archiMateService);
- $method = $reflection->getMethod('getAmefConfig');
+ $method = $reflection->getMethod('getAmefConfig');
$method->setAccessible(true);
return $method->invoke($archiMateService);
-
} catch (\Exception $e) {
- $this->logger->error('SettingsService: Failed to get AMEF config from ArchiMateService', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'SettingsService: Failed to get AMEF config from ArchiMateService',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
- // Fallback to direct config access if ArchiMateService is not available
- $config = $this->config->getValueString($this->_appName, 'amef_config', '{}');
+ // Fallback to direct config access if ArchiMateService is not available.
+ $config = $this->config->getValueString($this->_appName, 'amef_config', '{}');
$decoded = json_decode($config, true);
- if (!is_array($decoded)) {
- // Fallback to individual config values for backward compatibility
+ if (is_array($decoded) === false) {
+ // Fallback to individual config values for backward compatibility.
$decoded = [
- 'register_id' => $this->config->getValueString($this->_appName, 'amef_register_id', ''),
+ 'register_id' => $this->config->getValueString($this->_appName, 'amef_register_id', ''),
'organizations_schema' => $this->config->getValueString($this->_appName, 'amef_organizations_schema', ''),
- 'elements_schema' => $this->config->getValueString($this->_appName, 'amef_elements_schema', ''),
+ 'elements_schema' => $this->config->getValueString($this->_appName, 'amef_elements_schema', ''),
'relationships_schema' => $this->config->getValueString($this->_appName, 'amef_relationships_schema', ''),
- 'views_schema' => $this->config->getValueString($this->_appName, 'amef_views_schema', ''),
- 'models_schema' => $this->config->getValueString($this->_appName, 'amef_models_schema', '')
+ 'views_schema' => $this->config->getValueString($this->_appName, 'amef_views_schema', ''),
+ 'models_schema' => $this->config->getValueString($this->_appName, 'amef_models_schema', ''),
];
}
return $decoded;
- }
- }
+ }//end try
+ }//end getAmefConfig()
/**
* Set AMEF configuration as JSON object
*
- * @param array $config The AMEF configuration
+ * @param array $config The AMEF configuration.
+ *
* @return void
*/
public function setAmefConfig(array $config): void
@@ -3487,14 +3927,17 @@ public function setAmefConfig(array $config): void
$jsonConfig = json_encode($config, JSON_PRETTY_PRINT);
$this->config->setValueString($this->_appName, 'amef_config', $jsonConfig);
- // Clear configuration cache when AMEF config is updated
+ // Clear configuration cache when AMEF config is updated.
$this->clearConfigurationCache();
- $this->logger->debug('SettingsService: AMEF configuration updated and cache cleared', [
- 'config_keys' => array_keys($config),
- 'cache_cleared' => true
- ]);
- }
+ $this->logger->debug(
+ 'SettingsService: AMEF configuration updated and cache cleared',
+ [
+ 'config_keys' => array_keys($config),
+ 'cache_cleared' => true,
+ ]
+ );
+ }//end setAmefConfig()
/**
* Get Email configuration as JSON object
@@ -3503,43 +3946,44 @@ public function setAmefConfig(array $config): void
*/
public function getEmailConfig(): array
{
- $config = $this->config->getValueString($this->_appName, 'email_config', '{}');
+ $config = $this->config->getValueString($this->_appName, 'email_config', '{}');
$decoded = json_decode($config, true);
- if (!is_array($decoded)) {
- // Fallback to individual config values for backward compatibility
+ if (is_array($decoded) === false) {
+ // Fallback to individual config values for backward compatibility.
$decoded = [
- 'enabled' => $this->config->getValueString($this->_appName, 'email_enabled', 'false') === 'true',
- 'transport_type' => $this->config->getValueString($this->_appName, 'email_transport_type', 'smtp'),
- 'smtp_host' => $this->config->getValueString($this->_appName, 'email_smtp_host', ''),
- 'smtp_port' => $this->config->getValueString($this->_appName, 'email_smtp_port', '587'),
- 'smtp_username' => $this->config->getValueString($this->_appName, 'email_smtp_username', ''),
- 'smtp_password' => $this->config->getValueString($this->_appName, 'email_smtp_password', ''),
- 'smtp_encryption' => $this->config->getValueString($this->_appName, 'email_smtp_encryption', 'tls'),
- 'sender_email' => $this->config->getValueString($this->_appName, 'sender_email', ''),
- 'sender_name' => $this->config->getValueString($this->_appName, 'sender_name', ''),
- 'mailjet_api_key' => $this->config->getValueString($this->_appName, 'email_mailjet_api_key', ''),
- 'mailjet_secret_key' => $this->config->getValueString($this->_appName, 'email_mailjet_secret_key', '')
+ 'enabled' => $this->config->getValueString($this->_appName, 'email_enabled', 'false') === 'true',
+ 'transport_type' => $this->config->getValueString($this->_appName, 'email_transport_type', 'smtp'),
+ 'smtp_host' => $this->config->getValueString($this->_appName, 'email_smtp_host', ''),
+ 'smtp_port' => $this->config->getValueString($this->_appName, 'email_smtp_port', '587'),
+ 'smtp_username' => $this->config->getValueString($this->_appName, 'email_smtp_username', ''),
+ 'smtp_password' => $this->config->getValueString($this->_appName, 'email_smtp_password', ''),
+ 'smtp_encryption' => $this->config->getValueString($this->_appName, 'email_smtp_encryption', 'tls'),
+ 'sender_email' => $this->config->getValueString($this->_appName, 'sender_email', ''),
+ 'sender_name' => $this->config->getValueString($this->_appName, 'sender_name', ''),
+ 'mailjet_api_key' => $this->config->getValueString($this->_appName, 'email_mailjet_api_key', ''),
+ 'mailjet_secret_key' => $this->config->getValueString($this->_appName, 'email_mailjet_secret_key', ''),
];
}
return $decoded;
- }
+ }//end getEmailConfig()
/**
* Set Email configuration as JSON object
*
- * @param array $config The email configuration
+ * @param array $config The email configuration.
+ *
* @return void
*/
public function setEmailConfig(array $config): void
{
- // Email config doesn't typically affect schema/register IDs, but clear cache for consistency
+ // Email config doesn't typically affect schema/register IDs, but clear cache for consistency.
$this->clearConfigurationCache();
$jsonConfig = json_encode($config, JSON_PRETTY_PRINT);
$this->config->setValueString($this->_appName, 'email_config', $jsonConfig);
- }
+ }//end setEmailConfig()
/**
* Get ArchiMate import/export status and AMEF object counts
@@ -3552,38 +3996,52 @@ public function setEmailConfig(array $config): void
public function getArchiMateStatus(): array
{
try {
- // Get ArchiMateService from container to avoid circular dependency
+ // Get ArchiMateService from container to avoid circular dependency.
$archiMateService = $this->container->get(\OCA\SoftwareCatalog\Service\ArchiMateService::class);
return $archiMateService->getArchiMateStatus();
-
} catch (\Exception $e) {
- $this->logger->error('SettingsService: Failed to get ArchiMate status from ArchiMateService', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'SettingsService: Failed to get ArchiMate status from ArchiMateService',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
- // Fallback to direct config access if ArchiMateService is not available
+ // Fallback to direct config access if ArchiMateService is not available.
$importStatus = $this->config->getValueString($this->_appName, 'archimate_import_status', '{}');
$exportStatus = $this->config->getValueString($this->_appName, 'archimate_export_status', '{}');
$importDecoded = json_decode($importStatus, true);
$exportDecoded = json_decode($exportStatus, true);
- // Get AMEF object counts
+ // Get AMEF object counts.
$amefObjectCounts = $this->getAmefObjectCounts();
+ if (is_array($importDecoded) === true) {
+ $importValue = $importDecoded;
+ } else {
+ $importValue = [];
+ }
+
+ if (is_array($exportDecoded) === true) {
+ $exportValue = $exportDecoded;
+ } else {
+ $exportValue = [];
+ }
+
return [
- 'import' => is_array($importDecoded) ? $importDecoded : [],
- 'export' => is_array($exportDecoded) ? $exportDecoded : [],
- 'totalElementObjects' => $amefObjectCounts['totalElementObjects'],
- 'totalOrganizationObjects' => $amefObjectCounts['totalOrganizationObjects'],
- 'totalViewObjects' => $amefObjectCounts['totalViewObjects'],
+ 'import' => $importValue,
+ 'export' => $exportValue,
+ 'totalElementObjects' => $amefObjectCounts['totalElementObjects'],
+ 'totalOrganizationObjects' => $amefObjectCounts['totalOrganizationObjects'],
+ 'totalViewObjects' => $amefObjectCounts['totalViewObjects'],
'totalRelationshipsObjects' => $amefObjectCounts['totalRelationshipsObjects'],
- 'totalModelObjects' => $amefObjectCounts['totalModelObjects']
+ 'totalModelObjects' => $amefObjectCounts['totalModelObjects'],
];
- }
- }
+ }//end try
+ }//end getArchiMateStatus()
/**
* Get Voorzieningen object counts for statistics
@@ -3596,72 +4054,72 @@ private function getVoorzieningenObjectCounts(): array
$objectService = $this->getObjectService();
if ($objectService === null) {
return [
- 'totalOrganisatieObjects' => 0,
- 'totalContactpersoonObjects' => 0,
- 'totalVoorzieningObjects' => 0,
+ 'totalOrganisatieObjects' => 0,
+ 'totalContactpersoonObjects' => 0,
+ 'totalVoorzieningObjects' => 0,
'totalVoorzieningAanbodObjects' => 0,
'totalVoorzieningVersieObjects' => 0,
- 'totalKwetsbaarheidObjects' => 0,
- 'totalContractObjects' => 0,
- 'totalStandaardObjects' => 0,
- 'totalReviewObjects' => 0,
- 'totalKoppelingObjects' => 0,
- 'totalBeoordeelingObjects' => 0,
+ 'totalKwetsbaarheidObjects' => 0,
+ 'totalContractObjects' => 0,
+ 'totalStandaardObjects' => 0,
+ 'totalReviewObjects' => 0,
+ 'totalKoppelingObjects' => 0,
+ 'totalBeoordeelingObjects' => 0,
'totalVoorzieningModuleObjects' => 0,
- 'totalVerklaringObjects' => 0,
- 'totalKoppelingGebruikObjects' => 0,
- 'totalCompliancyObjects' => 0,
- 'totalModuleGebruikObjects' => 0,
- 'totalModuleVersieObjects' => 0,
- 'totalSectorObjects' => 0,
- 'totalGebruikObjects' => 0
+ 'totalVerklaringObjects' => 0,
+ 'totalKoppelingGebruikObjects' => 0,
+ 'totalCompliancyObjects' => 0,
+ 'totalModuleGebruikObjects' => 0,
+ 'totalModuleVersieObjects' => 0,
+ 'totalSectorObjects' => 0,
+ 'totalGebruikObjects' => 0,
];
- }
+ }//end if
$voorzieningenConfig = $this->getVoorzieningenConfig();
- $registerId = $voorzieningenConfig['register'] ?? null;
+ $registerId = $voorzieningenConfig['register'] ?? null;
- // Define all schema mappings
+ // Define all schema mappings.
$schemaMappings = [
- 'organisatie_schema' => 'totalOrganisatieObjects',
- 'contactpersoon_schema' => 'totalContactpersoonObjects',
- 'voorziening_schema' => 'totalVoorzieningObjects',
+ 'organisatie_schema' => 'totalOrganisatieObjects',
+ 'contactpersoon_schema' => 'totalContactpersoonObjects',
+ 'voorziening_schema' => 'totalVoorzieningObjects',
'voorziening_aanbod_schema' => 'totalVoorzieningAanbodObjects',
'voorziening_versie_schema' => 'totalVoorzieningVersieObjects',
- 'kwetsbaarheid_schema' => 'totalKwetsbaarheidObjects',
- 'contract_schema' => 'totalContractObjects',
- 'standaard_schema' => 'totalStandaardObjects',
- 'review_schema' => 'totalReviewObjects',
- 'koppeling_schema' => 'totalKoppelingObjects',
- 'beoordeeling_schema' => 'totalBeoordeelingObjects',
- 'module_schema' => 'totalVoorzieningModuleObjects',
- 'verklaring_schema' => 'totalVerklaringObjects',
- 'koppeling_gebruik_schema' => 'totalKoppelingGebruikObjects',
- 'compliancy_schema' => 'totalCompliancyObjects',
- 'module_gebruik_schema' => 'totalModuleGebruikObjects',
- 'module_versie_schema' => 'totalModuleVersieObjects',
- 'sector_schema' => 'totalSectorObjects',
- 'gebruik_schema' => 'totalGebruikObjects'
+ 'kwetsbaarheid_schema' => 'totalKwetsbaarheidObjects',
+ 'contract_schema' => 'totalContractObjects',
+ 'standaard_schema' => 'totalStandaardObjects',
+ 'review_schema' => 'totalReviewObjects',
+ 'koppeling_schema' => 'totalKoppelingObjects',
+ 'beoordeeling_schema' => 'totalBeoordeelingObjects',
+ 'module_schema' => 'totalVoorzieningModuleObjects',
+ 'verklaring_schema' => 'totalVerklaringObjects',
+ 'koppeling_gebruik_schema' => 'totalKoppelingGebruikObjects',
+ 'compliancy_schema' => 'totalCompliancyObjects',
+ 'module_gebruik_schema' => 'totalModuleGebruikObjects',
+ 'module_versie_schema' => 'totalModuleVersieObjects',
+ 'sector_schema' => 'totalSectorObjects',
+ 'gebruik_schema' => 'totalGebruikObjects',
];
$counts = [];
- // Initialize all counts to 0
+ // Initialize all counts to 0.
foreach ($schemaMappings as $key => $countKey) {
$counts[$countKey] = 0;
}
- // Count objects for each configured schema
+ // Count objects for each configured schema.
foreach ($schemaMappings as $configKey => $countKey) {
$schemaId = $voorzieningenConfig[$configKey] ?? null;
- if ($registerId && $schemaId) {
+ if ($registerId !== false && $schemaId === true) {
try {
- $query = [
+ $query = [
'@self' => [
'register' => (int) $registerId,
- 'schema' => (int) $schemaId
- ]
+ 'schema' => (int) $schemaId,
+ ],
];
$objects = $objectService->searchObjects($query);
$counts[$countKey] = count($objects);
@@ -3674,36 +4132,38 @@ private function getVoorzieningenObjectCounts(): array
$this->logger->debug('SettingsService: Retrieved Voorzieningen object counts', $counts);
return $counts;
-
} catch (\Exception $e) {
- $this->logger->error('SettingsService: Failed to get Voorzieningen object counts', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'SettingsService: Failed to get Voorzieningen object counts',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return [
- 'totalOrganisatieObjects' => 0,
- 'totalContactpersoonObjects' => 0,
- 'totalVoorzieningObjects' => 0,
+ 'totalOrganisatieObjects' => 0,
+ 'totalContactpersoonObjects' => 0,
+ 'totalVoorzieningObjects' => 0,
'totalVoorzieningAanbodObjects' => 0,
'totalVoorzieningVersieObjects' => 0,
- 'totalKwetsbaarheidObjects' => 0,
- 'totalContractObjects' => 0,
- 'totalStandaardObjects' => 0,
- 'totalReviewObjects' => 0,
- 'totalKoppelingObjects' => 0,
- 'totalBeoordeelingObjects' => 0,
+ 'totalKwetsbaarheidObjects' => 0,
+ 'totalContractObjects' => 0,
+ 'totalStandaardObjects' => 0,
+ 'totalReviewObjects' => 0,
+ 'totalKoppelingObjects' => 0,
+ 'totalBeoordeelingObjects' => 0,
'totalVoorzieningModuleObjects' => 0,
- 'totalVerklaringObjects' => 0,
- 'totalKoppelingGebruikObjects' => 0,
- 'totalCompliancyObjects' => 0,
- 'totalModuleGebruikObjects' => 0,
- 'totalModuleVersieObjects' => 0,
- 'totalSectorObjects' => 0,
- 'totalGebruikObjects' => 0
+ 'totalVerklaringObjects' => 0,
+ 'totalKoppelingGebruikObjects' => 0,
+ 'totalCompliancyObjects' => 0,
+ 'totalModuleGebruikObjects' => 0,
+ 'totalModuleVersieObjects' => 0,
+ 'totalSectorObjects' => 0,
+ 'totalGebruikObjects' => 0,
];
- }
- }
+ }//end try
+ }//end getVoorzieningenObjectCounts()
/**
* Gets AMEF object counts for consolidated configuration
@@ -3716,48 +4176,53 @@ private function getVoorzieningenObjectCounts(): array
private function getAmefObjectCounts(): array
{
try {
- // Get ArchiMateService from container to avoid circular dependency
+ // Get ArchiMateService from container to avoid circular dependency.
$archiMateService = $this->container->get(\OCA\SoftwareCatalog\Service\ArchiMateService::class);
- // Get object counts using ArchiMateService methods
- $elementObjects = $archiMateService->getElementObjects();
+ // Get object counts using ArchiMateService methods.
+ $elementObjects = $archiMateService->getElementObjects();
$organizationObjects = $archiMateService->getOrganizationObjects();
- $viewObjects = $archiMateService->getViewObjects();
+ $viewObjects = $archiMateService->getViewObjects();
$relationshipObjects = $archiMateService->getRelationshipObjects();
- $modelObjects = $archiMateService->getModelObjects();
-
- $this->logger->debug('SettingsService: Retrieved AMEF object counts', [
- 'elementObjects' => count($elementObjects),
- 'organizationObjects' => count($organizationObjects),
- 'viewObjects' => count($viewObjects),
- 'relationshipObjects' => count($relationshipObjects),
- 'modelObjects' => count($modelObjects)
- ]);
+ $modelObjects = $archiMateService->getModelObjects();
+
+ $this->logger->debug(
+ 'SettingsService: Retrieved AMEF object counts',
+ [
+ 'elementObjects' => count($elementObjects),
+ 'organizationObjects' => count($organizationObjects),
+ 'viewObjects' => count($viewObjects),
+ 'relationshipObjects' => count($relationshipObjects),
+ 'modelObjects' => count($modelObjects),
+ ]
+ );
return [
- 'totalElementObjects' => count($elementObjects),
- 'totalOrganizationObjects' => count($organizationObjects),
- 'totalViewObjects' => count($viewObjects),
+ 'totalElementObjects' => count($elementObjects),
+ 'totalOrganizationObjects' => count($organizationObjects),
+ 'totalViewObjects' => count($viewObjects),
'totalRelationshipsObjects' => count($relationshipObjects),
- 'totalModelObjects' => count($modelObjects)
+ 'totalModelObjects' => count($modelObjects),
];
-
} catch (\Exception $e) {
- $this->logger->error('SettingsService: Failed to get AMEF object counts', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'SettingsService: Failed to get AMEF object counts',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
- // Return zero counts on error to prevent API failures
+ // Return zero counts on error to prevent API failures.
return [
- 'totalElementObjects' => 0,
- 'totalOrganizationObjects' => 0,
- 'totalViewObjects' => 0,
+ 'totalElementObjects' => 0,
+ 'totalOrganizationObjects' => 0,
+ 'totalViewObjects' => 0,
'totalRelationshipsObjects' => 0,
- 'totalModelObjects' => 0
+ 'totalModelObjects' => 0,
];
- }
- }
+ }//end try
+ }//end getAmefObjectCounts()
/**
* Set ArchiMate import status
@@ -3765,28 +4230,31 @@ private function getAmefObjectCounts(): array
* This method delegates to ArchiMateService to avoid code duplication
* and ensure consistency in ArchiMate status management.
*
- * @param array $status The import status
+ * @param array $status The import status.
+ *
* @return void
*/
public function setArchiMateImportStatus(array $status): void
{
try {
- // Get ArchiMateService from container to avoid circular dependency
+ // Get ArchiMateService from container to avoid circular dependency.
$archiMateService = $this->container->get(\OCA\SoftwareCatalog\Service\ArchiMateService::class);
$archiMateService->setArchiMateImportStatus($status);
-
} catch (\Exception $e) {
- $this->logger->error('SettingsService: Failed to set ArchiMate import status via ArchiMateService', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'SettingsService: Failed to set ArchiMate import status via ArchiMateService',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
- // Fallback to direct config access if ArchiMateService is not available
+ // Fallback to direct config access if ArchiMateService is not available.
$jsonStatus = json_encode($status, JSON_PRETTY_PRINT);
$this->config->setValueString($this->_appName, 'archimate_import_status', $jsonStatus);
}
- }
+ }//end setArchiMateImportStatus()
/**
* Set ArchiMate export status
@@ -3794,28 +4262,31 @@ public function setArchiMateImportStatus(array $status): void
* This method delegates to ArchiMateService to avoid code duplication
* and ensure consistency in ArchiMate status management.
*
- * @param array $status The export status
+ * @param array $status The export status.
+ *
* @return void
*/
public function setArchiMateExportStatus(array $status): void
{
try {
- // Get ArchiMateService from container to avoid circular dependency
+ // Get ArchiMateService from container to avoid circular dependency.
$archiMateService = $this->container->get(\OCA\SoftwareCatalog\Service\ArchiMateService::class);
$archiMateService->setArchiMateExportStatus($status);
-
} catch (\Exception $e) {
- $this->logger->error('SettingsService: Failed to set ArchiMate export status via ArchiMateService', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'SettingsService: Failed to set ArchiMate export status via ArchiMateService',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
- // Fallback to direct config access if ArchiMateService is not available
+ // Fallback to direct config access if ArchiMateService is not available.
$jsonStatus = json_encode($status, JSON_PRETTY_PRINT);
$this->config->setValueString($this->_appName, 'archimate_export_status', $jsonStatus);
}
- }
+ }//end setArchiMateExportStatus()
/**
* Clear ArchiMate import status
@@ -3828,29 +4299,31 @@ public function setArchiMateExportStatus(array $status): void
public function clearArchiMateImportStatus(): array
{
try {
- // Get ArchiMateService from container to avoid circular dependency
+ // Get ArchiMateService from container to avoid circular dependency.
$archiMateService = $this->container->get(\OCA\SoftwareCatalog\Service\ArchiMateService::class);
return $archiMateService->clearArchiMateImportStatus();
-
} catch (\Exception $e) {
- $this->logger->error('SettingsService: Failed to clear ArchiMate import status via ArchiMateService', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'SettingsService: Failed to clear ArchiMate import status via ArchiMateService',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
- // Fallback to direct config access if ArchiMateService is not available
+ // Fallback to direct config access if ArchiMateService is not available.
$this->config->deleteKey($this->_appName, 'archimate_import_status');
return [
- 'cleared' => true,
+ 'cleared' => true,
'process_killed' => false,
- 'process_id' => null,
- 'was_running' => false,
- 'messages' => ['Import status cleared via fallback method']
+ 'process_id' => null,
+ 'was_running' => false,
+ 'messages' => ['Import status cleared via fallback method'],
];
- }
- }
+ }//end try
+ }//end clearArchiMateImportStatus()
/**
* Force kill running ArchiMate import process and clear status
@@ -3858,35 +4331,38 @@ public function clearArchiMateImportStatus(): array
* This method delegates to ArchiMateService to handle process termination
* and status cleanup.
*
- * @return array Kill operation result
+ * @return array Kill operation result
* @deprecated Use cancelArchiMateImport() instead
*/
public function killArchiMateImport(): array
{
try {
- // Get ArchiMateService from container to avoid circular dependency
+ // Get ArchiMateService from container to avoid circular dependency.
$archiMateService = $this->container->get(\OCA\SoftwareCatalog\Service\ArchiMateService::class);
- return $archiMateService->clearArchiMateImportStatus(true); // killProcess = true
-
+ return $archiMateService->clearArchiMateImportStatus(true);
+ // KillProcess = true.
} catch (\Exception $e) {
- $this->logger->error('SettingsService: Failed to kill ArchiMate import process via ArchiMateService', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'SettingsService: Failed to kill ArchiMate import process via ArchiMateService',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
- // Fallback to just clearing config if ArchiMateService is not available
+ // Fallback to just clearing config if ArchiMateService is not available.
$this->config->deleteKey($this->_appName, 'archimate_import_status');
return [
- 'cleared' => true,
+ 'cleared' => true,
'process_killed' => false,
- 'process_id' => null,
- 'was_running' => false,
- 'messages' => ['Import status cleared via fallback method - could not kill process']
+ 'process_id' => null,
+ 'was_running' => false,
+ 'messages' => ['Import status cleared via fallback method - could not kill process'],
];
- }
- }
+ }//end try
+ }//end killArchiMateImport()
/**
* Cancel a running ArchiMate import
@@ -3899,31 +4375,33 @@ public function killArchiMateImport(): array
public function cancelArchiMateImport(): array
{
try {
- // Get ArchiMateService from container to avoid circular dependency
+ // Get ArchiMateService from container to avoid circular dependency.
$archiMateService = $this->container->get(\OCA\SoftwareCatalog\Service\ArchiMateService::class);
return $archiMateService->cancelArchiMateImport();
-
} catch (\Exception $e) {
- $this->logger->error('SettingsService: Failed to cancel ArchiMate import via ArchiMateService', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'SettingsService: Failed to cancel ArchiMate import via ArchiMateService',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
- // Fallback to just clearing config if ArchiMateService is not available
+ // Fallback to just clearing config if ArchiMateService is not available.
$this->config->deleteKey($this->_appName, 'archimate_import_status');
return [
- 'cancelled' => true,
- 'was_running' => false,
- 'process_id' => null,
- 'process_killed' => false,
- 'status_cleared' => true,
+ 'cancelled' => true,
+ 'was_running' => false,
+ 'process_id' => null,
+ 'process_killed' => false,
+ 'status_cleared' => true,
'cancellation_time' => date('Y-m-d H:i:s'),
- 'messages' => ['Import status cleared via fallback method - ArchiMateService not available']
+ 'messages' => ['Import status cleared via fallback method - ArchiMateService not available'],
];
- }
- }
+ }//end try
+ }//end cancelArchiMateImport()
/**
* Clear ArchiMate export status
@@ -3936,21 +4414,23 @@ public function cancelArchiMateImport(): array
public function clearArchiMateExportStatus(): void
{
try {
- // Get ArchiMateService from container to avoid circular dependency
+ // Get ArchiMateService from container to avoid circular dependency.
$archiMateService = $this->container->get(\OCA\SoftwareCatalog\Service\ArchiMateService::class);
$archiMateService->clearArchiMateExportStatus();
-
} catch (\Exception $e) {
- $this->logger->error('SettingsService: Failed to clear ArchiMate export status via ArchiMateService', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'SettingsService: Failed to clear ArchiMate export status via ArchiMateService',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
- // Fallback to direct config access if ArchiMateService is not available
+ // Fallback to direct config access if ArchiMateService is not available.
$this->config->deleteKey($this->_appName, 'archimate_export_status');
}
- }
+ }//end clearArchiMateExportStatus()
/**
* Compact existing individual configuration values to JSON format
@@ -3961,91 +4441,96 @@ public function clearArchiMateExportStatus(): void
public function compactToJsonConfiguration(): array
{
$results = [
- 'success' => true,
+ 'success' => true,
'migrated' => [],
- 'errors' => []
+ 'errors' => [],
];
try {
- // 1. Migrate Voorzieningen configuration
+ // 1. Migrate Voorzieningen configuration.
$voorzieningenConfig = [
- 'register' => $this->config->getValueString($this->_appName, 'voorzieningen_register', ''),
- 'organisatie_schema' => $this->config->getValueString($this->_appName, 'voorzieningen_organisatie_schema', ''),
- 'contactpersoon_schema' => $this->config->getValueString($this->_appName, 'voorzieningen_contactpersoon_schema', ''),
- 'organisatie_source' => $this->config->getValueString($this->_appName, 'voorzieningen_organisatie_source', 'openregister'),
- 'contactpersoon_source' => $this->config->getValueString($this->_appName, 'voorzieningen_contactpersoon_source', 'openregister'),
- 'organisatie_register' => $this->config->getValueString($this->_appName, 'voorzieningen_organisatie_register', ''),
+ 'register' => $this->config->getValueString($this->_appName, 'voorzieningen_register', ''),
+ 'organisatie_schema' => $this->config->getValueString($this->_appName, 'voorzieningen_organisatie_schema', ''),
+ 'contactpersoon_schema' => $this->config->getValueString($this->_appName, 'voorzieningen_contactpersoon_schema', ''),
+ 'organisatie_source' => $this->config->getValueString($this->_appName, 'voorzieningen_organisatie_source', 'openregister'),
+ 'contactpersoon_source' => $this->config->getValueString($this->_appName, 'voorzieningen_contactpersoon_source', 'openregister'),
+ 'organisatie_register' => $this->config->getValueString($this->_appName, 'voorzieningen_organisatie_register', ''),
'contactpersoon_register' => $this->config->getValueString($this->_appName, 'voorzieningen_contactpersoon_register', ''),
];
- $this->setVoorzieningenConfig($voorzieningenConfig);
+ $this->setVoorzieningenConfig(config: $voorzieningenConfig);
$results['migrated']['voorzieningen'] = $voorzieningenConfig;
- // 2. Migrate AMEF configuration
+ // 2. Migrate AMEF configuration.
$amefConfig = [
- 'register_id' => $this->config->getValueString($this->_appName, 'amef_register_id', ''),
- 'organizations_schema' => $this->config->getValueString($this->_appName, 'amef_organizations_schema', ''),
- 'elements_schema' => $this->config->getValueString($this->_appName, 'amef_elements_schema', ''),
- 'relationships_schema' => $this->config->getValueString($this->_appName, 'amef_relationships_schema', ''),
- 'views_schema' => $this->config->getValueString($this->_appName, 'amef_views_schema', ''),
- 'models_schema' => $this->config->getValueString($this->_appName, 'amef_models_schema', ''),
- 'organization_source' => $this->config->getValueString($this->_appName, 'amef_organization_source', 'openregister'),
+ 'register_id' => $this->config->getValueString($this->_appName, 'amef_register_id', ''),
+ 'organizations_schema' => $this->config->getValueString($this->_appName, 'amef_organizations_schema', ''),
+ 'elements_schema' => $this->config->getValueString($this->_appName, 'amef_elements_schema', ''),
+ 'relationships_schema' => $this->config->getValueString($this->_appName, 'amef_relationships_schema', ''),
+ 'views_schema' => $this->config->getValueString($this->_appName, 'amef_views_schema', ''),
+ 'models_schema' => $this->config->getValueString($this->_appName, 'amef_models_schema', ''),
+ 'organization_source' => $this->config->getValueString($this->_appName, 'amef_organization_source', 'openregister'),
'organization_register' => $this->config->getValueString($this->_appName, 'amef_organization_register', ''),
- 'organization_schema' => $this->config->getValueString($this->_appName, 'amef_organization_schema', ''),
- // Note: These duplicated entries with typos are kept for backward compatibility but should be cleaned up
- 'elementss_schema' => $this->config->getValueString($this->_appName, 'amef_elementss_schema', ''),
+ 'organization_schema' => $this->config->getValueString($this->_appName, 'amef_organization_schema', ''),
+ // Note: These duplicated entries with typos are kept for backward compatibility but should be cleaned up.
+ 'elementss_schema' => $this->config->getValueString($this->_appName, 'amef_elementss_schema', ''),
'organizationss_schema' => $this->config->getValueString($this->_appName, 'amef_organizationss_schema', ''),
- 'relationshipss_schema' => $this->config->getValueString($this->_appName, 'amef_relationshipss_schema', '')
+ 'relationshipss_schema' => $this->config->getValueString($this->_appName, 'amef_relationshipss_schema', ''),
];
- $this->setAmefConfig($amefConfig);
+ $this->setAmefConfig(config: $amefConfig);
$results['migrated']['amef'] = $amefConfig;
- // 3. Migrate Email configuration
+ // 3. Migrate Email configuration.
$emailConfig = [
- 'enabled' => $this->config->getValueString($this->_appName, 'email_enabled', 'false') === 'true',
- 'transport_type' => $this->config->getValueString($this->_appName, 'email_transport_type', 'smtp'),
- 'smtp_host' => $this->config->getValueString($this->_appName, 'email_smtp_host', ''),
- 'smtp_port' => $this->config->getValueString($this->_appName, 'email_smtp_port', '587'),
- 'smtp_username' => $this->config->getValueString($this->_appName, 'email_smtp_username', ''),
- 'smtp_password' => $this->config->getValueString($this->_appName, 'email_smtp_password', ''),
- 'smtp_encryption' => $this->config->getValueString($this->_appName, 'email_smtp_encryption', 'tls'),
- 'sender_email' => $this->config->getValueString($this->_appName, 'sender_email', ''),
- 'sender_name' => $this->config->getValueString($this->_appName, 'sender_name', ''),
- 'mailjet_api_key' => $this->config->getValueString($this->_appName, 'email_mailjet_api_key', ''),
- 'mailjet_secret_key' => $this->config->getValueString($this->_appName, 'email_mailjet_secret_key', ''),
- 'sendgrid_api_key' => $this->config->getValueString($this->_appName, 'email_sendgrid_api_key', ''),
- 'mailgun_api_key' => $this->config->getValueString($this->_appName, 'email_mailgun_api_key', ''),
- 'mailgun_domain' => $this->config->getValueString($this->_appName, 'email_mailgun_domain', ''),
- 'postmark_api_key' => $this->config->getValueString($this->_appName, 'email_postmark_api_key', ''),
- 'ses_access_key' => $this->config->getValueString($this->_appName, 'email_ses_access_key', ''),
- 'ses_secret_key' => $this->config->getValueString($this->_appName, 'email_ses_secret_key', ''),
- 'ses_region' => $this->config->getValueString($this->_appName, 'email_ses_region', 'us-east-1'),
+ 'enabled' => $this->config->getValueString($this->_appName, 'email_enabled', 'false') === 'true',
+ 'transport_type' => $this->config->getValueString($this->_appName, 'email_transport_type', 'smtp'),
+ 'smtp_host' => $this->config->getValueString($this->_appName, 'email_smtp_host', ''),
+ 'smtp_port' => $this->config->getValueString($this->_appName, 'email_smtp_port', '587'),
+ 'smtp_username' => $this->config->getValueString($this->_appName, 'email_smtp_username', ''),
+ 'smtp_password' => $this->config->getValueString($this->_appName, 'email_smtp_password', ''),
+ 'smtp_encryption' => $this->config->getValueString($this->_appName, 'email_smtp_encryption', 'tls'),
+ 'sender_email' => $this->config->getValueString($this->_appName, 'sender_email', ''),
+ 'sender_name' => $this->config->getValueString($this->_appName, 'sender_name', ''),
+ 'mailjet_api_key' => $this->config->getValueString($this->_appName, 'email_mailjet_api_key', ''),
+ 'mailjet_secret_key' => $this->config->getValueString($this->_appName, 'email_mailjet_secret_key', ''),
+ 'sendgrid_api_key' => $this->config->getValueString($this->_appName, 'email_sendgrid_api_key', ''),
+ 'mailgun_api_key' => $this->config->getValueString($this->_appName, 'email_mailgun_api_key', ''),
+ 'mailgun_domain' => $this->config->getValueString($this->_appName, 'email_mailgun_domain', ''),
+ 'postmark_api_key' => $this->config->getValueString($this->_appName, 'email_postmark_api_key', ''),
+ 'ses_access_key' => $this->config->getValueString($this->_appName, 'email_ses_access_key', ''),
+ 'ses_secret_key' => $this->config->getValueString($this->_appName, 'email_ses_secret_key', ''),
+ 'ses_region' => $this->config->getValueString($this->_appName, 'email_ses_region', 'us-east-1'),
'org_registration_enabled' => $this->config->getValueString($this->_appName, 'email_org_registration_enabled', 'true') === 'true',
- 'org_activation_enabled' => $this->config->getValueString($this->_appName, 'email_org_activation_enabled', 'true') === 'true',
- 'user_creation_enabled' => $this->config->getValueString($this->_appName, 'email_user_creation_enabled', 'true') === 'true',
- 'user_password_enabled' => $this->config->getValueString($this->_appName, 'email_user_password_enabled', 'true') === 'true',
- 'test_receiver_override' => $this->config->getValueString($this->_appName, 'test_receiver_override', '')
+ 'org_activation_enabled' => $this->config->getValueString($this->_appName, 'email_org_activation_enabled', 'true') === 'true',
+ 'user_creation_enabled' => $this->config->getValueString($this->_appName, 'email_user_creation_enabled', 'true') === 'true',
+ 'user_password_enabled' => $this->config->getValueString($this->_appName, 'email_user_password_enabled', 'true') === 'true',
+ 'test_receiver_override' => $this->config->getValueString($this->_appName, 'test_receiver_override', ''),
];
- $this->setEmailConfig($emailConfig);
+ $this->setEmailConfig(config: $emailConfig);
$results['migrated']['email'] = $emailConfig;
- $this->logger->info('Configuration compaction to JSON format completed successfully', [
- 'compacted_sections' => array_keys($results['migrated'])
- ]);
-
+ $this->logger->info(
+ 'Configuration compaction to JSON format completed successfully',
+ [
+ 'compacted_sections' => array_keys($results['migrated']),
+ ]
+ );
} catch (\Exception $e) {
- $results['success'] = false;
- $results['errors'][] = 'Compaction failed: ' . $e->getMessage();
- $this->logger->error('Configuration compaction to JSON format failed', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
- }
+ $results['success'] = false;
+ $results['errors'][] = 'Compaction failed: '.$e->getMessage();
+ $this->logger->error(
+ 'Configuration compaction to JSON format failed',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+ }//end try
return $results;
- }
+ }//end compactToJsonConfiguration()
/**
* Clean up old individual configuration values after compaction
@@ -4058,28 +4543,33 @@ public function cleanupOldConfiguration(): array
$results = [
'success' => true,
'cleaned' => [],
- 'errors' => []
+ 'errors' => [],
];
try {
- // List of old configuration keys to remove
+ // List of old configuration keys to remove.
$oldKeys = [
- // Voorzieningen keys - old individual keys
+ // Voorzieningen keys - old individual keys.
'voorzieningen_register',
'voorzieningen_organisatie_schema',
'voorzieningen_contactpersoon_schema',
- 'voorzieningen_gebruiker_schema', // Deprecated - no longer used
- 'voorzieningen_contactgegevens_schema', // Deprecated - no longer used
+ 'voorzieningen_gebruiker_schema',
+ // Deprecated - no longer used.
+ 'voorzieningen_contactgegevens_schema',
+ // Deprecated - no longer used.
'voorzieningen_organisatie_source',
'voorzieningen_contactpersoon_source',
- 'voorzieningen_gebruiker_source', // Deprecated - no longer used
- 'voorzieningen_contactgegevens_source', // Deprecated - no longer used
+ 'voorzieningen_gebruiker_source',
+ // Deprecated - no longer used.
+ 'voorzieningen_contactgegevens_source',
+ // Deprecated - no longer used.
'voorzieningen_organisatie_register',
'voorzieningen_contactpersoon_register',
- 'voorzieningen_gebruiker_register', // Deprecated - no longer used
- 'voorzieningen_contactgegevens_register', // Deprecated - no longer used
-
- // Old Voorzieningen schema keys that no longer exist in register
+ 'voorzieningen_gebruiker_register',
+ // Deprecated - no longer used.
+ 'voorzieningen_contactgegevens_register',
+ // Deprecated - no longer used.
+ // Old Voorzieningen schema keys that no longer exist in register.
'voorzieningen_voorziening_schema',
'voorzieningen_voorziening_aanbod_schema',
'voorzieningen_voorziening_versie_schema',
@@ -4091,7 +4581,7 @@ public function cleanupOldConfiguration(): array
'voorzieningen_module_gebruik_schema',
'voorzieningen_module_versie_schema',
- // AMEF keys - old individual keys
+ // AMEF keys - old individual keys.
'amef_register_id',
'amef_organizations_schema',
'amef_elements_schema',
@@ -4106,11 +4596,11 @@ public function cleanupOldConfiguration(): array
'amef_organizationss_schema',
'amef_relationshipss_schema',
- // AMEF keys with hyphen format (old)
+ // AMEF keys with hyphen format (old).
'amef_property-definition_schema',
- 'amef_extendview_schema', // No longer in register
-
- // Email keys
+ 'amef_extendview_schema',
+ // No longer in register.
+ // Email keys.
'email_enabled',
'email_transport_type',
'email_smtp_host',
@@ -4133,7 +4623,7 @@ public function cleanupOldConfiguration(): array
'email_org_activation_enabled',
'email_user_creation_enabled',
'email_user_password_enabled',
- 'test_receiver_override'
+ 'test_receiver_override',
];
foreach ($oldKeys as $key) {
@@ -4141,29 +4631,34 @@ public function cleanupOldConfiguration(): array
$this->config->deleteKey($this->_appName, $key);
$results['cleaned'][] = $key;
} catch (\Exception $e) {
- $results['errors'][] = "Failed to delete key '{$key}': " . $e->getMessage();
+ $results['errors'][] = "Failed to delete key '{$key}': ".$e->getMessage();
}
}
- $this->logger->info('Old configuration cleanup completed', [
- 'cleaned_keys' => count($results['cleaned']),
- 'errors' => count($results['errors'])
- ]);
-
+ $this->logger->info(
+ 'Old configuration cleanup completed',
+ [
+ 'cleaned_keys' => count($results['cleaned']),
+ 'errors' => count($results['errors']),
+ ]
+ );
} catch (\Exception $e) {
- $results['success'] = false;
- $results['errors'][] = 'Cleanup failed: ' . $e->getMessage();
- $this->logger->error('Old configuration cleanup failed', [
- 'error' => $e->getMessage()
- ]);
- }
+ $results['success'] = false;
+ $results['errors'][] = 'Cleanup failed: '.$e->getMessage();
+ $this->logger->error(
+ 'Old configuration cleanup failed',
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
return $results;
- }
+ }//end cleanupOldConfiguration()
- // ========================================================================
- // CONTROLLER BUSINESS LOGIC METHODS
- // ========================================================================
+ // ======================================================.
+ // CONTROLLER BUSINESS LOGIC METHODS.
+ // ======================================================.
/**
* Get all settings including user groups and email settings
@@ -4174,49 +4669,51 @@ public function cleanupOldConfiguration(): array
public function getAllSettings(): array
{
try {
- // Provide only lightweight settings data. Section-specific data is
+ // Provide only lightweight settings data. Section-specific data is.
// available via focused endpoints for performance.
$base = $this->getSettings();
$versionInfo = $this->getVersionInfo();
- // Get voorzieningen config (lightweight - just reads from config storage)
+ // Get voorzieningen config (lightweight - just reads from config storage).
$voorzieningenConfig = $this->getVoorzieningenConfig();
- // Get amef config directly from config storage (avoid heavy ArchiMateService call)
+ // Get amef config directly from config storage (avoid heavy ArchiMateService call).
$amefConfigJson = $this->config->getValueString($this->_appName, 'amef_config', '{}');
- $amefConfig = json_decode($amefConfigJson, true);
- if (!is_array($amefConfig)) {
+ $amefConfig = json_decode($amefConfigJson, true);
+ if (is_array($amefConfig) === false) {
$amefConfig = [
- 'register' => $this->config->getValueString($this->_appName, 'amef_register_id', ''),
+ 'register' => $this->config->getValueString($this->_appName, 'amef_register_id', ''),
'organization_schema' => $this->config->getValueString($this->_appName, 'amef_organizations_schema', ''),
- 'element_schema' => $this->config->getValueString($this->_appName, 'amef_elements_schema', ''),
- 'relation_schema' => $this->config->getValueString($this->_appName, 'amef_relationships_schema', ''),
- 'view_schema' => $this->config->getValueString($this->_appName, 'amef_views_schema', ''),
- 'model_schema' => $this->config->getValueString($this->_appName, 'amef_models_schema', '')
+ 'element_schema' => $this->config->getValueString($this->_appName, 'amef_elements_schema', ''),
+ 'relation_schema' => $this->config->getValueString($this->_appName, 'amef_relationships_schema', ''),
+ 'view_schema' => $this->config->getValueString($this->_appName, 'amef_views_schema', ''),
+ 'model_schema' => $this->config->getValueString($this->_appName, 'amef_models_schema', ''),
];
}
$result = [
- 'availableRegisters' => $base['availableRegisters'] ?? [],
- 'versionInfo' => $versionInfo,
+ 'availableRegisters' => $base['availableRegisters'] ?? [],
+ 'versionInfo' => $versionInfo,
'voorzieningenConfig' => $voorzieningenConfig,
- 'amefConfig' => $amefConfig,
- 'timestamp' => time(),
+ 'amefConfig' => $amefConfig,
+ 'timestamp' => time(),
];
return $result;
-
} catch (\Exception $e) {
- $this->logger->error('SettingsService: Failed to get all settings', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'SettingsService: Failed to get all settings',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return [
'success' => false,
- 'error' => $e->getMessage(),
+ 'error' => $e->getMessage(),
];
- }
- }
+ }//end try
+ }//end getAllSettings()
/**
* Get object counts statistics for all configured registers
@@ -4228,86 +4725,89 @@ public function getObjectCountsStatistics(): array
try {
$statistics = [
'voorzieningen' => [],
- 'amef' => [],
- 'timestamp' => time()
+ 'amef' => [],
+ 'timestamp' => time(),
];
- // Get Voorzieningen statistics
+ // Get Voorzieningen statistics.
try {
$voorzieningenConfig = $this->getVoorzieningenConfig();
$voorzieningenCounts = $this->getVoorzieningenObjectCounts();
$statistics['voorzieningen'] = [
- 'config' => $voorzieningenConfig,
+ 'config' => $voorzieningenConfig,
'object_counts' => $voorzieningenCounts,
- 'configured' => !empty($voorzieningenConfig['register']) && !empty($voorzieningenConfig['organisatie_schema'])
+ 'configured' => empty($voorzieningenConfig['register']) === false
+ && empty($voorzieningenConfig['organisatie_schema']) === false,
];
} catch (\Exception $e) {
$this->logger->error('Failed to get Voorzieningen statistics', ['error' => $e->getMessage()]);
$statistics['voorzieningen'] = [
- 'config' => [],
+ 'config' => [],
'object_counts' => ['totalOrganisatieObjects' => 0, 'totalContactpersoonObjects' => 0],
- 'configured' => false,
- 'error' => $e->getMessage()
+ 'configured' => false,
+ 'error' => $e->getMessage(),
];
}
- // Get AMEF statistics
+ // Get AMEF statistics.
try {
$amefConfig = $this->getAmefConfig();
$amefCounts = $this->getAmefObjectCounts();
$statistics['amef'] = [
- 'config' => $amefConfig,
+ 'config' => $amefConfig,
'object_counts' => $amefCounts,
- 'configured' => !empty($amefConfig['register_id']) && !empty($amefConfig['elements_schema'])
+ 'configured' => empty($amefConfig['register_id']) === false && empty($amefConfig['elements_schema']) === false,
];
} catch (\Exception $e) {
$this->logger->error('Failed to get AMEF statistics', ['error' => $e->getMessage()]);
$statistics['amef'] = [
- 'config' => [],
+ 'config' => [],
'object_counts' => [
- 'totalElementObjects' => 0,
- 'totalOrganizationObjects' => 0,
- 'totalViewObjects' => 0,
+ 'totalElementObjects' => 0,
+ 'totalOrganizationObjects' => 0,
+ 'totalViewObjects' => 0,
'totalRelationshipsObjects' => 0,
- 'totalModelObjects' => 0
+ 'totalModelObjects' => 0,
],
- 'configured' => false,
- 'error' => $e->getMessage()
+ 'configured' => false,
+ 'error' => $e->getMessage(),
];
- }
+ }//end try
return $statistics;
-
} catch (\Exception $e) {
- $this->logger->error('SettingsService: Failed to get object counts statistics', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'SettingsService: Failed to get object counts statistics',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return [
'voorzieningen' => [
- 'config' => [],
+ 'config' => [],
'object_counts' => ['totalOrganisatieObjects' => 0, 'totalContactpersoonObjects' => 0],
- 'configured' => false,
- 'error' => $e->getMessage()
+ 'configured' => false,
+ 'error' => $e->getMessage(),
],
- 'amef' => [
- 'config' => [],
+ 'amef' => [
+ 'config' => [],
'object_counts' => [
- 'totalElementObjects' => 0,
- 'totalOrganizationObjects' => 0,
- 'totalViewObjects' => 0,
+ 'totalElementObjects' => 0,
+ 'totalOrganizationObjects' => 0,
+ 'totalViewObjects' => 0,
'totalRelationshipsObjects' => 0,
- 'totalModelObjects' => 0
+ 'totalModelObjects' => 0,
],
- 'configured' => false,
- 'error' => $e->getMessage()
+ 'configured' => false,
+ 'error' => $e->getMessage(),
],
- 'timestamp' => time(),
- 'error' => $e->getMessage()
+ 'timestamp' => time(),
+ 'error' => $e->getMessage(),
];
- }
- }
+ }//end try
+ }//end getObjectCountsStatistics()
/**
* Get all email templates with error handling
@@ -4318,11 +4818,11 @@ public function getObjectCountsStatistics(): array
public function getAllEmailTemplates(): array
{
$templateTypes = ['organization_registration', 'organization_activation', 'user_creation', 'user_password', 'user_organisation'];
- $templates = [];
+ $templates = [];
foreach ($templateTypes as $templateName) {
try {
- $templates[$templateName] = $this->getEmailTemplate($templateName);
+ $templates[$templateName] = $this->getEmailTemplate(templateName: $templateName);
} catch (\Exception $e) {
$this->logger->warning("Failed to get template {$templateName}", ['error' => $e->getMessage()]);
$templates[$templateName] = null;
@@ -4330,125 +4830,134 @@ public function getAllEmailTemplates(): array
}
return $templates;
- }
+ }//end getAllEmailTemplates()
/**
* Update generic user groups with validation
*
- * @param array $groups Groups to set
- * @return array Update result with validation
+ * @param array $groups Groups to set.
+ *
+ * @return array Update result with validation.
*/
public function updateGenericUserGroups(array $groups): array
{
try {
- $validation = $this->validateGroups($groups);
+ $validation = $this->validateGroups(groups: $groups);
- if (!empty($validation['invalid'])) {
+ if (empty($validation['invalid']) === false) {
return [
- 'success' => false,
- 'message' => 'Invalid group names provided',
- 'validation' => $validation
+ 'success' => false,
+ 'message' => 'Invalid group names provided',
+ 'validation' => $validation,
];
}
- $this->setGenericUserGroups($validation['valid']);
+ $this->setGenericUserGroups(groups: $validation['valid']);
return [
'success' => true,
'message' => 'Generic user groups updated successfully',
- 'groups' => $validation['valid']
+ 'groups' => $validation['valid'],
];
-
} catch (\Exception $e) {
- $this->logger->error('SettingsService: Failed to update generic user groups', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'SettingsService: Failed to update generic user groups',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return [
'success' => false,
- 'message' => 'Failed to update generic user groups: ' . $e->getMessage()
+ 'message' => 'Failed to update generic user groups: '.$e->getMessage(),
];
- }
- }
+ }//end try
+ }//end updateGenericUserGroups()
/**
* Update organization admin groups with validation
*
- * @param array $groups Groups to set
- * @return array Update result with validation
+ * @param array $groups Groups to set.
+ *
+ * @return array Update result with validation.
*/
public function updateOrganizationAdminGroups(array $groups): array
{
try {
- $validation = $this->validateGroups($groups);
+ $validation = $this->validateGroups(groups: $groups);
- if (!empty($validation['invalid'])) {
+ if (empty($validation['invalid']) === false) {
return [
- 'success' => false,
- 'message' => 'Invalid group names provided',
- 'validation' => $validation
+ 'success' => false,
+ 'message' => 'Invalid group names provided',
+ 'validation' => $validation,
];
}
- $this->setOrganizationAdminGroups($validation['valid']);
+ $this->setOrganizationAdminGroups(groups: $validation['valid']);
return [
'success' => true,
'message' => 'Organization admin groups updated successfully',
- 'groups' => $validation['valid']
+ 'groups' => $validation['valid'],
];
-
} catch (\Exception $e) {
- $this->logger->error('SettingsService: Failed to update organization admin groups', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'SettingsService: Failed to update organization admin groups',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return [
'success' => false,
- 'message' => 'Failed to update organization admin groups: ' . $e->getMessage()
+ 'message' => 'Failed to update organization admin groups: '.$e->getMessage(),
];
- }
- }
+ }//end try
+ }//end updateOrganizationAdminGroups()
/**
* Update super user groups with validation
*
- * @param array $groups Groups to set
- * @return array Update result with validation
+ * @param array $groups Groups to set.
+ *
+ * @return array Update result with validation.
*/
public function updateSuperUserGroups(array $groups): array
{
try {
- $validation = $this->validateGroups($groups);
+ $validation = $this->validateGroups(groups: $groups);
- if (!empty($validation['invalid'])) {
+ if (empty($validation['invalid']) === false) {
return [
- 'success' => false,
- 'message' => 'Invalid group names provided',
- 'validation' => $validation
+ 'success' => false,
+ 'message' => 'Invalid group names provided',
+ 'validation' => $validation,
];
}
- $this->setSuperUserGroups($validation['valid']);
+ $this->setSuperUserGroups(groups: $validation['valid']);
return [
'success' => true,
'message' => 'Super user groups updated successfully',
- 'groups' => $validation['valid']
+ 'groups' => $validation['valid'],
];
-
} catch (\Exception $e) {
- $this->logger->error('SettingsService: Failed to update super user groups', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'SettingsService: Failed to update super user groups',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return [
'success' => false,
- 'message' => 'Failed to update super user groups: ' . $e->getMessage()
+ 'message' => 'Failed to update super user groups: '.$e->getMessage(),
];
- }
- }
+ }//end try
+ }//end updateSuperUserGroups()
- // ========================================================================
- // FOCUSED ENDPOINT METHODS FOR PERFORMANCE OPTIMIZATION
- // ========================================================================
+ // ======================================================.
+ // FOCUSED ENDPOINT METHODS FOR PERFORMANCE OPTIMIZATION.
+ // ======================================================.
/**
* Get ArchiMate configuration only
@@ -4462,21 +4971,24 @@ public function getArchiMateConfig(): array
$status = $this->getArchiMateStatus();
return [
- 'success' => true,
- 'config' => $config,
- 'status' => $status,
- 'timestamp' => time()
+ 'success' => true,
+ 'config' => $config,
+ 'status' => $status,
+ 'timestamp' => time(),
];
} catch (\Exception $e) {
- $this->logger->error('Failed to get ArchiMate config', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to get ArchiMate config',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return [
'success' => false,
- 'message' => 'Failed to get ArchiMate config: ' . $e->getMessage()
+ 'message' => 'Failed to get ArchiMate config: '.$e->getMessage(),
];
- }
- }
+ }//end try
+ }//end getArchiMateConfig()
/**
* Update ArchiMate configuration
@@ -4488,24 +5000,27 @@ public function getArchiMateConfig(): array
public function updateArchiMateConfig(array $config): array
{
try {
- $this->setAmefConfig($config);
+ $this->setAmefConfig(config: $config);
return [
'success' => true,
'message' => 'ArchiMate configuration updated successfully',
- 'config' => $this->getAmefConfig()
+ 'config' => $this->getAmefConfig(),
];
} catch (\Exception $e) {
- $this->logger->error('Failed to update ArchiMate config', [
- 'exception' => $e->getMessage(),
- 'config' => $config
- ]);
+ $this->logger->error(
+ 'Failed to update ArchiMate config',
+ [
+ 'exception' => $e->getMessage(),
+ 'config' => $config,
+ ]
+ );
return [
'success' => false,
- 'message' => 'Failed to update ArchiMate config: ' . $e->getMessage()
+ 'message' => 'Failed to update ArchiMate config: '.$e->getMessage(),
];
- }
- }
+ }//end try
+ }//end updateArchiMateConfig()
/**
* Get email configuration only
@@ -4515,25 +5030,28 @@ public function updateArchiMateConfig(array $config): array
public function getEmailConfigFocused(): array
{
try {
- $emailSettings = $this->getEmailSettings();
+ $emailSettings = $this->getEmailSettings();
$emailTemplates = $this->getAllEmailTemplates();
return [
- 'success' => true,
- 'emailSettings' => $emailSettings,
+ 'success' => true,
+ 'emailSettings' => $emailSettings,
'emailTemplates' => $emailTemplates,
- 'timestamp' => time()
+ 'timestamp' => time(),
];
} catch (\Exception $e) {
- $this->logger->error('Failed to get email config', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to get email config',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return [
'success' => false,
- 'message' => 'Failed to get email config: ' . $e->getMessage()
+ 'message' => 'Failed to get email config: '.$e->getMessage(),
];
- }
- }
+ }//end try
+ }//end getEmailConfigFocused()
/**
* Update email configuration
@@ -4545,9 +5063,9 @@ public function getEmailConfigFocused(): array
public function updateEmailConfig(array $config): array
{
try {
- if (isset($config)) {
- $result = $this->updateEmailSettings($config);
- if (!$result['success']) {
+ if (isset($config) === true) {
+ $result = $this->updateEmailSettings(emailSettings: $config);
+ if ($result['success'] === false) {
return $result;
}
}
@@ -4555,19 +5073,22 @@ public function updateEmailConfig(array $config): array
return [
'success' => true,
'message' => 'Email configuration updated successfully',
- 'config' => $this->getEmailConfig()
+ 'config' => $this->getEmailConfig(),
];
} catch (\Exception $e) {
- $this->logger->error('Failed to update email config', [
- 'exception' => $e->getMessage(),
- 'config' => $config
- ]);
+ $this->logger->error(
+ 'Failed to update email config',
+ [
+ 'exception' => $e->getMessage(),
+ 'config' => $config,
+ ]
+ );
return [
'success' => false,
- 'message' => 'Failed to update email config: ' . $e->getMessage()
+ 'message' => 'Failed to update email config: '.$e->getMessage(),
];
- }
- }
+ }//end try
+ }//end updateEmailConfig()
/**
* Get AMEF configuration only
@@ -4580,20 +5101,23 @@ public function getAmefConfigFocused(): array
$config = $this->getAmefConfig();
return [
- 'success' => true,
- 'config' => $config,
- 'timestamp' => time()
+ 'success' => true,
+ 'config' => $config,
+ 'timestamp' => time(),
];
} catch (\Exception $e) {
- $this->logger->error('Failed to get AMEF config', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to get AMEF config',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return [
'success' => false,
- 'message' => 'Failed to get AMEF config: ' . $e->getMessage()
+ 'message' => 'Failed to get AMEF config: '.$e->getMessage(),
];
}
- }
+ }//end getAmefConfigFocused()
/**
* Update AMEF configuration
@@ -4605,21 +5129,28 @@ public function getAmefConfigFocused(): array
public function updateAmefConfig(array $config): array
{
try {
- // Remove framework routing keys
+ // Remove framework routing keys.
unset($config['_route']);
- // Load existing config to allow merging
+ // Load existing config to allow merging.
$existing = $this->getAmefConfig();
- if (!is_array($existing)) {
+ if (is_array($existing) === false) {
$existing = [];
}
- // Determine target register id
- $targetRegisterId = isset($config['register']) ? (string)$config['register']
- : (isset($existing['register']) ? (string)$existing['register'] : '');
+ // Determine target register id.
+ if (isset($config['register']) === true) {
+ $targetRegisterId = (string) $config['register'];
+ } else {
+ if (isset($existing['register']) === true) {
+ $targetRegisterId = (string) $existing['register'];
+ } else {
+ $targetRegisterId = '';
+ }
+ }
- // If a register is provided, validate that provided schema ids belong to that register
- // Only accept singular keys; ignore unknown keys silently
+ // If a register is provided, validate that provided schema ids belong to that register.
+ // Only accept singular keys; ignore unknown keys silently.
$allowedKeys = [
'organization_schema',
'element_schema',
@@ -4636,78 +5167,89 @@ public function updateAmefConfig(array $config): array
// Build a set of schema ids for the chosen register.
try {
$registerService = $this->getRegisterService();
- $registers = $registerService->findAll();
- $schemaIdSet = [];
+ $registers = $registerService->findAll();
+ $schemaIdSet = [];
foreach ($registers as $register) {
$register = $register->jsonSerialize();
- if ((string)($register['id'] ?? '') === $targetRegisterId) {
+ if ((string) ($register['id'] ?? '') === $targetRegisterId) {
foreach (($register['schemas'] ?? []) as $schema) {
- $schemaIdSet[(string)$schema['id']] = true;
+ $schemaIdSet[(string) $schema['id']] = true;
}
+
break;
}
}
} catch (\TypeError | \Exception $e) {
- $this->logger->warning('OpenRegister RegisterService->findAll() failed in updateAmefConfig', [
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine()
- ]);
- // Continue with empty schema set which will cause validation to fail gracefully
+ $this->logger->warning(
+ 'OpenRegister RegisterService->findAll() failed in updateAmefConfig',
+ [
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]
+ );
+ // Continue with empty schema set which will cause validation to fail gracefully.
$schemaIdSet = [];
- }
+ }//end try
- // Validate each provided schema id against the chosen register
+ // Validate each provided schema id against the chosen register.
foreach ($allowedKeys as $key) {
- if (array_key_exists($key, $config)) {
- $value = (string)$config[$key];
- if ($value !== '' && isset($schemaIdSet[$value])) {
+ if (array_key_exists($key, $config) === true) {
+ $value = (string) $config[$key];
+ if ($value !== '' && isset($schemaIdSet[$value]) === true) {
$validated[$key] = $value;
} else {
- // Skip invalid or cross-register ids
- $this->logger->warning('SettingsService: Ignored AMEF config key due to invalid schema/register combination', [
- 'key' => $key,
- 'value' => $value,
- 'register' => $targetRegisterId,
- ]);
+ // Skip invalid or cross-register ids.
+ $this->logger->warning(
+ 'SettingsService: Ignored AMEF config key due to invalid schema/register combination',
+ [
+ 'key' => $key,
+ 'value' => $value,
+ 'register' => $targetRegisterId,
+ ]
+ );
}
}
}
- }
- }
+ }//end if
+ }//end if
- // Merge: keep register and any validated schema keys; drop unknowns
+ // Merge: keep register and any validated schema keys; drop unknowns.
$merged = $existing;
if ($targetRegisterId !== '') {
$merged['register'] = $targetRegisterId;
}
+
foreach ($allowedKeys as $key) {
- if (array_key_exists($key, $validated)) {
+ if (array_key_exists($key, $validated) === true) {
$merged[$key] = $validated[$key];
- } elseif (!array_key_exists($key, $merged)) {
- // Ensure key presence with empty string for frontend mapping stability
+ } else if (array_key_exists($key, $merged) === false) {
+ // Ensure key presence with empty string for frontend mapping stability.
$merged[$key] = '';
}
}
- $this->setAmefConfig($merged);
+ $this->setAmefConfig(config: $merged);
return [
'success' => true,
'message' => 'AMEF configuration updated successfully',
- 'config' => $this->getAmefConfig()
+ 'config' => $this->getAmefConfig(),
];
} catch (\Exception $e) {
- $this->logger->error('Failed to update AMEF config', [
- 'exception' => $e->getMessage(),
- 'config' => $config
- ]);
+ $this->logger->error(
+ 'Failed to update AMEF config',
+ [
+ 'exception' => $e->getMessage(),
+ 'config' => $config,
+ ]
+ );
return [
'success' => false,
- 'message' => 'Failed to update AMEF config: ' . $e->getMessage()
+ 'message' => 'Failed to update AMEF config: '.$e->getMessage(),
];
- }
- }
+ }//end try
+ }//end updateAmefConfig()
/**
* Get Voorzieningen configuration only
@@ -4720,20 +5262,23 @@ public function getVoorzieningenConfigFocused(): array
$config = $this->getVoorzieningenConfig();
return [
- 'success' => true,
- 'config' => $config,
- 'timestamp' => time()
+ 'success' => true,
+ 'config' => $config,
+ 'timestamp' => time(),
];
} catch (\Exception $e) {
- $this->logger->error('Failed to get Voorzieningen config', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to get Voorzieningen config',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return [
'success' => false,
- 'message' => 'Failed to get Voorzieningen config: ' . $e->getMessage()
+ 'message' => 'Failed to get Voorzieningen config: '.$e->getMessage(),
];
}
- }
+ }//end getVoorzieningenConfigFocused()
/**
* Update Voorzieningen configuration
@@ -4745,24 +5290,27 @@ public function getVoorzieningenConfigFocused(): array
public function updateVoorzieningenConfig(array $config): array
{
try {
- $this->setVoorzieningenConfig($config);
+ $this->setVoorzieningenConfig(config: $config);
return [
'success' => true,
'message' => 'Voorzieningen configuration updated successfully',
- 'config' => $this->getVoorzieningenConfig()
+ 'config' => $this->getVoorzieningenConfig(),
];
} catch (\Exception $e) {
- $this->logger->error('Failed to update Voorzieningen config', [
- 'exception' => $e->getMessage(),
- 'config' => $config
- ]);
+ $this->logger->error(
+ 'Failed to update Voorzieningen config',
+ [
+ 'exception' => $e->getMessage(),
+ 'config' => $config,
+ ]
+ );
return [
'success' => false,
- 'message' => 'Failed to update Voorzieningen config: ' . $e->getMessage()
+ 'message' => 'Failed to update Voorzieningen config: '.$e->getMessage(),
];
- }
- }
+ }//end try
+ }//end updateVoorzieningenConfig()
/**
* Get object counts only (lightweight)
@@ -4774,24 +5322,27 @@ public function getObjectsCounts(): array
try {
$counts = [
'voorzieningen' => $this->getVoorzieningenObjectCounts(),
- 'amef' => $this->getAmefObjectCounts(),
- 'timestamp' => time()
+ 'amef' => $this->getAmefObjectCounts(),
+ 'timestamp' => time(),
];
return [
'success' => true,
- 'counts' => $counts
+ 'counts' => $counts,
];
} catch (\Exception $e) {
- $this->logger->error('Failed to get object counts', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to get object counts',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return [
'success' => false,
- 'message' => 'Failed to get object counts: ' . $e->getMessage()
+ 'message' => 'Failed to get object counts: '.$e->getMessage(),
];
- }
- }
+ }//end try
+ }//end getObjectsCounts()
/**
* Get object statistics (full statistics with configuration)
@@ -4804,19 +5355,22 @@ public function getObjectsStatistics(): array
$statistics = $this->getObjectCountsStatistics();
return [
- 'success' => true,
- 'statistics' => $statistics
+ 'success' => true,
+ 'statistics' => $statistics,
];
} catch (\Exception $e) {
- $this->logger->error('Failed to get object statistics', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to get object statistics',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return [
'success' => false,
- 'message' => 'Failed to get object statistics: ' . $e->getMessage()
+ 'message' => 'Failed to get object statistics: '.$e->getMessage(),
];
}
- }
+ }//end getObjectsStatistics()
/**
* Get user groups configuration only
@@ -4827,27 +5381,30 @@ public function getUserGroupsConfig(): array
{
try {
$config = [
- 'generic' => $this->getGenericUserGroups(),
+ 'generic' => $this->getGenericUserGroups(),
'organizationAdmin' => $this->getOrganizationAdminGroups(),
- 'superUser' => $this->getSuperUserGroups(),
- 'allGroups' => $this->getAllGroups()
+ 'superUser' => $this->getSuperUserGroups(),
+ 'allGroups' => $this->getAllGroups(),
];
return [
- 'success' => true,
- 'config' => $config,
- 'timestamp' => time()
+ 'success' => true,
+ 'config' => $config,
+ 'timestamp' => time(),
];
} catch (\Exception $e) {
- $this->logger->error('Failed to get user groups config', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to get user groups config',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return [
'success' => false,
- 'message' => 'Failed to get user groups config: ' . $e->getMessage()
+ 'message' => 'Failed to get user groups config: '.$e->getMessage(),
];
- }
- }
+ }//end try
+ }//end getUserGroupsConfig()
/**
* Update user groups configuration
@@ -4861,47 +5418,53 @@ public function updateUserGroupsConfig(array $config): array
try {
$results = [];
- if (isset($config['generic'])) {
- $results['generic'] = $this->updateGenericUserGroups($config['generic']);
+ if (isset($config['generic']) === true) {
+ $results['generic'] = $this->updateGenericUserGroups(groups: $config['generic']);
}
- if (isset($config['organizationAdmin'])) {
- $results['organizationAdmin'] = $this->updateOrganizationAdminGroups($config['organizationAdmin']);
+ if (isset($config['organizationAdmin']) === true) {
+ $results['organizationAdmin'] = $this->updateOrganizationAdminGroups(groups: $config['organizationAdmin']);
}
- if (isset($config['superUser'])) {
- $results['superUser'] = $this->updateSuperUserGroups($config['superUser']);
+ if (isset($config['superUser']) === true) {
+ $results['superUser'] = $this->updateSuperUserGroups(groups: $config['superUser']);
}
- // Check if any updates failed
- $failed = array_filter($results, function($result) {
- return !$result['success'];
- });
+ // Check if any updates failed.
+ $failed = array_filter(
+ $results,
+ function ($result) {
+ return $result['success'] === false;
+ }
+ );
- if (!empty($failed)) {
+ if (empty($failed) === false) {
return [
'success' => false,
'message' => 'Some user group updates failed',
- 'results' => $results
+ 'results' => $results,
];
}
return [
'success' => true,
'message' => 'User groups configuration updated successfully',
- 'config' => $this->getUserGroupsConfig()
+ 'config' => $this->getUserGroupsConfig(),
];
} catch (\Exception $e) {
- $this->logger->error('Failed to update user groups config', [
- 'exception' => $e->getMessage(),
- 'config' => $config
- ]);
+ $this->logger->error(
+ 'Failed to update user groups config',
+ [
+ 'exception' => $e->getMessage(),
+ 'config' => $config,
+ ]
+ );
return [
'success' => false,
- 'message' => 'Failed to update user groups config: ' . $e->getMessage()
+ 'message' => 'Failed to update user groups config: '.$e->getMessage(),
];
- }
- }
+ }//end try
+ }//end updateUserGroupsConfig()
/**
* Get catalog location
@@ -4911,18 +5474,19 @@ public function updateUserGroupsConfig(array $config): array
public function getCatalogLocation(): string
{
return $this->config->getValueString($this->_appName, 'catalog_location', '');
- }
+ }//end getCatalogLocation()
/**
* Set catalog location
*
- * @param string $location The catalog location URL
+ * @param string $location The catalog location URL.
+ *
* @return void
*/
public function setCatalogLocation(string $location): void
{
$this->config->setValueString($this->_appName, 'catalog_location', $location);
- }
+ }//end setCatalogLocation()
/**
* High-performance sync of OpenRegister organisations to voorzieningen register
@@ -4931,147 +5495,186 @@ public function setCatalogLocation(string $location): void
* Uses OpenRegister's ultraFastBulkSave for maximum performance.
*
* @param array $options Configuration options:
- * - batch_size: Number of organisations per batch (default: 500)
- * - dry_run: Only check what would be created (default: false)
+ * - batch_size: Number of organisations per batch (default: 500)
+ * - dry_run: Only check what would be created (default: false)
*
* @return array Sync results with performance metrics
*/
- public function syncOrganisationsToVoorzieningenOptimized(array $options = []): array
+ public function syncOrganisationsToVoorzieningenOptimized(array $options=[]): array
{
$startTime = microtime(true);
$batchSize = $options['batch_size'] ?? 500;
- $isDryRun = $options['dry_run'] ?? false;
+ $isDryRun = $options['dry_run'] ?? false;
try {
- $this->logger->info('Starting optimized organisation sync', [
- 'batch_size' => $batchSize,
- 'dry_run' => $isDryRun
- ]);
+ $this->logger->info(
+ 'Starting optimized organisation sync',
+ [
+ 'batch_size' => $batchSize,
+ 'dry_run' => $isDryRun,
+ ]
+ );
- // 1. Validate prerequisites
+ // 1. Validate prerequisites.
$objectService = $this->getObjectService();
if ($objectService === null) {
return ['success' => false, 'message' => 'OpenRegister service not available'];
}
$voorzieningenConfig = $this->getVoorzieningenConfig();
- if (empty($voorzieningenConfig['register']) || empty($voorzieningenConfig['organisatie_schema'])) {
+ if (empty($voorzieningenConfig['register']) === true || empty($voorzieningenConfig['organisatie_schema']) === true) {
return ['success' => false, 'message' => 'Voorzieningen register or organisatie schema not configured'];
}
- $this->logger->debug('Prerequisites validated', [
- 'register_id' => $voorzieningenConfig['register'],
- 'organisatie_schema_id' => $voorzieningenConfig['organisatie_schema']
- ]);
+ $this->logger->debug(
+ 'Prerequisites validated',
+ [
+ 'register_id' => $voorzieningenConfig['register'],
+ 'organisatie_schema_id' => $voorzieningenConfig['organisatie_schema'],
+ ]
+ );
- // 2. BULK FETCH: Get all organisations in one query
+ // 2. BULK FETCH: Get all organisations in one query.
$organisationMapper = $this->container->get(\OCA\OpenRegister\Db\OrganisationMapper::class);
- $allOrganisations = $organisationMapper->findAllWithUserCount();
+ $allOrganisations = $organisationMapper->findAllWithUserCount();
- $this->logger->info('Retrieved organisations from OpenRegister', [
- 'total_organisations' => count($allOrganisations)
- ]);
+ $this->logger->info(
+ 'Retrieved organisations from OpenRegister',
+ [
+ 'total_organisations' => count($allOrganisations),
+ ]
+ );
- // 3. BULK FETCH: Get existing organisaties in one query
+ // 3. BULK FETCH: Get existing organisaties in one query.
$existingOrganisaties = $objectService->searchObjectsPaginated(
query: [
- '@self' => [
+ '@self' => [
'register' => $voorzieningenConfig['register'],
- 'schema' => $voorzieningenConfig['organisatie_schema']
+ 'schema' => $voorzieningenConfig['organisatie_schema'],
],
- '_limit' => 10000 // Get all existing
+ '_limit' => 10000,
+ // Get all existing.
],
_rbac: false,
_multitenancy: false
);
- $this->logger->info('Retrieved existing organisaties from voorzieningen register', [
- 'existing_count' => count($existingOrganisaties['results'] ?? [])
- ]);
+ $this->logger->info(
+ 'Retrieved existing organisaties from voorzieningen register',
+ [
+ 'existing_count' => count($existingOrganisaties['results'] ?? []),
+ ]
+ );
+
+ // 4. MEMORY-EFFICIENT: Build lookup set for existing UUIDs.
+ // Now we can compare by UUID since we force UUIDs to match OpenRegister organisation UUIDs.
+ $existingUuids = array_flip(
+ array_map(
+ function ($org) {
+ if ($org instanceof \OCA\OpenRegister\Db\ObjectEntity) {
+ return $org->getUuid() ?? '';
+ }
- // 4. MEMORY-EFFICIENT: Build lookup set for existing UUIDs
- // Now we can compare by UUID since we force UUIDs to match OpenRegister organisation UUIDs
- $existingUuids = array_flip(array_map(function($org) {
- return $org['@self']['id'] ?? '';
- }, $existingOrganisaties['results'] ?? []));
+ return $org['@self']['id'] ?? '';
+ },
+ $existingOrganisaties['results'] ?? []
+ )
+ );
- $this->logger->debug('Deduplication analysis', [
- 'existing_uuids_count' => count($existingUuids),
- 'existing_uuids_sample' => array_slice(array_keys($existingUuids), 0, 3),
- 'total_openregister_orgs' => count($allOrganisations)
- ]);
+ $this->logger->debug(
+ 'Deduplication analysis',
+ [
+ 'existing_uuids_count' => count($existingUuids),
+ 'existing_uuids_sample' => array_slice(array_keys($existingUuids), 0, 3),
+ 'total_openregister_orgs' => count($allOrganisations),
+ ]
+ );
- // 5. BATCH PREPARATION: Filter and prepare objects for bulk creation
+ // 5. BATCH PREPARATION: Filter and prepare objects for bulk creation.
$organisationsToCreate = [];
- $skippedCount = 0;
+ $skippedCount = 0;
foreach ($allOrganisations as $organisation) {
$orgUuid = $organisation->getUuid();
- // DEBUG: Log first few comparisons
+ // DEBUG: Log first few comparisons.
if (count($organisationsToCreate) < 3) {
- $this->logger->debug('UUID comparison debug', [
- 'openregister_uuid' => $orgUuid,
- 'exists_in_voorzieningen' => isset($existingUuids[$orgUuid]),
- 'organisation_name' => $organisation->getName()
- ]);
+ $this->logger->debug(
+ 'UUID comparison debug',
+ [
+ 'openregister_uuid' => $orgUuid,
+ 'exists_in_voorzieningen' => isset($existingUuids[$orgUuid]) === true,
+ 'organisation_name' => $organisation->getName(),
+ ]
+ );
}
- // Skip if already exists (compare by UUID now that we force UUIDs)
- if (isset($existingUuids[$orgUuid])) {
+ // Skip if already exists (compare by UUID now that we force UUIDs).
+ if (isset($existingUuids[$orgUuid]) === true) {
$skippedCount++;
continue;
}
- // Prepare organisatie data with forced UUID
+ // Prepare organisatie data with forced UUID.
+ if ($organisation->getActive() === true) {
+ $statusValue = 'Actief';
+ } else {
+ $statusValue = 'Inactief';
+ }
+
$organisationsToCreate[] = [
- 'id' => $orgUuid, // Force the UUID to match OpenRegister organisation UUID
- '@self' => [
- 'id' => $orgUuid, // Also set in @self section for consistency
- 'uuid' => $orgUuid
+ 'id' => $orgUuid,
+ // Force the UUID to match OpenRegister organisation UUID.
+ '@self' => [
+ 'id' => $orgUuid,
+ // Also set in @self section for consistency.
+ 'uuid' => $orgUuid,
],
- 'naam' => $organisation->getName(),
- 'beschrijving' => $organisation->getDescription() ?? '',
- 'type' => $this->determineOrganisationType($organisation),
- 'status' => $organisation->getActive() ? 'Actief' : 'Inactief',
- 'website' => '',
- 'e-mailadres' => null,
- 'telefoonnummer' => null,
- 'oin' => '',
- 'cbs' => '',
- 'deelnemers' => [],
+ 'naam' => $organisation->getName(),
+ 'beschrijving' => $organisation->getDescription() ?? '',
+ 'type' => $this->determineOrganisationType(organisation: $organisation),
+ 'status' => $statusValue,
+ 'website' => '',
+ 'e-mailadres' => null,
+ 'telefoonnummer' => null,
+ 'oin' => '',
+ 'cbs' => '',
+ 'deelnemers' => [],
'contactpersonen' => [],
];
- }
+ }//end foreach
$results = [
'total_organisations' => count($allOrganisations),
- 'existing_count' => count($existingUuids),
- 'to_create_count' => count($organisationsToCreate),
- 'created_count' => 0,
- 'failed_count' => 0,
- 'batches_processed' => 0,
- 'performance' => []
+ 'existing_count' => count($existingUuids),
+ 'to_create_count' => count($organisationsToCreate),
+ 'created_count' => 0,
+ 'failed_count' => 0,
+ 'batches_processed' => 0,
+ 'performance' => [],
];
- $this->logger->info('Organisation analysis completed', [
- 'total' => $results['total_organisations'],
- 'existing' => $results['existing_count'],
- 'to_create' => $results['to_create_count'],
- 'skipped_count' => $skippedCount,
- 'deduplication_working' => $skippedCount > 0
- ]);
+ $this->logger->info(
+ 'Organisation analysis completed',
+ [
+ 'total' => $results['total_organisations'],
+ 'existing' => $results['existing_count'],
+ 'to_create' => $results['to_create_count'],
+ 'skipped_count' => $skippedCount,
+ 'deduplication_working' => $skippedCount > 0,
+ ]
+ );
- if ($isDryRun) {
+ if (empty($isDryRun) === false) {
$results['message'] = "DRY RUN: Would create {$results['to_create_count']} organisations";
return ['success' => true, 'results' => $results];
}
- if (empty($organisationsToCreate)) {
+ if (empty($organisationsToCreate) === true) {
$results['message'] = 'All organisations already exist in voorzieningen register';
return ['success' => true, 'results' => $results];
}
- // 6. ULTRA-FAST BULK PROCESSING: Process in optimized batches
+ // 6. ULTRA-FAST BULK PROCESSING: Process in optimized batches.
$objectService->setRegister($voorzieningenConfig['register']);
$objectService->setSchema($voorzieningenConfig['organisatie_schema']);
@@ -5081,110 +5684,142 @@ public function syncOrganisationsToVoorzieningenOptimized(array $options = []):
$batchStartTime = microtime(true);
try {
- $this->logger->debug('Processing batch', [
- 'batch' => $batchIndex + 1,
- 'total_batches' => count($batches),
- 'objects_in_batch' => count($batch)
- ]);
+ $this->logger->debug(
+ 'Processing batch',
+ [
+ 'batch' => $batchIndex + 1,
+ 'total_batches' => count($batches),
+ 'objects_in_batch' => count($batch),
+ ]
+ );
- // BULK OPERATION: Create entire batch in single operation
+ // BULK OPERATION: Create entire batch in single operation.
$bulkResult = $objectService->saveObjects(
objects: $batch,
register: $voorzieningenConfig['register'],
schema: $voorzieningenConfig['organisatie_schema'],
_rbac: false,
_multitenancy: false,
- validation: false, // Skip validation for performance
- events: false // Skip events for performance
+ validation: false,
+ // Skip validation for performance.
+ events: false
+ // Skip events for performance.
);
- $batchTime = microtime(true) - $batchStartTime;
+ $batchTime = microtime(true) - $batchStartTime;
$objectsPerSecond = count($batch) / $batchTime;
$results['created_count'] += $bulkResult['statistics']['saved'] ?? 0;
- $results['failed_count'] += $bulkResult['statistics']['errors'] ?? 0;
+ $results['failed_count'] += $bulkResult['statistics']['errors'] ?? 0;
$results['batches_processed']++;
$results['performance'][] = [
- 'batch' => $batchIndex + 1,
- 'objects' => count($batch),
- 'time_seconds' => round($batchTime, 3),
- 'objects_per_second' => round($objectsPerSecond, 0)
+ 'batch' => $batchIndex + 1,
+ 'objects' => count($batch),
+ 'time_seconds' => round($batchTime, 3),
+ 'objects_per_second' => round($objectsPerSecond, 0),
];
- $this->logger->info("Bulk organisation sync batch completed", [
- 'batch' => $batchIndex + 1,
- 'total_batches' => count($batches),
- 'objects_in_batch' => count($batch),
- 'objects_per_second' => round($objectsPerSecond, 0)
- ]);
-
+ $this->logger->info(
+ "Bulk organisation sync batch completed",
+ [
+ 'batch' => $batchIndex + 1,
+ 'total_batches' => count($batches),
+ 'objects_in_batch' => count($batch),
+ 'objects_per_second' => round($objectsPerSecond, 0),
+ ]
+ );
} catch (\Exception $e) {
$results['failed_count'] += count($batch);
- $this->logger->error("Bulk organisation sync batch failed", [
- 'batch' => $batchIndex + 1,
- 'error' => $e->getMessage(),
- 'objects_in_batch' => count($batch)
- ]);
- }
- }
+ $this->logger->error(
+ "Bulk organisation sync batch failed",
+ [
+ 'batch' => $batchIndex + 1,
+ 'error' => $e->getMessage(),
+ 'objects_in_batch' => count($batch),
+ ]
+ );
+ }//end try
+ }//end foreach
$totalTime = microtime(true) - $startTime;
- $overallPerformance = $results['created_count'] > 0 ? $results['created_count'] / $totalTime : 0;
+ if ($results['created_count'] > 0) {
+ $overallPerformance = $results['created_count'] / $totalTime;
+ } else {
+ $overallPerformance = 0;
+ }
+
+ if ($overallPerformance > 10) {
+ $estimatedImprovementValue = round($overallPerformance / 10, 1).'x faster than individual operations';
+ } else {
+ $estimatedImprovementValue = 'baseline';
+ }
+
+ $createdCount = $results['created_count'];
+ $existingCount = $results['existing_count'];
+ $failedCount = $results['failed_count'];
+ $syncMessage = "Sync completed: {$createdCount} created, {$existingCount} existing, {$failedCount} failed";
return [
'success' => true,
- 'message' => "Sync completed: {$results['created_count']} created, {$results['existing_count']} existing, {$results['failed_count']} failed",
- 'results' => array_merge($results, [
- 'total_time_seconds' => round($totalTime, 3),
- 'overall_objects_per_second' => round($overallPerformance, 0),
- 'estimated_improvement' => $overallPerformance > 10 ? round($overallPerformance / 10, 1) . 'x faster than individual operations' : 'baseline'
- ])
+ 'message' => $syncMessage,
+ 'results' => array_merge(
+ $results,
+ [
+ 'total_time_seconds' => round($totalTime, 3),
+ 'overall_objects_per_second' => round($overallPerformance, 0),
+ 'estimated_improvement' => $estimatedImprovementValue,
+ ]
+ ),
];
-
} catch (\Exception $e) {
- $this->logger->error('Organisation sync failed', [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->logger->error(
+ 'Organisation sync failed',
+ [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return [
'success' => false,
- 'message' => 'Organisation sync failed: ' . $e->getMessage(),
- 'error' => $e->getMessage()
+ 'message' => 'Organisation sync failed: '.$e->getMessage(),
+ 'error' => $e->getMessage(),
];
- }
- }
+ }//end try
+ }//end syncOrganisationsToVoorzieningenOptimized()
/**
* Determine organisation type based on organisation properties
*
- * @param \OCA\OpenRegister\Db\Organisation $organisation The organisation entity
- * @return string The organisation type
+ * @param \OCA\OpenRegister\Db\Organisation $organisation The organisation entity.
+ *
+ * @return string The organisation type.
*/
private function determineOrganisationType(\OCA\OpenRegister\Db\Organisation $organisation): string
{
- $name = strtolower($organisation->getName());
+ $name = strtolower($organisation->getName() === true);
if (strpos($name, 'gemeente') !== false) {
return 'Gemeente';
- } elseif (strpos($name, 'provincie') !== false) {
+ } else if (strpos($name, 'provincie') !== false) {
return 'Provincie';
- } elseif (strpos($name, 'ministerie') !== false) {
+ } else if (strpos($name, 'ministerie') !== false) {
return 'Ministerie';
} else {
- return 'Leverancier'; // Default
+ return 'Leverancier';
+ // Default.
}
- }
+ }//end determineOrganisationType()
- // ========================================================================
- // CRONJOB CONFIGURATION METHODS
- // ========================================================================
+ // ======================================================.
+ // CRONJOB CONFIGURATION METHODS.
+ // ======================================================.
/**
* Get all cronjob configurations.
*
- * Returns configuration for all registered cronjobs including their
- * user and organisation context settings.
+ * @deprecated Cronjob context is no longer needed since sync operations use _rbac: false.
+ * Will be removed in a future version.
*
* @return array The cronjob configurations indexed by job name
*/
@@ -5192,9 +5827,9 @@ public function getCronjobConfig(): array
{
try {
$configJson = $this->config->getValueString($this->_appName, 'cronjob_config', '{}');
- $config = json_decode($configJson, true);
+ $config = json_decode($configJson, true);
- if (!is_array($config)) {
+ if (is_array($config) === false) {
$config = [];
}
@@ -5205,65 +5840,74 @@ public function getCronjobConfig(): array
$result = [];
foreach ($availableCronjobs as $jobId => $jobMeta) {
$result[$jobId] = [
- 'id' => $jobId,
- 'name' => $jobMeta['name'],
- 'description' => $jobMeta['description'],
- 'interval' => $jobMeta['interval'],
- 'userId' => $config[$jobId]['userId'] ?? null,
+ 'id' => $jobId,
+ 'name' => $jobMeta['name'],
+ 'description' => $jobMeta['description'],
+ 'interval' => $jobMeta['interval'],
+ 'userId' => $config[$jobId]['userId'] ?? null,
'organisationUuid' => $config[$jobId]['organisationUuid'] ?? null,
- 'enabled' => $config[$jobId]['enabled'] ?? true,
+ 'enabled' => $config[$jobId]['enabled'] ?? true,
];
}
return [
- 'success' => true,
- 'cronjobs' => $result,
- 'timestamp' => time()
+ 'success' => true,
+ 'cronjobs' => $result,
+ 'timestamp' => time(),
];
-
} catch (\Exception $e) {
- $this->logger->error('Failed to get cronjob config', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to get cronjob config',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return [
- 'success' => false,
- 'message' => 'Failed to get cronjob config: ' . $e->getMessage(),
- 'cronjobs' => []
+ 'success' => false,
+ 'message' => 'Failed to get cronjob config: '.$e->getMessage(),
+ 'cronjobs' => [],
];
- }
- }
+ }//end try
+ }//end getCronjobConfig()
/**
* Get list of available cronjobs with their metadata.
*
+ * @deprecated Cronjob context is no longer needed since sync operations use _rbac: false.
+ *
* @return array List of cronjob definitions
*/
private function getAvailableCronjobs(): array
{
return [
'organization_contact_sync' => [
- 'name' => 'Organization Contact Sync',
+ 'name' => 'Organization Contact Sync',
'description' => 'Synchronizes organizations and contact persons between SoftwareCatalog objects and OpenRegister entities.',
- 'interval' => 300, // 5 minutes
- 'class' => 'OCA\\SoftwareCatalog\\BackgroundJob\\OrganizationContactSyncJob',
+ 'interval' => 300,
+ // 5 minutes.
+ 'class' => 'OCA\\SoftwareCatalog\\BackgroundJob\\OrganizationContactSyncJob',
],
];
- }
+ }//end getAvailableCronjobs()
/**
* Update cronjob configuration.
*
- * @param array $data The cronjob configuration data
- * @return array Result of the update operation
+ * @param array $data The cronjob configuration data.
+ *
+ * @return array Result of the update operation.
+ *
+ * @deprecated Cronjob context is no longer needed since sync operations use _rbac: false.
+ * Will be removed in a future version.
*/
public function updateCronjobConfig(array $data): array
{
try {
// Get existing config.
$configJson = $this->config->getValueString($this->_appName, 'cronjob_config', '{}');
- $config = json_decode($configJson, true);
+ $config = json_decode($configJson, true);
- if (!is_array($config)) {
+ if (is_array($config) === false) {
$config = [];
}
@@ -5272,24 +5916,24 @@ public function updateCronjobConfig(array $data): array
if ($jobId === null) {
return [
'success' => false,
- 'message' => 'Job ID is required'
+ 'message' => 'Job ID is required',
];
}
// Validate that the job exists.
$availableCronjobs = $this->getAvailableCronjobs();
- if (!isset($availableCronjobs[$jobId])) {
+ if (isset($availableCronjobs[$jobId]) === false) {
return [
'success' => false,
- 'message' => 'Unknown cronjob: ' . $jobId
+ 'message' => 'Unknown cronjob: '.$jobId,
];
}
// Update the config for this job.
$config[$jobId] = [
- 'userId' => $data['userId'] ?? null,
+ 'userId' => $data['userId'] ?? null,
'organisationUuid' => $data['organisationUuid'] ?? null,
- 'enabled' => $data['enabled'] ?? true,
+ 'enabled' => $data['enabled'] ?? true,
];
// Save the updated config.
@@ -5299,81 +5943,90 @@ public function updateCronjobConfig(array $data): array
json_encode($config, JSON_PRETTY_PRINT)
);
- $this->logger->info('Cronjob configuration updated', [
- 'jobId' => $jobId,
- 'userId' => $config[$jobId]['userId'],
- 'organisationUuid' => $config[$jobId]['organisationUuid']
- ]);
+ $this->logger->info(
+ 'Cronjob configuration updated',
+ [
+ 'jobId' => $jobId,
+ 'userId' => $config[$jobId]['userId'],
+ 'organisationUuid' => $config[$jobId]['organisationUuid'],
+ ]
+ );
return [
'success' => true,
'message' => 'Cronjob configuration updated successfully',
- 'config' => $config[$jobId]
+ 'config' => $config[$jobId],
];
-
} catch (\Exception $e) {
- $this->logger->error('Failed to update cronjob config', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to update cronjob config',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return [
'success' => false,
- 'message' => 'Failed to update cronjob config: ' . $e->getMessage()
+ 'message' => 'Failed to update cronjob config: '.$e->getMessage(),
];
- }
- }
+ }//end try
+ }//end updateCronjobConfig()
/**
* Get cronjob context for a specific job.
*
- * This is used by the cronjob itself to get its configured user and organisation.
+ * @param string $jobId The cronjob identifier.
+ *
+ * @return array|null The context configuration or null if not configured.
*
- * @param string $jobId The cronjob identifier
- * @return array|null The context configuration or null if not configured
+ * @deprecated Cronjob context is no longer needed since sync operations use _rbac: false.
+ * Will be removed in a future version.
*/
public function getCronjobContext(string $jobId): ?array
{
try {
$configJson = $this->config->getValueString($this->_appName, 'cronjob_config', '{}');
- $config = json_decode($configJson, true);
+ $config = json_decode($configJson, true);
- if (!is_array($config) || !isset($config[$jobId])) {
+ if (is_array($config) === false || isset($config[$jobId]) === false) {
return null;
}
$jobConfig = $config[$jobId];
// Only return if both user and organisation are configured.
- if (empty($jobConfig['userId']) || empty($jobConfig['organisationUuid'])) {
+ if (empty($jobConfig['userId']) === true || empty($jobConfig['organisationUuid']) === true) {
return null;
}
return [
- 'userId' => $jobConfig['userId'],
+ 'userId' => $jobConfig['userId'],
'organisationUuid' => $jobConfig['organisationUuid'],
- 'enabled' => $jobConfig['enabled'] ?? true,
+ 'enabled' => $jobConfig['enabled'] ?? true,
];
-
} catch (\Exception $e) {
- $this->logger->error('Failed to get cronjob context', [
- 'jobId' => $jobId,
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to get cronjob context',
+ [
+ 'jobId' => $jobId,
+ 'exception' => $e->getMessage(),
+ ]
+ );
return null;
- }
- }
+ }//end try
+ }//end getCronjobContext()
/**
* Get available users for cronjob configuration.
*
- * Returns a list of users that can be selected for running cronjobs.
- * Typically limited to admin users or users in specific groups.
+ * @deprecated Cronjob context is no longer needed since sync operations use _rbac: false.
+ * Will be removed in a future version.
*
* @return array List of users with id and display name
*/
public function getAvailableUsersForCronjobs(): array
{
try {
- $userManager = $this->container->get(\OCP\IUserManager::class);
+ $userManager = $this->container->get(\OCP\IUserManager::class);
$groupManager = $this->container->get(\OCP\IGroupManager::class);
$users = [];
@@ -5383,9 +6036,9 @@ public function getAvailableUsersForCronjobs(): array
if ($adminGroup !== null) {
foreach ($adminGroup->getUsers() as $user) {
$users[] = [
- 'id' => $user->getUID(),
+ 'id' => $user->getUID(),
'displayName' => $user->getDisplayName(),
- 'email' => $user->getEMailAddress(),
+ 'email' => $user->getEMailAddress(),
];
}
}
@@ -5398,11 +6051,11 @@ public function getAvailableUsersForCronjobs(): array
foreach ($group->getUsers() as $user) {
// Avoid duplicates.
$exists = array_filter($users, fn($u) => $u['id'] === $user->getUID());
- if (empty($exists)) {
+ if (empty($exists) === true) {
$users[] = [
- 'id' => $user->getUID(),
+ 'id' => $user->getUID(),
'displayName' => $user->getDisplayName(),
- 'email' => $user->getEMailAddress(),
+ 'email' => $user->getEMailAddress(),
];
}
}
@@ -5411,36 +6064,39 @@ public function getAvailableUsersForCronjobs(): array
return [
'success' => true,
- 'users' => $users
+ 'users' => $users,
];
-
} catch (\Exception $e) {
- $this->logger->error('Failed to get available users for cronjobs', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to get available users for cronjobs',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return [
'success' => false,
- 'message' => 'Failed to get available users: ' . $e->getMessage(),
- 'users' => []
+ 'message' => 'Failed to get available users: '.$e->getMessage(),
+ 'users' => [],
];
- }
- }
+ }//end try
+ }//end getAvailableUsersForCronjobs()
/**
* Get available organisations for cronjob configuration.
*
- * Returns a list of organisations that can be selected for running cronjobs.
+ * @deprecated Cronjob context is no longer needed since sync operations use _rbac: false.
+ * Will be removed in a future version.
*
* @return array List of organisations with uuid and name
*/
public function getAvailableOrganisationsForCronjobs(): array
{
try {
- if (!in_array('openregister', $this->appManager->getInstalledApps())) {
+ if (in_array('openregister', $this->appManager->getInstalledApps()) === false) {
return [
- 'success' => false,
- 'message' => 'OpenRegister is not installed',
- 'organisations' => []
+ 'success' => false,
+ 'message' => 'OpenRegister is not installed',
+ 'organisations' => [],
];
}
@@ -5452,27 +6108,28 @@ public function getAvailableOrganisationsForCronjobs(): array
$result = [];
foreach ($organisations as $org) {
$result[] = [
- 'uuid' => $org->getUuid(),
- 'name' => $org->getName(),
+ 'uuid' => $org->getUuid(),
+ 'name' => $org->getName(),
'description' => $org->getDescription(),
];
}
return [
- 'success' => true,
- 'organisations' => $result
+ 'success' => true,
+ 'organisations' => $result,
];
-
} catch (\Exception $e) {
- $this->logger->error('Failed to get available organisations for cronjobs', [
- 'exception' => $e->getMessage()
- ]);
+ $this->logger->error(
+ 'Failed to get available organisations for cronjobs',
+ [
+ 'exception' => $e->getMessage(),
+ ]
+ );
return [
- 'success' => false,
- 'message' => 'Failed to get available organisations: ' . $e->getMessage(),
- 'organisations' => []
+ 'success' => false,
+ 'message' => 'Failed to get available organisations: '.$e->getMessage(),
+ 'organisations' => [],
];
- }
- }
-
-}
+ }//end try
+ }//end getAvailableOrganisationsForCronjobs()
+}//end class
diff --git a/lib/Service/SoftwareCatalogue/ContactPersonHandler.php b/lib/Service/SoftwareCatalogue/ContactPersonHandler.php
index 8276a969..73aef9b4 100644
--- a/lib/Service/SoftwareCatalogue/ContactPersonHandler.php
+++ b/lib/Service/SoftwareCatalogue/ContactPersonHandler.php
@@ -10,7 +10,6 @@
* @package OCA\SoftwareCatalog\Service\SoftwareCatalogue
* @author Conduction b.v.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
@@ -31,13 +30,12 @@
use OCA\SoftwareCatalog\Service\SymfonyEmailService;
/**
- * Handler for contact person-related operations
+ * Handler for contact person-related operations.
*
* @category Handler
* @package OCA\SoftwareCatalog\Service\SoftwareCatalogue
* @author Conduction b.v.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
class ContactPersonHandler
@@ -45,28 +43,28 @@ class ContactPersonHandler
/**
* ContactPersonHandler constructor
*
- * @param IUserManager $_userManager User manager interface
- * @param ISecureRandom $_secureRandom Secure random generator
- * @param IGroupManager $_groupManager Group manager interface
- * @param IAppConfig $_config Config interface
- * @param ContainerInterface $_container Container interface
- * @param IAppManager $_appManager App manager interface
- * @param LoggerInterface $_logger Logger interface
- * @param SymfonyEmailService $_emailService Email service
+ * @param IUserManager $_userManager User manager interface.
+ * @param ISecureRandom $_secureRandom Secure random generator.
+ * @param IGroupManager $_groupManager Group manager interface.
+ * @param IAppConfig $_config Config interface.
+ * @param ContainerInterface $_container Container interface.
+ * @param IAppManager $_appManager App manager interface.
+ * @param LoggerInterface $_logger Logger interface.
+ * @param SymfonyEmailService $_emailService Email service.
+ * @param IConfig $config Config interface.
*/
public function __construct(
- private readonly IUserManager $_userManager,
- private readonly ISecureRandom $_secureRandom,
- private readonly IGroupManager $_groupManager,
- private readonly IAppConfig $_config,
- private readonly ContainerInterface $_container,
- private readonly IAppManager $_appManager,
- private readonly LoggerInterface $_logger,
+ private readonly IUserManager $_userManager,
+ private readonly ISecureRandom $_secureRandom,
+ private readonly IGroupManager $_groupManager,
+ private readonly IAppConfig $_config,
+ private readonly ContainerInterface $_container,
+ private readonly IAppManager $_appManager,
+ private readonly LoggerInterface $_logger,
private readonly SymfonyEmailService $_emailService,
- private readonly IConfig $config,
- )
- {
- }
+ private readonly IConfig $config,
+ ) {
+ }//end __construct()
/**
* Gets the OpenRegister ObjectService if available
@@ -74,15 +72,14 @@ public function __construct(
* @return \OCA\OpenRegister\Service\ObjectService|null ObjectService instance or null
* @throws \RuntimeException If service is not available
*/
- private function _getObjectService(): ?\OCA\OpenRegister\Service\ObjectService
+ private function getObjectService(): ?\OCA\OpenRegister\Service\ObjectService
{
- if (in_array('openregister', $this->_appManager->getInstalledApps())) {
+ if (in_array('openregister', $this->_appManager->getInstalledApps()) === true) {
return $this->_container->get('OCA\OpenRegister\Service\ObjectService');
}
throw new \RuntimeException('OpenRegister service is not available.');
- }
-
+ }//end getObjectService()
/**
* Generates a username from contact data with fallback strategies
@@ -94,370 +91,526 @@ private function _getObjectService(): ?\OCA\OpenRegister\Service\ObjectService
public function generateUsernameFromContactData(array $contactData): string
{
-
- $voornaam = $contactData['voornaam'] ?? '';
+ $voornaam = $contactData['voornaam'] ?? '';
$tussenvoegsel = $contactData['tussenvoegsel'] ?? '';
- $achternaam = $contactData['achternaam'] ?? '';
- $email = $contactData['email'] ?? $contactData['e-mailadres'] ?? '';
-
-
- // Strategy 1: full email address (PRIORITY)
- if (!empty($email) && strpos($email, '@') !== false) {
- $username = strtolower($email);
- if ($this->isValidUsername($username)) {
+ $achternaam = $contactData['achternaam'] ?? '';
+ $email = $contactData['email'] ?? $contactData['e-mailadres'] ?? '';
+
+ // Strategy 1: full email address (PRIORITY).
+ // Sanitize the email first to strip subaddressing (+tag) and invalid chars.
+ if (empty($email) === false && strpos($email, '@') !== false) {
+ $username = $this->sanitizeEmailForUsername(email: $email);
+ if ($this->isValidUsername(username: $username) === true) {
return $username;
}
}
- // Strategy 2: firstname.lastname (fallback)
- if (!empty($voornaam) && !empty($achternaam)) {
- // Strip spaces and non-alphanumeric chars from name parts
- $cleanVoornaam = preg_replace('/[^a-z0-9]/', '', strtolower($voornaam));
+ // Strategy 2: firstname.lastname (fallback).
+ if (empty($voornaam) === false && empty($achternaam) === false) {
+ // Strip spaces and non-alphanumeric chars from name parts.
+ $cleanVoornaam = preg_replace('/[^a-z0-9]/', '', strtolower($voornaam));
$cleanAchternaam = preg_replace('/[^a-z0-9]/', '', strtolower($achternaam));
- if (!empty($cleanVoornaam) && !empty($cleanAchternaam)) {
- $username = $cleanVoornaam . '.' . $cleanAchternaam;
- if ($this->isValidUsername($username)) {
+ if (empty($cleanVoornaam) === false && empty($cleanAchternaam) === false) {
+ $username = $cleanVoornaam.'.'.$cleanAchternaam;
+ if ($this->isValidUsername(username: $username) === true) {
return $username;
}
}
}
- // Strategy 3: firstnamelastname (fallback)
- if (!empty($voornaam) && !empty($achternaam)) {
- $cleanVoornaam = preg_replace('/[^a-z0-9]/', '', strtolower($voornaam));
+ // Strategy 3: firstnamelastname (fallback).
+ if (empty($voornaam) === false && empty($achternaam) === false) {
+ $cleanVoornaam = preg_replace('/[^a-z0-9]/', '', strtolower($voornaam));
$cleanAchternaam = preg_replace('/[^a-z0-9]/', '', strtolower($achternaam));
- if (!empty($cleanVoornaam) && !empty($cleanAchternaam)) {
- $username = $cleanVoornaam . $cleanAchternaam;
- if ($this->isValidUsername($username)) {
+ if (empty($cleanVoornaam) === false && empty($cleanAchternaam) === false) {
+ $username = $cleanVoornaam.$cleanAchternaam;
+ if ($this->isValidUsername(username: $username) === true) {
return $username;
}
}
}
- // Strategy 4: timestamp fallback
- $username = 'user' . time();
- if ($this->isValidUsername($username)) {
+ // Strategy 4: timestamp fallback.
+ $username = 'user'.time();
+ if ($this->isValidUsername(username: $username) === true) {
return $username;
}
- // If all strategies fail, log error and return empty string
+ // If all strategies fail, log error and return empty string.
$this->_logger->error('All username generation strategies failed', ['contactData' => $contactData]);
return '';
- }
+ }//end generateUsernameFromContactData()
/**
- * Validates if a username meets Nextcloud requirements
+ * Validates if a username meets Nextcloud requirements.
+ *
+ * @param string $username The username to validate.
+ *
+ * @return bool True if valid, false otherwise.
*/
private function isValidUsername(string $username): bool
{
- if (empty($username)) {
+ if (empty($username) === true) {
return false;
}
- // Basic validation rules (adjust based on your Nextcloud configuration)
+ // Basic validation rules (adjust based on your Nextcloud configuration).
if (strlen($username) < 3 || strlen($username) > 64) {
return false;
}
- // Must start with alphanumeric
- if (!preg_match('/^[a-z0-9]/', $username)) {
+ // Must start with alphanumeric.
+ if (preg_match('/^[a-z0-9]/', $username) === 0) {
return false;
}
- // Only allow alphanumeric, dots, underscores, dashes, and @ (matching Nextcloud's allowed chars)
- if (!preg_match('/^[a-z0-9._@\-]+$/', $username)) {
+ // Only allow alphanumeric, dots, underscores, dashes, and @ (matching Nextcloud's allowed chars).
+ if (preg_match('/^[a-z0-9._@\-]+$/', $username) === 0) {
return false;
}
return true;
- }
+ }//end isValidUsername()
+
+ /**
+ * Sanitizes an email address for use as a Nextcloud username.
+ *
+ * Strips subaddressing (the +tag part) from the local part of the email,
+ * since Nextcloud does not allow + in usernames. For example,
+ * "user+tag@example.com" becomes "user@example.com".
+ *
+ * @param string $email The email address to sanitize.
+ *
+ * @return string The sanitized email suitable for use as a username.
+ */
+ public function sanitizeEmailForUsername(string $email): string
+ {
+ $lowered = strtolower(trim($email));
+
+ // Strip subaddressing (+tag) from the local part of the email.
+ if (strpos($lowered, '+') !== false && strpos($lowered, '@') !== false) {
+ $parts = explode(separator: '@', string: $lowered, limit: 2);
+ $localPart = preg_replace(pattern: '/\+.*$/', replacement: '', subject: $parts[0]);
+ $lowered = $localPart.'@'.$parts[1];
+ }
+
+ // Remove any remaining characters not allowed in Nextcloud usernames.
+ $lowered = preg_replace(pattern: '/[^a-z0-9._@\-]/', replacement: '', subject: $lowered);
+
+ return $lowered;
+
+ }//end sanitizeEmailForUsername()
/**
* Validates an email address for use as a Nextcloud username.
* Returns null if valid, or an error message string if invalid.
*
- * @param string $email The email address to validate
- * @return string|null Null if valid, error message if invalid
+ * If the email contains subaddressing (a +tag), it is stripped before
+ * validation since Nextcloud does not support + in usernames. The
+ * sanitized form will be used as the actual username.
+ *
+ * @param string $email The email address to validate.
+ *
+ * @return string|null Null if valid, error message if invalid.
*/
public function validateEmailForUsername(string $email): ?string
{
- if (empty($email)) {
- return 'No email address found. The contact person must have a valid email address (in the "email" or "e-mailadres" field) to be activated.';
+ if (empty($email) === true) {
+ return 'No email address found on the contact person.';
}
- if (strpos($email, '@') === false) {
+ if (strpos(haystack: $email, needle: '@') === false) {
return "The email address \"{$email}\" is not a valid email address (missing @).";
}
- $lowered = strtolower($email);
-
- // Find invalid characters
- $invalidChars = preg_replace('/[a-z0-9._@\-]/', '', $lowered);
- if (!empty($invalidChars)) {
- $uniqueChars = implode(' ', array_unique(str_split($invalidChars)));
- return "The email address \"{$email}\" contains characters that are not allowed in a Nextcloud username: {$uniqueChars}. Only letters (a-z), numbers (0-9), dots (.), underscores (_), dashes (-) and @ are allowed. Please correct the email address on the contact person before activating.";
- }
+ // Sanitize the email by stripping subaddressing and removing invalid chars.
+ $sanitized = $this->sanitizeEmailForUsername(email: $email);
- if (strlen($lowered) < 3) {
+ if (strlen($sanitized) < 3) {
return "The email address \"{$email}\" is too short to be used as a username (minimum 3 characters).";
}
- if (strlen($lowered) > 64) {
+ if (strlen($sanitized) > 64) {
return "The email address \"{$email}\" is too long to be used as a username (maximum 64 characters).";
}
+ // Validate the sanitized result has no remaining invalid characters.
+ $invalidChars = preg_replace(pattern: '/[a-z0-9._@\-]/', replacement: '', subject: $sanitized);
+ if (empty($invalidChars) === false) {
+ $uniqueChars = implode(
+ separator: ' ',
+ array: array_unique(str_split($invalidChars)),
+ );
+ return "Invalid username characters: {$uniqueChars}.";
+ }
+
return null;
- }
+
+ }//end validateEmailForUsername()
/**
- * Ensures username is unique by adding counter if needed
+ * Ensures username is unique by adding counter if needed.
+ *
+ * @param string $username The username to check.
+ *
+ * @return string The unique username.
*/
private function ensureUniqueUsername(string $username): string
{
$originalUsername = $username;
- $counter = 1;
+ $counter = 1;
- while ($this->_userManager->userExists($username)) {
- $username = $originalUsername . $counter;
+ while ($this->_userManager->userExists($username) === true) {
+ $username = $originalUsername.$counter;
$counter++;
-
- // Safety check to prevent infinite loop
+ // Safety check to prevent infinite loop.
if ($counter > 9999) {
- $username = $originalUsername . uniqid();
+ $username = $originalUsername.uniqid();
break;
}
}
return $username;
- }
+ }//end ensureUniqueUsername()
/**
- * Creates a user account for a contact person
+ * Creates a user account for a contact person.
*
- * @param object $contactpersoonObject The contact person object
+ * @param object $contactpersoonObject The contact person object.
+ * @param bool $isFirstContact Whether this is the first contact.
*
- * @return \OCP\IUser|null The created user or null if failed
+ * @return \OCP\IUser|null The created user or null if failed.
*/
- public function createUserAccount(object $contactpersoonObject, bool $isFirstContact = false): ?\OCP\IUser
+ public function createUserAccount(object $contactpersoonObject, bool $isFirstContact=false): ?\OCP\IUser
{
$startTime = microtime(true);
try {
- $objectData = $contactpersoonObject->getObject();
- $contactId = $contactpersoonObject->getId();
- $email = $objectData['email'] ?? $objectData['e-mailadres'] ?? '';
+ $objectData = $contactpersoonObject->getObject();
+ $contactId = $contactpersoonObject->getId();
+ $email = $objectData['email'] ?? $objectData['e-mailadres'] ?? '';
$organizationUuid = $objectData['organisation'] ?? $objectData['organisatie'] ?? '';
- $this->_logger->debug('User account creation started', [
- 'app' => 'softwarecatalog',
- 'contactId' => $contactId,
- 'email' => $email,
- 'organizationUuid' => $organizationUuid,
- 'isFirstContact' => $isFirstContact,
- ]);
-
- if (empty($email)) {
- $this->_logger->error('❌ USER CREATION FAILED - NO EMAIL', [
- 'app' => 'softwarecatalog',
- 'contactpersoonId' => $contactId
- ]);
+ $this->_logger->debug(
+ 'User account creation started',
+ [
+ 'app' => 'softwarecatalog',
+ 'contactId' => $contactId,
+ 'email' => $email,
+ 'organizationUuid' => $organizationUuid,
+ 'isFirstContact' => $isFirstContact,
+ ]
+ );
+
+ if (empty($email) === true) {
+ $this->_logger->error(
+ '❌ USER CREATION FAILED - NO EMAIL',
+ [
+ 'app' => 'softwarecatalog',
+ 'contactpersoonId' => $contactId,
+ ]
+ );
return null;
}
- // Generate username first to check both email and username existence
+ // Generate username first to check both email and username existence.
$username = $objectData['username'] ?? '';
- if (empty($username)) {
- $this->_logger->info('[USER] Step 1: Generating username', [
- 'contactId' => $contactId,
- 'email' => $email
- ]);
- $username = $this->generateUsernameFromContactData($objectData);
- $this->_logger->critical('📝 USERNAME GENERATED', [
- 'app' => 'softwarecatalog',
- 'contactId' => $contactId,
- 'generatedUsername' => $username,
- 'email' => $email
- ]);
+ if (empty($username) === true) {
+ $this->_logger->info(
+ '[USER] Step 1: Generating username',
+ [
+ 'contactId' => $contactId,
+ 'email' => $email,
+ ]
+ );
+ $username = $this->generateUsernameFromContactData(contactData: $objectData);
+ $this->_logger->critical(
+ '📝 USERNAME GENERATED',
+ [
+ 'app' => 'softwarecatalog',
+ 'contactId' => $contactId,
+ 'generatedUsername' => $username,
+ 'email' => $email,
+ ]
+ );
}
- // Check if user already exists by email
- $this->_logger->info('[USER] Step 2: Checking existing user by email', [
- 'email' => $email
- ]);
- if ($this->_userManager->userExists($email)) {
- $this->_logger->critical('♻️ USER EXISTS BY EMAIL', [
- 'app' => 'softwarecatalog',
- 'email' => $email,
- 'contactpersoonId' => $contactId
- ]);
-
+ // Check if user already exists by email.
+ $this->_logger->info(
+ '[USER] Step 2: Checking existing user by email',
+ [
+ 'email' => $email,
+ ]
+ );
+ if ($this->_userManager->userExists($email) === true) {
+ $this->_logger->critical(
+ '♻️ USER EXISTS BY EMAIL',
+ [
+ 'app' => 'softwarecatalog',
+ 'email' => $email,
+ 'contactpersoonId' => $contactId,
+ ]
+ );
+
$existingUser = $this->_userManager->get($email);
- if ($existingUser) {
- // Store organization UUID for existing user
- if (!empty($organizationUuid)) {
- $this->storeUserOrganizationUuid($existingUser, $organizationUuid);
+ if (empty($existingUser) === false) {
+ // Store organization UUID for existing user.
+ if (empty($organizationUuid) === false) {
+ $this->storeUserOrganizationUuid(user: $existingUser, organizationUuid: $organizationUuid);
}
- // Store contact name fields for existing user (update if contact data changed)
- $this->storeContactNameFields($existingUser, $objectData);
+ // Store contact name fields for existing user (update if contact data changed).
+ $this->storeContactNameFields(user: $existingUser, contactData: $objectData);
- // Update groups for existing user
- $this->assignUserGroups($existingUser, $objectData, $isFirstContact);
+ // Update groups for existing user.
+ $this->assignUserGroups(user: $existingUser, objectData: $objectData, isFirstContact: $isFirstContact);
- $this->_logger->critical('✅ EXISTING USER UPDATED', [
- 'app' => 'softwarecatalog',
- 'username' => $existingUser->getUID(),
- 'email' => $email,
- 'organizationUuid' => $organizationUuid
- ]);
+ $this->_logger->critical(
+ '✅ EXISTING USER UPDATED',
+ [
+ 'app' => 'softwarecatalog',
+ 'username' => $existingUser->getUID(),
+ 'email' => $email,
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
return $existingUser;
- }
- }
+ }//end if
+ }//end if
- // Check if user already exists by username
- $this->_logger->info('[USER] Step 3: Checking existing user by username', [
- 'username' => $username
- ]);
+ // Check if user already exists by username.
+ $this->_logger->info(
+ '[USER] Step 3: Checking existing user by username',
+ [
+ 'username' => $username,
+ ]
+ );
$existingUserByUsername = $this->_userManager->get($username);
- if ($existingUserByUsername) {
- $this->_logger->critical('♻️ USER EXISTS BY USERNAME', [
- 'app' => 'softwarecatalog',
- 'username' => $username,
- 'contactpersoonId' => $contactId
- ]);
+ if (empty($existingUserByUsername) === false) {
+ $this->_logger->critical(
+ '♻️ USER EXISTS BY USERNAME',
+ [
+ 'app' => 'softwarecatalog',
+ 'username' => $username,
+ 'contactpersoonId' => $contactId,
+ ]
+ );
- // Store organization UUID for existing user
- if (!empty($organizationUuid)) {
- $this->storeUserOrganizationUuid($existingUserByUsername, $organizationUuid);
+ // Store organization UUID for existing user.
+ if (empty($organizationUuid) === false) {
+ $this->storeUserOrganizationUuid(user: $existingUserByUsername, organizationUuid: $organizationUuid);
}
- // Store contact name fields for existing user (update if contact data changed)
- $this->storeContactNameFields($existingUserByUsername, $objectData);
+ // Store contact name fields for existing user (update if contact data changed).
+ $this->storeContactNameFields(user: $existingUserByUsername, contactData: $objectData);
- // Update groups for existing user
- $this->assignUserGroups($existingUserByUsername, $objectData, $isFirstContact);
+ // Update groups for existing user.
+ $this->assignUserGroups(user: $existingUserByUsername, objectData: $objectData, isFirstContact: $isFirstContact);
- $this->_logger->critical('✅ EXISTING USER UPDATED BY USERNAME', [
- 'app' => 'softwarecatalog',
- 'username' => $username,
- 'email' => $existingUserByUsername->getEMailAddress(),
- 'organizationUuid' => $organizationUuid
- ]);
+ $this->_logger->critical(
+ '✅ EXISTING USER UPDATED BY USERNAME',
+ [
+ 'app' => 'softwarecatalog',
+ 'username' => $username,
+ 'email' => $existingUserByUsername->getEMailAddress(),
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
return $existingUserByUsername;
- }
+ }//end if
- // Create new user account
- $this->_logger->critical('🚀 CREATING NEW USER ACCOUNT', [
- 'app' => 'softwarecatalog',
- 'username' => $username,
- 'email' => $email,
- 'contactId' => $contactId
- ]);
+ // Create new user account.
+ $this->_logger->critical(
+ '🚀 CREATING NEW USER ACCOUNT',
+ [
+ 'app' => 'softwarecatalog',
+ 'username' => $username,
+ 'email' => $email,
+ 'contactId' => $contactId,
+ ]
+ );
$randomPw = $this->_secureRandom->generate(length: 12);
- $user = $this->_userManager->createUser(uid: $username, password: $randomPw);
+ $user = $this->_userManager->createUser(uid: $username, password: $randomPw);
- if ($user) {
- $this->_logger->critical('🎊 NEW USER ACCOUNT CREATED', [
- 'app' => 'softwarecatalog',
- 'username' => $username,
- 'email' => $email,
- 'contactId' => $contactId,
- 'userId' => $user->getUID()
- ]);
-
- // Set user details
- $this->_logger->info('[USER] Step 4: Setting user details', [
- 'username' => $username
- ]);
+ if (empty($user) === false) {
+ $this->_logger->critical(
+ '🎊 NEW USER ACCOUNT CREATED',
+ [
+ 'app' => 'softwarecatalog',
+ 'username' => $username,
+ 'email' => $email,
+ 'contactId' => $contactId,
+ 'userId' => $user->getUID(),
+ ]
+ );
+
+ // Fire-and-forget filesystem pre-warm in a background process.
+ // This replicates Session::prepareUserLogin() (setupFS, copySkeleton,.
+ // updateLastLoginTimestamp) so the user's first login is instant.
+ // Uses exec('... &') for true async — returns immediately, the forked.
+ // process does the ~5s work without blocking the admin's request.
+ try {
+ $phpBin = PHP_BINARY;
+ if (empty($phpBin) === true) {
+ $phpBin = 'php';
+ }
+
+ $serverRoot = \OC::$SERVERROOT;
+ $safeUser = escapeshellarg($username);
+ $exportedUser = var_export($username, true);
+ $requirePart = 'require "'.$serverRoot.'/lib/base.php";';
+ $setupPart = ' \OC_Util::setupFS('.$exportedUser.');';
+ $folderPart = ' $f = \OC::$server->getUserFolder('.$exportedUser.');';
+ $skelPart = ' \OC_Util::copySkeleton('.$exportedUser.', $f);';
+ // phpcs:ignore Generic.Files.LineLength.TooLong
+ $loginPart = ' \OC::$server->get(\OCP\IUserManager::class)->get('.$exportedUser.')->updateLastLoginTimestamp();';
+ $script = $requirePart.$setupPart.$folderPart.$skelPart.$loginPart;
+ $cmd = sprintf(
+ '%s -r %s > /dev/null 2>&1 &',
+ escapeshellarg($phpBin),
+ escapeshellarg($script)
+ );
+ exec($cmd);
+ } catch (\Exception $e) {
+ $this->_logger->warning(
+ 'Filesystem pre-warm exec failed for '.$username,
+ [
+ 'app' => 'softwarecatalog',
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+
+ // Set user details.
+ $this->_logger->info(
+ '[USER] Step 4: Setting user details',
+ [
+ 'username' => $username,
+ ]
+ );
$user->setEMailAddress($email);
- $displayName = $this->getDisplayNameFromContactData($objectData);
+ $displayName = $this->getDisplayNameFromContactData(contactData: $objectData);
$user->setDisplayName($displayName);
- // Store contact name fields in Nextcloud user config for /me endpoint
- $this->storeContactNameFields($user, $objectData);
+ // Store contact name fields in Nextcloud user config for /me endpoint.
+ $this->storeContactNameFields(user: $user, contactData: $objectData);
- $this->_logger->critical('📋 USER DETAILS SET', [
- 'app' => 'softwarecatalog',
- 'username' => $username,
- 'email' => $email,
- 'displayName' => $displayName,
- 'firstName' => $objectData['voornaam'] ?? '',
- 'middleName' => $objectData['tussenvoegsel'] ?? '',
- 'lastName' => $objectData['achternaam'] ?? '',
- 'functie' => $objectData['functie'] ?? ''
- ]);
-
- // Store organization UUID in user config for OpenConnector access
- if (!empty($organizationUuid)) {
- $this->_logger->info('[USER] Step 5: Storing organization UUID', [
- 'username' => $username,
- 'organizationUuid' => $organizationUuid
- ]);
- $this->storeUserOrganizationUuid($user, $organizationUuid);
+ $this->_logger->critical(
+ '📋 USER DETAILS SET',
+ [
+ 'app' => 'softwarecatalog',
+ 'username' => $username,
+ 'email' => $email,
+ 'displayName' => $displayName,
+ 'firstName' => $objectData['voornaam'] ?? '',
+ 'middleName' => $objectData['tussenvoegsel'] ?? '',
+ 'lastName' => $objectData['achternaam'] ?? '',
+ 'functie' => $objectData['functie'] ?? '',
+ ]
+ );
+
+ // Store organization UUID in user config for OpenConnector access.
+ if (empty($organizationUuid) === false) {
+ $this->_logger->info(
+ '[USER] Step 5: Storing organization UUID',
+ [
+ 'username' => $username,
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
+ $this->storeUserOrganizationUuid(user: $user, organizationUuid: $organizationUuid);
}
- // Set user groups based on roles and organization
- $this->_logger->info('[USER] Step 6: Assigning user groups', [
- 'username' => $username,
- 'isFirstContact' => $isFirstContact
- ]);
- $this->assignUserGroups($user, $objectData, $isFirstContact);
+ // Set user groups based on roles and organization.
+ $this->_logger->info(
+ '[USER] Step 6: Assigning user groups',
+ [
+ 'username' => $username,
+ 'isFirstContact' => $isFirstContact,
+ ]
+ );
+ $assignedRole = $this->assignUserGroups(user: $user, objectData: $objectData, isFirstContact: $isFirstContact);
- // Update contactpersoon with username
+ // Update contactpersoon with username and auto-assigned role.
$objectData['username'] = $username;
+
+ // Populate the rollen field if a role was assigned and rollen is empty.
+ $currentRollen = $objectData['rollen'] ?? [];
+ if (empty($assignedRole) === false && (empty($currentRollen) === true || is_array($currentRollen) === false)) {
+ $objectData['rollen'] = [$assignedRole];
+ $this->_logger->info(
+ 'Auto-populated rollen field on contactpersoon',
+ [
+ 'username' => $username,
+ 'assignedRole' => $assignedRole,
+ ]
+ );
+ }
+
$contactpersoonObject->setObject($objectData);
- // Send user creation email
- $this->_logger->info('[USER] Step 7: Sending user creation email', [
- 'username' => $username,
- 'email' => $email
- ]);
- $this->sendUserCreationEmail($user, $objectData);
+ // Send user creation email.
+ $this->_logger->info(
+ '[USER] Step 7: Sending user creation email',
+ [
+ 'username' => $username,
+ 'email' => $email,
+ ]
+ );
+ $this->sendUserCreationEmail(user: $user, objectData: $objectData);
$creationTime = round(microtime(true) - $startTime, 3);
- $this->_logger->critical('🎉 USER ACCOUNT CREATION COMPLETED', [
- 'app' => 'softwarecatalog',
- 'contactpersoonId' => $contactId,
- 'username' => $username,
- 'email' => $email,
- 'displayName' => $displayName,
- 'organizationUuid' => $organizationUuid,
- 'creationTime' => $creationTime . 's'
- ]);
+ $this->_logger->critical(
+ '🎉 USER ACCOUNT CREATION COMPLETED',
+ [
+ 'app' => 'softwarecatalog',
+ 'contactpersoonId' => $contactId,
+ 'username' => $username,
+ 'email' => $email,
+ 'displayName' => $displayName,
+ 'organizationUuid' => $organizationUuid,
+ 'creationTime' => $creationTime.'s',
+ ]
+ );
return $user;
} else {
- $this->_logger->error('❌ USER CREATION RETURNED NULL', [
- 'app' => 'softwarecatalog',
- 'username' => $username,
- 'email' => $email,
- 'contactpersoonId' => $contactId,
- 'note' => 'No exception thrown but createUser returned null'
- ]);
- }
+ $this->_logger->error(
+ '❌ USER CREATION RETURNED NULL',
+ [
+ 'app' => 'softwarecatalog',
+ 'username' => $username,
+ 'email' => $email,
+ 'contactpersoonId' => $contactId,
+ 'note' => 'No exception thrown but createUser returned null',
+ ]
+ );
+ }//end if
return null;
-
} catch (\Exception $e) {
- $this->_logger->error('💥 USER CREATION EXCEPTION', [
- 'app' => 'softwarecatalog',
- 'contactpersoonId' => $contactpersoonObject->getId(),
- 'email' => $objectData['email'] ?? $objectData['e-mailadres'] ?? 'unknown',
- 'username' => $username ?? 'unknown',
- 'exception' => $e->getMessage(),
- 'exception_class' => get_class($e),
- 'exception_code' => $e->getCode(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->_logger->error(
+ '💥 USER CREATION EXCEPTION',
+ [
+ 'app' => 'softwarecatalog',
+ 'contactpersoonId' => $contactpersoonObject->getId(),
+ 'email' => $objectData['email'] ?? $objectData['e-mailadres'] ?? 'unknown',
+ 'username' => $username ?? 'unknown',
+ 'exception' => $e->getMessage(),
+ 'exception_class' => get_class($e),
+ 'exception_code' => $e->getCode(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return null;
- }
- }
+ }//end try
+ }//end createUserAccount()
/**
* Assigns user groups based on organization type and roles
@@ -468,26 +621,29 @@ public function createUserAccount(object $contactpersoonObject, bool $isFirstCon
*
* @return void
*/
+
/**
* Assign user to appropriate groups based on their role and organization.
- *
+ *
* Users are NOT added to generic groups or organization-specific groups.
* Users are tied to organization entities in OpenRegister instead.
- *
- * @param \OCP\IUser $user The user to assign groups to.
- * @param array $objectData The contactpersoon object data.
- * @param bool $isFirstContact Whether this is the first contact of the organization.
- *
- * @return void
+ *
+ * @param \OCP\IUser $user The user to assign groups to.
+ * @param array $objectData The contactpersoon object data.
+ * @param bool $isFirstContact Whether this is the first contact of the organization.
+ *
+ * @return string
*/
- private function assignUserGroups(\OCP\IUser $user, array $objectData, bool $isFirstContact = false): void
+ private function assignUserGroups(\OCP\IUser $user, array $objectData, bool $isFirstContact=false): string
{
+ $assignedRole = '';
+
try {
- $roles = $objectData['roles'] ?? [];
+ $roles = $objectData['rollen'] ?? $objectData['roles'] ?? [];
$organizationId = $objectData['organisation'] ?? $objectData['organisatie'] ?? '';
// Ensure roles is an array.
- if (!is_array($roles)) {
+ if (is_array($roles) === false) {
$roles = [$roles];
}
@@ -495,76 +651,110 @@ private function assignUserGroups(\OCP\IUser $user, array $objectData, bool $isF
$settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
// Add user to organization admin groups if this is the first contact.
- if ($isFirstContact) {
+ if (empty($isFirstContact) === false) {
$organizationAdminGroups = $settingsService->getOrganizationAdminGroups();
foreach ($organizationAdminGroups as $groupName) {
- $this->addUserToGroupWithCheck($user, $groupName, 'organization-admin');
+ $this->addUserToGroupWithCheck(user: $user, groupName: $groupName, type: 'organization-admin');
}
-
+
$this->_logger->info(
'Assigned organization admin groups to first contact',
[
- 'username' => $user->getUID(),
+ 'username' => $user->getUID(),
'organizationId' => $organizationId,
- 'adminGroups' => $organizationAdminGroups
+ 'adminGroups' => $organizationAdminGroups,
]
);
}
// Assign role based on organization type.
- if (!empty($organizationId)) {
- $organizationType = $this->getOrganizationType((string)$organizationId);
- $roleGroup = $this->getRoleGroupByOrganizationType($organizationType);
-
- if (!empty($roleGroup)) {
- $this->addUserToGroupWithCheck($user, $roleGroup, 'organization-type-role');
-
+ if (empty($organizationId) === false) {
+ $organizationType = $this->getOrganizationType(organizationId: (string) $organizationId);
+ $roleGroup = $this->getRoleGroupByOrganizationType(organizationType: $organizationType);
+
+ if (empty($roleGroup) === false) {
+ $this->addUserToGroupWithCheck(user: $user, groupName: $roleGroup, type: 'organization-type-role');
+
+ // Map the lowercase group name to the title-case enum value for the rollen field.
+ $assignedRole = $this->mapGroupNameToRollenEnum(groupName: $roleGroup);
+
$this->_logger->info(
'Assigned role based on organization type',
[
- 'username' => $user->getUID(),
- 'organizationId' => $organizationId,
+ 'username' => $user->getUID(),
+ 'organizationId' => $organizationId,
'organizationType' => $organizationType,
- 'assignedRole' => $roleGroup
+ 'assignedRole' => $roleGroup,
+ 'rollenEnumValue' => $assignedRole,
]
);
} else {
$this->_logger->warning(
'No role mapping found for organization type',
[
- 'username' => $user->getUID(),
- 'organizationId' => $organizationId,
- 'organizationType' => $organizationType
+ 'username' => $user->getUID(),
+ 'organizationId' => $organizationId,
+ 'organizationType' => $organizationType,
]
);
- }
- }
+ }//end if
+ }//end if
// Users are now tied to organisation entities in OpenRegister.
// No need to add to organization-specific groups.
+ if ($isFirstContact === true) {
+ $organizationAdminGroupsValue = ($organizationAdminGroups ?? []);
+ } else {
+ $organizationAdminGroupsValue = [];
+ }
$this->_logger->info(
'Successfully assigned user groups based on organization type',
[
- 'username' => $user->getUID(),
- 'isFirstContact' => $isFirstContact,
- 'organizationAdminGroups' => $isFirstContact ? ($organizationAdminGroups ?? []) : [],
- 'organizationId' => $organizationId,
- 'roleGroup' => $roleGroup ?? 'none',
- 'organizationType' => $organizationType ?? 'unknown'
+ 'username' => $user->getUID(),
+ 'isFirstContact' => $isFirstContact,
+ 'organizationAdminGroups' => $organizationAdminGroupsValue,
+ 'organizationId' => $organizationId,
+ 'roleGroup' => $roleGroup ?? 'none',
+ 'organizationType' => $organizationType ?? 'unknown',
+ 'assignedRollenEnum' => $assignedRole,
]
);
-
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to assign user groups: ' . $e->getMessage(),
+ 'Failed to assign user groups: '.$e->getMessage(),
[
- 'username' => $user->getUID(),
- 'exception' => $e
+ 'username' => $user->getUID(),
+ 'exception' => $e,
]
);
- }
- }
+ }//end try
+
+ return $assignedRole;
+ }//end assignUserGroups()
+
+ /**
+ * Maps a lowercase group name to the title-case rollen enum value
+ *
+ * The contactpersoon schema uses title-case enum values (e.g., "Gebruik-beheerder")
+ * while Nextcloud groups use lowercase (e.g., "gebruik-beheerder").
+ *
+ * @param string $groupName The lowercase group name
+ *
+ * @return string The title-case enum value for the rollen field
+ */
+ private function mapGroupNameToRollenEnum(string $groupName): string
+ {
+ $mapping = [
+ 'aanbod-beheerder' => 'Aanbod-beheerder',
+ 'gebruik-beheerder' => 'Gebruik-beheerder',
+ 'gebruik-raadpleger' => 'Gebruik-raadpleger',
+ 'functioneel-beheerder' => 'Functioneel-beheerder',
+ 'organisatie-beheerder' => 'Organisatie-beheerder',
+ ];
+
+ return $mapping[strtolower(trim($groupName))] ?? '';
+ }//end mapGroupNameToRollenEnum()
/**
* Gets the mapping of allowed roles to group names
@@ -574,22 +764,22 @@ private function assignUserGroups(\OCP\IUser $user, array $objectData, bool $isF
private function getAllowedRoleGroups(): array
{
return [
- 'Aanbod-beheerder' => 'Aanbod-beheerder',
- 'Gebruik-beheerder' => 'Gebruik-beheerder',
- 'Gebruik-raadpleger' => 'Gebruik-raadpleger',
+ 'Aanbod-beheerder' => 'Aanbod-beheerder',
+ 'Gebruik-beheerder' => 'Gebruik-beheerder',
+ 'Gebruik-raadpleger' => 'Gebruik-raadpleger',
'Functioneel-beheerder' => 'Functioneel-beheerder',
- 'VNG-raadpleger' => 'VNG-raadpleger',
+ 'VNG-raadpleger' => 'VNG-raadpleger',
'Organisatie-beheerder' => 'Organisatie-beheerder',
- 'Ambtenaar' => 'Ambtenaar'
+ 'Ambtenaar' => 'Ambtenaar',
];
- }
+ }//end getAllowedRoleGroups()
/**
* Adds a user to a group, creating the group if it doesn't exist
*
- * @param \OCP\IUser $user The user to add
- * @param string $groupName The group name
- * @param string $type The type of group assignment (for logging)
+ * @param \OCP\IUser $user The user to add
+ * @param string $groupName The group name
+ * @param string $type The type of group assignment (for logging)
*
* @return void
*/
@@ -597,9 +787,9 @@ private function addUserToGroup(\OCP\IUser $user, string $groupName, string $typ
{
try {
$group = $this->_groupManager->get($groupName);
- if (!$group) {
+ if ($group === null) {
$group = $this->_groupManager->createGroup($groupName);
- if ($group) {
+ if (empty($group) === false) {
$this->_logger->info(
'Created group for user assignment',
['groupName' => $groupName, 'type' => $type]
@@ -607,36 +797,36 @@ private function addUserToGroup(\OCP\IUser $user, string $groupName, string $typ
}
}
- if ($group && !$group->inGroup($user)) {
+ if ($group !== false && $group->inGroup($user) === false) {
$group->addUser($user);
$this->_logger->info(
'Added user to group',
[
- 'username' => $user->getUID(),
+ 'username' => $user->getUID(),
'groupName' => $groupName,
- 'type' => $type
+ 'type' => $type,
]
);
}
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to add user to group: ' . $e->getMessage(),
+ 'Failed to add user to group: '.$e->getMessage(),
[
- 'username' => $user->getUID(),
+ 'username' => $user->getUID(),
'groupName' => $groupName,
- 'type' => $type,
- 'exception' => $e
+ 'type' => $type,
+ 'exception' => $e,
]
);
- }
- }
+ }//end try
+ }//end addUserToGroup()
/**
* Adds a user to a group only if the group exists, does not create new groups
*
- * @param \OCP\IUser $user The user to add to the group
- * @param string $groupName The name of the group
- * @param string $type The type of group assignment for logging
+ * @param \OCP\IUser $user The user to add to the group
+ * @param string $groupName The name of the group
+ * @param string $type The type of group assignment for logging
*
* @return void
*/
@@ -644,58 +834,58 @@ private function addUserToGroupWithCheck(\OCP\IUser $user, string $groupName, st
{
try {
$group = $this->_groupManager->get($groupName);
-
- if (!$group) {
+
+ if ($group === null) {
$this->_logger->warning(
'Group does not exist, skipping user assignment',
[
- 'username' => $user->getUID(),
+ 'username' => $user->getUID(),
'groupName' => $groupName,
- 'type' => $type
+ 'type' => $type,
]
);
return;
}
- if (!$group->inGroup($user)) {
+ if ($group->inGroup($user) === false) {
$group->addUser($user);
$this->_logger->info(
'Added user to existing group',
[
- 'username' => $user->getUID(),
+ 'username' => $user->getUID(),
'groupName' => $groupName,
- 'type' => $type
+ 'type' => $type,
]
);
} else {
$this->_logger->debug(
'User already in group',
[
- 'username' => $user->getUID(),
+ 'username' => $user->getUID(),
'groupName' => $groupName,
- 'type' => $type
+ 'type' => $type,
]
);
}
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to add user to group with check: ' . $e->getMessage(),
+ 'Failed to add user to group with check: '.$e->getMessage(),
[
- 'username' => $user->getUID(),
+ 'username' => $user->getUID(),
'groupName' => $groupName,
- 'type' => $type,
- 'exception' => $e
+ 'type' => $type,
+ 'exception' => $e,
]
);
- }
- }
+ }//end try
+ }//end addUserToGroupWithCheck()
/**
* Updates user groups when contact person data changes
* Note: Role assignment is now based on organization type, not individual roles
*
- * @param \OCP\IUser $user The user to update
- * @param array $contactData The updated contact person data
+ * @param \OCP\IUser $user The user to update
+ * @param array $contactData The updated contact person data
*
* @return void
*/
@@ -703,8 +893,8 @@ public function updateUserGroupsFromContactData(\OCP\IUser $user, array $contact
{
try {
$organizationId = $contactData['organisation'] ?? $contactData['organisatie'] ?? '';
-
- if (empty($organizationId)) {
+
+ if (empty($organizationId) === true) {
$this->_logger->warning(
'No organization ID found for user group update',
['username' => $user->getUID()]
@@ -712,92 +902,91 @@ public function updateUserGroupsFromContactData(\OCP\IUser $user, array $contact
return;
}
- // Get organization type and determine role group
- $organizationType = $this->getOrganizationType((string)$organizationId);
- $newRoleGroup = $this->getRoleGroupByOrganizationType($organizationType);
-
- if (empty($newRoleGroup)) {
+ // Get organization type and determine role group.
+ $organizationType = $this->getOrganizationType(organizationId: (string) $organizationId);
+ $newRoleGroup = $this->getRoleGroupByOrganizationType(organizationType: $organizationType);
+
+ if (empty($newRoleGroup) === true) {
$this->_logger->warning(
'No role group mapping found for organization type during update',
[
- 'username' => $user->getUID(),
- 'organizationId' => $organizationId,
- 'organizationType' => $organizationType
+ 'username' => $user->getUID(),
+ 'organizationId' => $organizationId,
+ 'organizationType' => $organizationType,
]
);
return;
}
- // Remove user from old organization type role groups
+ // Remove user from old organization type role groups.
$allPossibleRoleGroups = ['gebruik-beheerder', 'aanbod-beheerder'];
foreach ($allPossibleRoleGroups as $roleGroup) {
if ($roleGroup !== $newRoleGroup) {
$group = $this->_groupManager->get($roleGroup);
- if ($group && $group->inGroup($user)) {
+ if ($group !== false && $group->inGroup($user) === true) {
$group->removeUser($user);
$this->_logger->info(
'Removed user from old organization type role group',
[
- 'username' => $user->getUID(),
+ 'username' => $user->getUID(),
'groupName' => $roleGroup,
- 'reason' => 'organization type changed'
+ 'reason' => 'organization type changed',
]
);
}
}
}
- // Add user to new role group if it exists
- $this->addUserToGroupWithCheck($user, $newRoleGroup, 'organization-type-role-update');
+ // Add user to new role group if it exists.
+ $this->addUserToGroupWithCheck(user: $user, groupName: $newRoleGroup, type: 'organization-type-role-update');
$this->_logger->info(
'Updated user groups based on organization type',
[
- 'username' => $user->getUID(),
- 'organizationId' => $organizationId,
+ 'username' => $user->getUID(),
+ 'organizationId' => $organizationId,
'organizationType' => $organizationType,
- 'assignedRole' => $newRoleGroup
+ 'assignedRole' => $newRoleGroup,
]
);
-
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to update user groups from contact data: ' . $e->getMessage(),
+ 'Failed to update user groups from contact data: '.$e->getMessage(),
[
- 'username' => $user->getUID(),
- 'exception' => $e
+ 'username' => $user->getUID(),
+ 'exception' => $e,
]
);
- }
- }
+ }//end try
+ }//end updateUserGroupsFromContactData()
/**
* Legacy method for backward compatibility - now redirects to organization type-based logic
*
- * @param \OCP\IUser $user The user to update
- * @param array $newRoles The new roles (ignored - kept for compatibility)
- * @param array $oldRoles The old roles (ignored - kept for compatibility)
+ * @param \OCP\IUser $user The user to update
+ * @param array $newRoles The new roles (ignored - kept for compatibility)
+ * @param array $oldRoles The old roles (ignored - kept for compatibility)
*
- * @return void
+ * @return void
* @deprecated Use updateUserGroupsFromContactData instead
*/
- public function updateUserGroupsFromRoles(\OCP\IUser $user, array $newRoles, array $oldRoles = []): void
+ public function updateUserGroupsFromRoles(\OCP\IUser $user, array $newRoles, array $oldRoles=[]): void
{
$this->_logger->info(
'updateUserGroupsFromRoles is deprecated - role assignment now based on organization type',
[
'username' => $user->getUID(),
'newRoles' => $newRoles,
- 'oldRoles' => $oldRoles
+ 'oldRoles' => $oldRoles,
]
);
-
- // For backward compatibility, try to find the user's contact data and update based on organization type
+
+ // For backward compatibility, try to find the user's contact data and update based on organization type.
try {
- $contactObject = $this->findContactpersoonByUsername($user->getUID());
- if ($contactObject) {
+ $contactObject = $this->findContactpersoonByUsername(username: $user->getUID());
+ if (empty($contactObject) === false) {
$contactData = $contactObject->getObject();
- $this->updateUserGroupsFromContactData($user, $contactData);
+ $this->updateUserGroupsFromContactData(user: $user, contactData: $contactData);
} else {
$this->_logger->warning(
'Could not find contact person data for user - cannot update groups',
@@ -806,16 +995,14 @@ public function updateUserGroupsFromRoles(\OCP\IUser $user, array $newRoles, arr
}
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to update user groups via legacy method: ' . $e->getMessage(),
+ 'Failed to update user groups via legacy method: '.$e->getMessage(),
[
- 'username' => $user->getUID(),
- 'exception' => $e
+ 'username' => $user->getUID(),
+ 'exception' => $e,
]
);
}
- }
-
-
+ }//end updateUserGroupsFromRoles()
/**
* Finds contactpersoon object by username
@@ -827,41 +1014,41 @@ public function updateUserGroupsFromRoles(\OCP\IUser $user, array $newRoles, arr
private function findContactpersoonByUsername(string $username): ?object
{
try {
- $objectService = $this->_getObjectService();
+ $objectService = $this->getObjectService();
$settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
- // Get configuration values
+ // Get configuration values.
$registerId = $settingsService->getVoorzieningenRegisterId();
$contactpersoonSchemaId = $settingsService->getSchemaIdForObjectType('contactpersoon');
- if (!$registerId || !$contactpersoonSchemaId) {
+ if ($registerId === null || $contactpersoonSchemaId === false) {
throw new \Exception('Register or schema ID not configured for contactpersoon');
}
- // Search for contactpersoon with the given username
+ // Search for contactpersoon with the given username.
$searchFilters = [
- 'username' => $username
+ 'username' => $username,
];
$results = $objectService->findAll($searchFilters, $registerId, $contactpersoonSchemaId);
- if (!empty($results)) {
- return $results[0]; // Return the first match
+ if (empty($results) === false) {
+ return $results[0];
+ // Return the first match.
}
return null;
-
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to find contactpersoon by username: ' . $e->getMessage(),
+ 'Failed to find contactpersoon by username: '.$e->getMessage(),
[
- 'username' => $username,
- 'exception' => $e
+ 'username' => $username,
+ 'exception' => $e,
]
);
return null;
- }
- }
+ }//end try
+ }//end findContactpersoonByUsername()
/**
* Gets the organization group for a given organization ID
@@ -873,71 +1060,73 @@ private function findContactpersoonByUsername(string $username): ?object
private function getOrganizationGroup(string $organizationId): ?\OCP\IGroup
{
try {
- // Get the organization object to find its group
- $objectService = $this->_getObjectService();
- if (!$objectService) {
+ // Get the organization object to find its group.
+ $objectService = $this->getObjectService();
+ if ($objectService === null) {
return null;
}
- // Get register and schema IDs dynamically from configuration
- $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
- $registerId = $settingsService->getVoorzieningenRegisterId();
+ // Get register and schema IDs dynamically from configuration.
+ $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
+ $registerId = $settingsService->getVoorzieningenRegisterId();
$organisatieSchemaId = $settingsService->getSchemaIdForObjectType('organisatie');
- if (!$registerId || !$organisatieSchemaId) {
+ if ($registerId === null || $organisatieSchemaId === false) {
$this->_logger->warning('Register or schema ID not configured for organisatie');
return null;
}
- // Use find() method with proper register/schema context
+ // Use find() method with proper register/schema context.
$organizationObject = $objectService->find($organizationId, [], false, $registerId, $organisatieSchemaId);
- if ($organizationObject) {
+ if (empty($organizationObject) === false) {
$organizationData = $organizationObject->getObject();
- $groupId = $organizationData['group'] ?? '';
+ $groupId = $organizationData['group'] ?? '';
- if (!empty($groupId)) {
+ if (empty($groupId) === false) {
$group = $this->_groupManager->get($groupId);
return $group;
}
}
return null;
-
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to get organization group: ' . $e->getMessage(),
+ 'Failed to get organization group: '.$e->getMessage(),
[
'organizationId' => $organizationId,
- 'exception' => $e
+ 'exception' => $e,
]
);
return null;
- }
- }
+ }//end try
+ }//end getOrganizationGroup()
/**
* Determines if this contact object is the first contact for the organization
*
* @param object $contactObject The contact object being processed (contactpersoon)
- * @param array $objectData The contact data
+ * @param array $objectData The contact data
*
* @return bool True if this is the first contact for the organization
*/
public function isFirstContactForOrganization(object $contactObject, array $objectData): bool
{
- // Simplified approach: Default to true so the first contact always gets admin rights
- // A more sophisticated check can be implemented later if needed to track
- // whether other contacts already exist for this organization
- $this->_logger->info('isFirstContactForOrganization: Defaulting to true (simplified)', [
- 'app' => 'softwarecatalog',
- 'contactId' => $contactObject->getId(),
- 'contactUuid' => $contactObject->getUuid()
- ]);
+ // Simplified approach: Default to true so the first contact always gets admin rights.
+ // A more sophisticated check can be implemented later if needed to track.
+ // whether other contacts already exist for this organization.
+ $this->_logger->info(
+ 'isFirstContactForOrganization: Defaulting to true (simplified)',
+ [
+ 'app' => 'softwarecatalog',
+ 'contactId' => $contactObject->getId(),
+ 'contactUuid' => $contactObject->getUuid(),
+ ]
+ );
return true;
- }
+ }//end isFirstContactForOrganization()
/**
* Stores organization UUID in user config for OpenConnector access
@@ -947,7 +1136,7 @@ public function isFirstContactForOrganization(object $contactObject, array $obje
* It also sets the user's active organisation in OpenRegister so they're
* automatically logged into the correct organisation.
*
- * @param IUser $user The user object
+ * @param IUser $user The user object
* @param string|int $organizationUuid The organization UUID (can be string or int)
*
* @return void
@@ -955,11 +1144,11 @@ public function isFirstContactForOrganization(object $contactObject, array $obje
private function storeUserOrganizationUuid(IUser $user, string|int $organizationUuid): void
{
try {
- if (!empty($organizationUuid)) {
- // Convert to string to ensure consistent storage
- $organizationUuidStr = (string)$organizationUuid;
+ if (empty($organizationUuid) === false) {
+ // Convert to string to ensure consistent storage.
+ $organizationUuidStr = (string) $organizationUuid;
- // Store in core config for OpenConnector access
+ // Store in core config for OpenConnector access.
$this->config->setUserValue(
$user->getUID(),
'core',
@@ -967,7 +1156,7 @@ private function storeUserOrganizationUuid(IUser $user, string|int $organization
$organizationUuidStr
);
- // Also set as active organisation in OpenRegister
+ // Also set as active organisation in OpenRegister.
try {
$this->config->setUserValue(
$user->getUID(),
@@ -979,34 +1168,34 @@ private function storeUserOrganizationUuid(IUser $user, string|int $organization
$this->_logger->info(
'Stored organization UUID in user config and set as active organisation',
[
- 'username' => $user->getUID(),
- 'organizationUuid' => $organizationUuidStr,
- 'organizationUuid_type' => gettype($organizationUuid)
+ 'username' => $user->getUID(),
+ 'organizationUuid' => $organizationUuidStr,
+ 'organizationUuid_type' => gettype($organizationUuid),
]
);
} catch (\Exception $e) {
$this->_logger->warning(
'Failed to set active organisation in OpenRegister config, but core config was successful',
[
- 'username' => $user->getUID(),
+ 'username' => $user->getUID(),
'organizationUuid' => $organizationUuidStr,
- 'error' => $e->getMessage()
+ 'error' => $e->getMessage(),
]
);
- }
- }
+ }//end try
+ }//end if
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to store organization UUID in user config: ' . $e->getMessage(),
+ 'Failed to store organization UUID in user config: '.$e->getMessage(),
[
- 'username' => $user->getUID(),
- 'organizationUuid' => $organizationUuid,
+ 'username' => $user->getUID(),
+ 'organizationUuid' => $organizationUuid,
'organizationUuid_type' => gettype($organizationUuid),
- 'exception' => $e
+ 'exception' => $e,
]
);
- }
- }
+ }//end try
+ }//end storeUserOrganizationUuid()
/**
* Gets a display name from contact data
@@ -1017,14 +1206,21 @@ private function storeUserOrganizationUuid(IUser $user, string|int $organization
*/
private function getDisplayNameFromContactData(array $contactData): string
{
- $parts = array_filter([
- $contactData['voornaam'] ?? '',
- $contactData['tussenvoegsel'] ?? '',
- $contactData['achternaam'] ?? ''
- ]);
+ $parts = array_filter(
+ [
+ $contactData['voornaam'] ?? '',
+ $contactData['tussenvoegsel'] ?? '',
+ $contactData['achternaam'] ?? '',
+ ]
+ );
- return implode(' ', $parts) ?: ($contactData['email'] ?? $contactData['e-mailadres'] ?? 'Unknown User');
- }
+ $fullName = implode(' ', $parts);
+ if (empty($fullName) === false) {
+ return $fullName;
+ } else {
+ return ($contactData['email'] ?? $contactData['e-mailadres'] ?? 'Unknown User');
+ }
+ }//end getDisplayNameFromContactData()
/**
* Stores contact person name fields in Nextcloud user config
@@ -1043,39 +1239,39 @@ public function storeContactNameFields(\OCP\IUser $user, array $contactData): vo
try {
$userId = $user->getUID();
- // Store name fields in Nextcloud user config (core app)
- // These are read by OpenRegister UserService::getCustomNameFields()
- $firstName = $contactData['voornaam'] ?? '';
+ // Store name fields in Nextcloud user config (core app).
+ // These are read by OpenRegister UserService::getCustomNameFields().
+ $firstName = $contactData['voornaam'] ?? '';
$middleName = $contactData['tussenvoegsel'] ?? '';
- $lastName = $contactData['achternaam'] ?? '';
- $functie = $contactData['functie'] ?? '';
+ $lastName = $contactData['achternaam'] ?? '';
+ $functie = $contactData['functie'] ?? '';
- if (!empty($firstName)) {
+ if (empty($firstName) === false) {
$this->config->setUserValue($userId, 'core', 'firstName', $firstName);
}
- if (!empty($lastName)) {
+ if (empty($lastName) === false) {
$this->config->setUserValue($userId, 'core', 'lastName', $lastName);
}
- if (!empty($middleName)) {
+ if (empty($middleName) === false) {
$this->config->setUserValue($userId, 'core', 'middleName', $middleName);
}
- // Store functie in AccountManager as 'role' property
- // This is read by OpenRegister UserService via AccountManager
- if (!empty($functie)) {
+ // Store functie in AccountManager as 'role' property.
+ // This is read by OpenRegister UserService via AccountManager.
+ if (empty($functie) === false) {
try {
$accountManager = $this->_container->get('OCP\Accounts\IAccountManager');
- $account = $accountManager->getAccount($user);
+ $account = $accountManager->getAccount($user);
- // Try to set the role property
+ // Try to set the role property.
$roleProperty = $account->getProperty(\OCP\Accounts\IAccountManager::PROPERTY_ROLE);
if ($roleProperty !== null) {
$roleProperty->setValue($functie);
$accountManager->updateAccount($account);
} else {
- // Property doesn't exist, create it
+ // Property doesn't exist, create it.
$account->setProperty(
\OCP\Accounts\IAccountManager::PROPERTY_ROLE,
$functie,
@@ -1085,41 +1281,52 @@ public function storeContactNameFields(\OCP\IUser $user, array $contactData): vo
$accountManager->updateAccount($account);
}
} catch (\Exception $e) {
- // Fallback: store functie in user config if AccountManager fails
+ // Fallback: store functie in user config if AccountManager fails.
$this->config->setUserValue($userId, 'core', 'functie', $functie);
- $this->_logger->warning('Failed to store functie in AccountManager, stored in user config', [
- 'userId' => $userId,
- 'functie' => $functie,
- 'error' => $e->getMessage()
- ]);
- }
- }
+ $this->_logger->warning(
+ 'Failed to store functie in AccountManager, stored in user config',
+ [
+ 'userId' => $userId,
+ 'functie' => $functie,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end if
// Sync e-mailadres to user's email property.
$email = $contactData['e-mailadres'] ?? $contactData['email'] ?? '';
- if (!empty($email) && $email !== $user->getEMailAddress()) {
+ if (empty($email) === false && $email !== $user->getEMailAddress()) {
$user->setEMailAddress($email);
- $this->_logger->info('Updated user email from contactpersoon', [
- 'userId' => $userId,
- 'email' => $email
- ]);
+ $this->_logger->info(
+ 'Updated user email from contactpersoon',
+ [
+ 'userId' => $userId,
+ 'email' => $email,
+ ]
+ );
}
- $this->_logger->info('Stored contact name fields in user config', [
- 'userId' => $userId,
- 'firstName' => $firstName,
- 'middleName' => $middleName,
- 'lastName' => $lastName,
- 'functie' => $functie
- ]);
-
+ $this->_logger->info(
+ 'Stored contact name fields in user config',
+ [
+ 'userId' => $userId,
+ 'firstName' => $firstName,
+ 'middleName' => $middleName,
+ 'lastName' => $lastName,
+ 'functie' => $functie,
+ ]
+ );
} catch (\Exception $e) {
- $this->_logger->error('Failed to store contact name fields', [
- 'userId' => $user->getUID(),
- 'error' => $e->getMessage()
- ]);
- }
- }
+ $this->_logger->error(
+ 'Failed to store contact name fields',
+ [
+ 'userId' => $user->getUID(),
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end storeContactNameFields()
/**
* Handles new contact creation
@@ -1131,23 +1338,25 @@ public function storeContactNameFields(\OCP\IUser $user, array $contactData): vo
public function handleNewContact(object $contactObject): void
{
try {
- $this->_logger->info('Handling new contact', [
- 'objectId' => $contactObject->getId()
- ]);
-
- // Process the contact to ensure proper user structure
- $this->processContactpersoon($contactObject);
+ $this->_logger->info(
+ 'Handling new contact',
+ [
+ 'objectId' => $contactObject->getId(),
+ ]
+ );
+ // Process the contact to ensure proper user structure.
+ $this->processContactpersoon(contactpersoonObject: $contactObject);
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to handle new contact: ' . $e->getMessage(),
+ 'Failed to handle new contact: '.$e->getMessage(),
[
- 'objectId' => $contactObject->getId(),
- 'exception' => $e
+ 'objectId' => $contactObject->getId(),
+ 'exception' => $e,
]
);
}
- }
+ }//end handleNewContact()
/**
* Handles contact update
@@ -1159,23 +1368,25 @@ public function handleNewContact(object $contactObject): void
public function handleContactUpdate(object $contactObject): void
{
try {
- $this->_logger->info('Handling contact update', [
- 'objectId' => $contactObject->getId()
- ]);
-
- // Process the updated contact
- $this->processContactpersoon($contactObject);
+ $this->_logger->info(
+ 'Handling contact update',
+ [
+ 'objectId' => $contactObject->getId(),
+ ]
+ );
+ // Process the updated contact.
+ $this->processContactpersoon(contactpersoonObject: $contactObject);
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to handle contact update: ' . $e->getMessage(),
+ 'Failed to handle contact update: '.$e->getMessage(),
[
- 'objectId' => $contactObject->getId(),
- 'exception' => $e
+ 'objectId' => $contactObject->getId(),
+ 'exception' => $e,
]
);
}
- }
+ }//end handleContactUpdate()
/**
* Handles contact deletion
@@ -1187,92 +1398,96 @@ public function handleContactUpdate(object $contactObject): void
public function handleContactDeletion(object $contactObject): void
{
try {
- $this->_logger->info('Handling contact deletion', [
- 'objectId' => $contactObject->getId()
- ]);
+ $this->_logger->info(
+ 'Handling contact deletion',
+ [
+ 'objectId' => $contactObject->getId(),
+ ]
+ );
- // Get the contact data before deletion
+ // Get the contact data before deletion.
$objectData = $contactObject->getObject();
- $username = $objectData['username'] ?? '';
+ $username = $objectData['username'] ?? '';
- if (!empty($username)) {
+ if (empty($username) === false) {
$user = $this->_userManager->get($username);
- if ($user) {
- // Option 1: Delete the user account
- // $user->delete();
-
- // Option 2: Just disable the user
+ if (empty($user) === false) {
+ // Option 1: Delete the user account.
+ // $user->delete();.
+ // Option 2: Just disable the user.
$user->setEnabled(false);
$this->_logger->info(
'User account disabled due to contact deletion',
[
- 'username' => $username,
- 'contactId' => $contactObject->getId()
+ 'username' => $username,
+ 'contactId' => $contactObject->getId(),
]
);
- // Send account suspension notification email
- $this->sendAccountSuspensionEmail($user, $objectData);
+ // Send account suspension notification email.
+ $this->sendAccountSuspensionEmail(user: $user, objectData: $objectData);
}
- }
-
+ }//end if
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to handle contact deletion: ' . $e->getMessage(),
+ 'Failed to handle contact deletion: '.$e->getMessage(),
[
- 'objectId' => $contactObject->getId(),
- 'exception' => $e
+ 'objectId' => $contactObject->getId(),
+ 'exception' => $e,
]
);
- }
- }
+ }//end try
+ }//end handleContactDeletion()
/**
* Assigns beheerder role to a user
*
* @param object $contactpersoonObject The contactpersoon object
- * @param string $username The username
- * @param string $organizationUuid The organization UUID
+ * @param string $username The username
+ * @param string $organizationUuid The organization UUID
*
* @return void
*/
public function assignBeheerderRole(object $contactpersoonObject, string $username, string $organizationUuid): void
{
try {
- $objectData = $contactpersoonObject->getObject();
+ $objectData = $contactpersoonObject->getObject();
$currentRoles = $objectData['roles'] ?? [];
- if (!is_array($currentRoles)) {
+ if (is_array($currentRoles) === false) {
$currentRoles = [];
}
- // Add beheerder role if not already present
- if (!in_array('beheerder', array_map('strtolower', $currentRoles))) {
+ // Add beheerder role if not already present.
+ if (in_array('beheerder', array_map('strtolower', $currentRoles)) === false) {
$currentRoles[] = 'beheerder';
- // Update the contactpersoon object (but don't save to prevent event loops)
+ // Update the contactpersoon object (but don't save to prevent event loops).
$objectData['roles'] = $currentRoles;
$contactpersoonObject->setObject($objectData);
- // Note: NOT saving the object here to prevent infinite event loops
- // The original API call/operation will handle persistence
- $this->_logger->info('Beheerder role added to contactpersoon object, but not saved to prevent event loops', [
- 'username' => $username,
- 'organizationUuid' => $organizationUuid,
- 'updatedRoles' => $currentRoles,
- 'objectId' => $contactpersoonObject->getId()
- ]);
+ // Note: NOT saving the object here to prevent infinite event loops.
+ // The original API call/operation will handle persistence.
+ $this->_logger->info(
+ 'Beheerder role added to contactpersoon object, but not saved to prevent event loops',
+ [
+ 'username' => $username,
+ 'organizationUuid' => $organizationUuid,
+ 'updatedRoles' => $currentRoles,
+ 'objectId' => $contactpersoonObject->getId(),
+ ]
+ );
- // Add user to beheerder group
+ // Add user to beheerder group.
$beheerderGroup = $this->_groupManager->get('beheerder');
- if (!$beheerderGroup) {
+ if ($beheerderGroup === null) {
$beheerderGroup = $this->_groupManager->createGroup('beheerder');
}
- if ($beheerderGroup) {
+ if (empty($beheerderGroup) === false) {
$user = $this->_userManager->get($username);
- if ($user && !$beheerderGroup->inGroup($user)) {
+ if ($user !== false && $beheerderGroup->inGroup($user) === false) {
$beheerderGroup->addUser($user);
}
}
@@ -1280,29 +1495,28 @@ public function assignBeheerderRole(object $contactpersoonObject, string $userna
$this->_logger->info(
'Assigned beheerder role to first user in organization',
[
- 'username' => $username,
+ 'username' => $username,
'organization' => $organizationUuid,
- 'newRoles' => $currentRoles
+ 'newRoles' => $currentRoles,
]
);
- }
-
+ }//end if
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to assign beheerder role: ' . $e->getMessage(),
+ 'Failed to assign beheerder role: '.$e->getMessage(),
[
- 'username' => $username,
+ 'username' => $username,
'organization' => $organizationUuid,
- 'exception' => $e
+ 'exception' => $e,
]
);
- }
- }
+ }//end try
+ }//end assignBeheerderRole()
/**
* Sets a user's manager in Nextcloud
*
- * @param string $username The username
+ * @param string $username The username
* @param string $managerUsername The manager's username
*
* @return void
@@ -1310,24 +1524,24 @@ public function assignBeheerderRole(object $contactpersoonObject, string $userna
public function setUserManager(string $username, string $managerUsername): void
{
try {
- $user = $this->_userManager->get($username);
+ $user = $this->_userManager->get($username);
$manager = $this->_userManager->get($managerUsername);
- if (!$user || !$manager) {
+ if ($user === null || $manager === false) {
$this->_logger->warning(
'Cannot set manager - user or manager not found',
[
- 'username' => $username,
- 'manager' => $managerUsername,
- 'userExists' => $user !== null,
- 'managerExists' => $manager !== null
+ 'username' => $username,
+ 'manager' => $managerUsername,
+ 'userExists' => $user !== null,
+ 'managerExists' => $manager !== null,
]
);
return;
}
- // In Nextcloud, we can set this as a user preference or custom attribute
- // Since there's no built-in manager field, we'll use preferences
+ // In Nextcloud, we can set this as a user preference or custom attribute.
+ // Since there's no built-in manager field, we'll use preferences.
\OC::$server->getConfig()->setUserValue(
$username,
'softwarecatalog',
@@ -1339,21 +1553,20 @@ public function setUserManager(string $username, string $managerUsername): void
'Set user manager',
[
'username' => $username,
- 'manager' => $managerUsername
+ 'manager' => $managerUsername,
]
);
-
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to set user manager: ' . $e->getMessage(),
+ 'Failed to set user manager: '.$e->getMessage(),
[
- 'username' => $username,
- 'manager' => $managerUsername,
- 'exception' => $e
+ 'username' => $username,
+ 'manager' => $managerUsername,
+ 'exception' => $e,
]
);
- }
- }
+ }//end try
+ }//end setUserManager()
/**
* Gets a user's manager
@@ -1372,19 +1585,22 @@ public function getUserManager(string $username): ?string
''
);
- return !empty($manager) ? $manager : null;
-
+ if (empty($manager) === false) {
+ return $manager;
+ } else {
+ return null;
+ }
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to get user manager: ' . $e->getMessage(),
+ 'Failed to get user manager: '.$e->getMessage(),
[
- 'username' => $username,
- 'exception' => $e
+ 'username' => $username,
+ 'exception' => $e,
]
);
return null;
- }
- }
+ }//end try
+ }//end getUserManager()
/**
* Gets the organization type for a given organization ID
@@ -1396,20 +1612,23 @@ public function getUserManager(string $username): ?string
private function getOrganizationType(string $organizationId): string
{
try {
- // Get the organization object to find its type
- $objectService = $this->_getObjectService();
+ // Get the organization object to find its type.
+ $objectService = $this->getObjectService();
- $this->_logger->info('Getting organization type', [
- 'organizationId' => $organizationId
- ]);
+ $this->_logger->info(
+ 'Getting organization type',
+ [
+ 'organizationId' => $organizationId,
+ ]
+ );
- // Get voorzieningen config for register and schema
- $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
+ // Get voorzieningen config for register and schema.
+ $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
$voorzieningenConfig = $settingsService->getVoorzieningenConfig();
- $register = $voorzieningenConfig['register'] ?? '';
- $organizationSchema = $voorzieningenConfig['organisatie_schema'] ?? '';
+ $register = $voorzieningenConfig['register'] ?? '';
+ $organizationSchema = $voorzieningenConfig['organisatie_schema'] ?? '';
- // Find by UUID - use find() with register and schema
+ // Find by UUID - use find() with register and schema.
$organizationObject = $objectService->find(
id: $organizationId,
register: $register,
@@ -1418,37 +1637,43 @@ private function getOrganizationType(string $organizationId): string
_multitenancy: false
);
- if ($organizationObject) {
+ if (empty($organizationObject) === false) {
$organizationData = $organizationObject->getObject();
$organizationType = $organizationData['type'] ?? '';
-
- $this->_logger->info('Found organization type', [
- 'organizationId' => $organizationId,
- 'type' => $organizationType,
- 'normalizedType' => strtolower($organizationType)
- ]);
-
- return $organizationType; // Don't convert to lowercase here, let getRoleGroupByOrganizationType handle it
+
+ $this->_logger->info(
+ 'Found organization type',
+ [
+ 'organizationId' => $organizationId,
+ 'type' => $organizationType,
+ 'normalizedType' => strtolower($organizationType),
+ ]
+ );
+
+ return $organizationType;
+ // Don't convert to lowercase here, let getRoleGroupByOrganizationType handle it.
}
- $this->_logger->warning('Organization not found', [
- 'organizationId' => $organizationId
- ]);
+ $this->_logger->warning(
+ 'Organization not found',
+ [
+ 'organizationId' => $organizationId,
+ ]
+ );
return '';
-
} catch (\Exception $e) {
- $this->_logger->error(
- 'Failed to get organization type: ' . $e->getMessage(),
+ // "Object not found" is expected for NC org UUIDs that don't have a matching.
+ // register organisatie object (e.g. orgs created outside the sync process).
+ // Log at warning level — the caller handles '' gracefully.
+ $this->_logger->warning(
+ 'Could not determine organization type (org may not be synced yet): '.$e->getMessage(),
[
'organizationId' => $organizationId,
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine()
]
);
return '';
- }
- }
+ }//end try
+ }//end getOrganizationType()
/**
* Maps organization type to role group based on business rules
@@ -1459,29 +1684,29 @@ private function getOrganizationType(string $organizationId): string
*/
private function getRoleGroupByOrganizationType(string $organizationType): string
{
- // Normalize the organization type to lowercase for comparison
+ // Normalize the organization type to lowercase for comparison.
$normalizedType = strtolower(trim($organizationType));
-
- // Define the mapping based on requirements:
- // "Gemeente" -> "gebruik-beheerder"
- // "Leverancier" -> "aanbod-beheerder"
- // "Samenwerking" -> "gebruik-beheerder"
- // "Community" -> "aanbod-beheerder"
+
+ // Define the mapping based on requirements:.
+ // "Gemeente" -> "gebruik-beheerder".
+ // "Leverancier" -> "aanbod-beheerder".
+ // "Samenwerking" -> "gebruik-beheerder".
+ // "Community" -> "aanbod-beheerder".
$typeToRoleMapping = [
- 'gemeente' => 'gebruik-beheerder',
- 'leverancier' => 'aanbod-beheerder',
+ 'gemeente' => 'gebruik-beheerder',
+ 'leverancier' => 'aanbod-beheerder',
'samenwerking' => 'gebruik-beheerder',
- 'community' => 'aanbod-beheerder'
+ 'community' => 'aanbod-beheerder',
];
return $typeToRoleMapping[$normalizedType] ?? '';
- }
+ }//end getRoleGroupByOrganizationType()
/**
* Sends user creation email
*
- * @param \OCP\IUser $user The created user
- * @param array $objectData The contact person data
+ * @param \OCP\IUser $user The created user
+ * @param array $objectData The contact person data
*
* @return void
*/
@@ -1489,76 +1714,93 @@ private function sendUserCreationEmail(\OCP\IUser $user, array $objectData): voi
{
try {
- $this->_logger->info('Sending user creation email', [
- 'username' => $user->getUID(),
- 'email' => $user->getEMailAddress()
- ]);
+ $this->_logger->info(
+ 'Sending user creation email',
+ [
+ 'username' => $user->getUID(),
+ 'email' => $user->getEMailAddress(),
+ ]
+ );
- // Prepare user data for email
+ // Prepare user data for email.
$userData = [
- 'username' => $user->getUID(),
- 'email' => $user->getEMailAddress(),
+ 'username' => $user->getUID(),
+ 'email' => $user->getEMailAddress(),
'displayName' => $user->getDisplayName(),
- 'voornaam' => $objectData['voornaam'] ?? '',
- 'achternaam' => $objectData['achternaam'] ?? '',
- 'roles' => $objectData['roles'] ?? []
+ 'voornaam' => $objectData['voornaam'] ?? '',
+ 'achternaam' => $objectData['achternaam'] ?? '',
+ 'roles' => $objectData['roles'] ?? [],
];
- // Get organization data if available
+ // Get organization data if available.
$organizationData = [];
- $organizationId = $objectData['organisation'] ?? $objectData['organisatie'] ?? '';
- if (!empty($organizationId)) {
+ $organizationId = $objectData['organisation'] ?? $objectData['organisatie'] ?? '';
+ if (empty($organizationId) === false) {
try {
- $objectService = $this->_getObjectService();
- // Get register and schema IDs dynamically from configuration
- $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
- $registerId = $settingsService->getVoorzieningenRegisterId();
+ $objectService = $this->getObjectService();
+ // Get register and schema IDs dynamically from configuration.
+ $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
+ $registerId = $settingsService->getVoorzieningenRegisterId();
$organisatieSchemaId = $settingsService->getSchemaIdForObjectType('organisatie');
- if (!$registerId || !$organisatieSchemaId) {
+ if ($registerId === null || $organisatieSchemaId === false) {
$this->_logger->warning('Register or schema ID not configured for organisatie');
return;
}
$organizationObject = $objectService->find($organizationId, [], false, $registerId, $organisatieSchemaId);
- if ($organizationObject) {
+ if (empty($organizationObject) === false) {
$organizationData = $organizationObject->getObject();
- $this->_logger->info('Retrieved organization data for email', [
- 'organizationId' => $organizationId,
- 'organizationUuid' => $organizationData['id'] ?? 'NOT_SET',
- 'organizationName' => $organizationData['naam'] ?? 'NOT_SET'
- ]);
+ $this->_logger->info(
+ 'Retrieved organization data for email',
+ [
+ 'organizationId' => $organizationId,
+ 'organizationUuid' => $organizationData['id'] ?? 'NOT_SET',
+ 'organizationName' => $organizationData['naam'] ?? 'NOT_SET',
+ ]
+ );
}
} catch (\Exception $e) {
- $this->_logger->warning('Failed to get organization data for email: ' . $e->getMessage(), [
- 'organizationId' => $organizationId
- ]);
- }
- }
+ $this->_logger->warning(
+ 'Failed to get organization data for email: '.$e->getMessage(),
+ [
+ 'organizationId' => $organizationId,
+ ]
+ );
+ }//end try
+ }//end if
- // Send user creation email
+ // Send user creation email.
$success = $this->_emailService->sendUserCreationEmail($userData, $organizationData);
- if ($success) {
- $this->_logger->info('User creation email sent successfully', [
- 'username' => $user->getUID(),
- 'email' => $user->getEMailAddress()
- ]);
+ if (empty($success) === false) {
+ $this->_logger->info(
+ 'User creation email sent successfully',
+ [
+ 'username' => $user->getUID(),
+ 'email' => $user->getEMailAddress(),
+ ]
+ );
} else {
- $this->_logger->warning('Failed to send user creation email', [
- 'username' => $user->getUID(),
- 'email' => $user->getEMailAddress()
- ]);
+ $this->_logger->warning(
+ 'Failed to send user creation email',
+ [
+ 'username' => $user->getUID(),
+ 'email' => $user->getEMailAddress(),
+ ]
+ );
}
-
} catch (\Exception $e) {
- $this->_logger->error('Exception sending user creation email: ' . $e->getMessage(), [
- 'username' => $user->getUID(),
- 'email' => $user->getEMailAddress(),
- 'exception' => $e
- ]);
- }
- }
+ $this->_logger->error(
+ 'Exception sending user creation email: '.$e->getMessage(),
+ [
+ 'username' => $user->getUID(),
+ 'email' => $user->getEMailAddress(),
+ 'exception' => $e,
+ ]
+ );
+ }//end try
+ }//end sendUserCreationEmail()
/**
* Processes a contactpersoon object to create an inactive user
@@ -1567,123 +1809,138 @@ private function sendUserCreationEmail(\OCP\IUser $user, array $objectData): voi
* this method will create an inactive user account and set the username property.
*
* @param object $contactpersoonObject The contactpersoon object to process
- * @param bool $isUpdate Whether this is an update operation (defaults to false)
+ * @param bool $isUpdate Whether this is an update operation (defaults to false)
*
* @return bool True if processing was successful
* @throws \Exception If processing fails
*/
- public function processContactpersoon(object $contactpersoonObject, bool $isUpdate = false): bool
+ public function processContactpersoon(object $contactpersoonObject, bool $isUpdate=false): bool
{
try {
- $this->_logger->info('Processing contactpersoon object', [
- 'objectId' => $contactpersoonObject->getId(),
- 'isUpdate' => $isUpdate
- ]);
+ $this->_logger->info(
+ 'Processing contactpersoon object',
+ [
+ 'objectId' => $contactpersoonObject->getId(),
+ 'isUpdate' => $isUpdate,
+ ]
+ );
- // Get object data
+ // Get object data.
$objectData = $contactpersoonObject->getObject();
- // Check if username exists and is filled
+ // Check if username exists and is filled.
$username = $objectData['username'] ?? '';
- if (empty($username)) {
+ if (empty($username) === true) {
$this->_logger->info('Username not found or empty, creating inactive user account');
- // Generate username from name fields
- $username = $this->generateUsernameFromContactData($objectData);
+ // Generate username from name fields.
+ $username = $this->generateUsernameFromContactData(contactData: $objectData);
- // For updates, try to find existing user first to avoid expensive isFirstContactForOrganization check
- if ($isUpdate) {
+ // For updates, try to find existing user first to avoid expensive isFirstContactForOrganization check.
+ if (empty($isUpdate) === false) {
$existingUser = $this->_userManager->get($username);
- if ($existingUser) {
- $this->_logger->info('Found existing user during update, skipping expensive first contact check', [
- 'username' => $username,
- 'objectId' => $contactpersoonObject->getId()
- ]);
-
- // Update the contactpersoon object with the username (but don't save to prevent event loops)
+ if (empty($existingUser) === false) {
+ $this->_logger->info(
+ 'Found existing user during update, skipping expensive first contact check',
+ [
+ 'username' => $username,
+ 'objectId' => $contactpersoonObject->getId(),
+ ]
+ );
+
+ // Update the contactpersoon object with the username (but don't save to prevent event loops).
$objectData['username'] = $username;
$contactpersoonObject->setObject($objectData);
- $this->_logger->info('Username added to contactpersoon object during update, but not saved to prevent event loops', [
- 'username' => $username,
- 'objectId' => $contactpersoonObject->getId()
- ]);
+ $this->_logger->info(
+ 'Username added to contactpersoon object during update, but not saved to prevent event loops',
+ [
+ 'username' => $username,
+ 'objectId' => $contactpersoonObject->getId(),
+ ]
+ );
- // Ensure contactpersoon is added to organization
- $this->ensureContactpersoonInOrganization($contactpersoonObject);
+ // Ensure contactpersoon is added to organization.
+ $this->ensureContactpersoonInOrganization(contactpersoonObject: $contactpersoonObject);
return true;
- }
- }
+ }//end if
+ }//end if
- // Determine if this is the first contact for the organization (expensive operation)
- $isFirstContact = $this->isFirstContactForOrganization($contactpersoonObject, $objectData);
+ // Determine if this is the first contact for the organization (expensive operation).
+ $isFirstContact = $this->isFirstContactForOrganization(contactObject: $contactpersoonObject, objectData: $objectData);
- // Create the user account
- $user = $this->createUserAccount($contactpersoonObject, $isFirstContact);
+ // Create the user account.
+ $user = $this->createUserAccount(contactpersoonObject: $contactpersoonObject, isFirstContact: $isFirstContact);
if ($user === null) {
throw new \Exception('Failed to create user account');
}
- // Set user to inactive initially
- $this->setUserInactive($user->getUID());
+ // Set user to inactive initially.
+ $this->setUserInactive(username: $user->getUID());
- // Update the contactpersoon object with the username (but don't save to prevent event loops)
+ // Update the contactpersoon object with the username (but don't save to prevent event loops).
$objectData['username'] = $username;
$contactpersoonObject->setObject($objectData);
- // Note: NOT saving the object here to prevent infinite event loops
- // The original API call/operation will handle persistence
- $this->_logger->info('Username added to contactpersoon object, but not saved to prevent event loops', [
- 'username' => $username,
- 'objectId' => $contactpersoonObject->getId()
- ]);
+ // Note: NOT saving the object here to prevent infinite event loops.
+ // The original API call/operation will handle persistence.
+ $this->_logger->info(
+ 'Username added to contactpersoon object, but not saved to prevent event loops',
+ [
+ 'username' => $username,
+ 'objectId' => $contactpersoonObject->getId(),
+ ]
+ );
- // Ensure contactpersoon is added to organization
- $this->ensureContactpersoonInOrganization($contactpersoonObject);
+ // Ensure contactpersoon is added to organization.
+ $this->ensureContactpersoonInOrganization(contactpersoonObject: $contactpersoonObject);
- // Also add user to organization entity (OpenRegister entity, not object)
+ // Also add user to organization entity (OpenRegister entity, not object).
$organizationUuid = $objectData['organisation'] ?? $objectData['organisatie'] ?? '';
- $this->addUserToOrganizationEntity($contactpersoonObject, $username, $organizationUuid);
+ $this->addUserToOrganizationEntity(
+ contactpersoonObject: $contactpersoonObject,
+ username: $username,
+ organizationUuidOverride: $organizationUuid
+ );
$this->_logger->info(
'Successfully created inactive user and updated contactpersoon',
[
'username' => $username,
- 'objectId' => $contactpersoonObject->getId()
+ 'objectId' => $contactpersoonObject->getId(),
]
);
return true;
- }
+ }//end if
$this->_logger->info(
'Username already exists, contactpersoon processed',
[
'username' => $username,
- 'objectId' => $contactpersoonObject->getId()
+ 'objectId' => $contactpersoonObject->getId(),
]
);
- // Ensure contactpersoon is added to organization (even for existing users)
- $this->ensureContactpersoonInOrganization($contactpersoonObject);
+ // Ensure contactpersoon is added to organization (even for existing users).
+ $this->ensureContactpersoonInOrganization(contactpersoonObject: $contactpersoonObject);
return true;
-
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to process contactpersoon object: ' . $e->getMessage(),
+ 'Failed to process contactpersoon object: '.$e->getMessage(),
[
'exception' => $e,
- 'objectId' => $contactpersoonObject->getId() ?? 'unknown'
+ 'objectId' => $contactpersoonObject->getId() ?? 'unknown',
]
);
throw $e;
- }
- }
+ }//end try
+ }//end processContactpersoon()
/**
* Sets a user account to inactive
@@ -1697,13 +1954,13 @@ public function setUserInactive(string $username): bool
try {
$user = $this->_userManager->get($username);
- if ($user) {
+ if (empty($user) === false) {
$user->setEnabled(false);
$this->_logger->info(
'Set user account to inactive',
[
- 'username' => $username
+ 'username' => $username,
]
);
@@ -1712,25 +1969,24 @@ public function setUserInactive(string $username): bool
$this->_logger->warning(
'User not found when trying to set inactive',
[
- 'username' => $username
+ 'username' => $username,
]
);
return false;
- }
-
+ }//end if
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to set user inactive: ' . $e->getMessage(),
+ 'Failed to set user inactive: '.$e->getMessage(),
[
- 'username' => $username,
- 'exception' => $e
+ 'username' => $username,
+ 'exception' => $e,
]
);
return false;
- }
- }
+ }//end try
+ }//end setUserInactive()
/**
* Sets a user account to active
@@ -1744,13 +2000,13 @@ public function setUserActive(string $username): bool
try {
$user = $this->_userManager->get($username);
- if ($user) {
+ if (empty($user) === false) {
$user->setEnabled(true);
$this->_logger->info(
'Set user account to active',
[
- 'username' => $username
+ 'username' => $username,
]
);
@@ -1759,30 +2015,29 @@ public function setUserActive(string $username): bool
$this->_logger->warning(
'User not found when trying to set active',
[
- 'username' => $username
+ 'username' => $username,
]
);
return false;
- }
-
+ }//end if
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to set user active: ' . $e->getMessage(),
+ 'Failed to set user active: '.$e->getMessage(),
[
- 'username' => $username,
- 'exception' => $e
+ 'username' => $username,
+ 'exception' => $e,
]
);
return false;
- }
- }
+ }//end try
+ }//end setUserActive()
/**
* Handles contactpersoon updates, particularly role changes
*
- * @param object $contactpersoonObject The updated contactpersoon object
+ * @param object $contactpersoonObject The updated contactpersoon object
* @param object $oldContactpersoonObject The previous contactpersoon object
*
* @return void
@@ -1790,100 +2045,110 @@ public function setUserActive(string $username): bool
public function handleContactpersoonUpdate(object $contactpersoonObject, object $oldContactpersoonObject): void
{
try {
- $this->_logger->info('Handling contactpersoon update', [
- 'objectId' => $contactpersoonObject->getId()
- ]);
+ $this->_logger->info(
+ 'Handling contactpersoon update',
+ [
+ 'objectId' => $contactpersoonObject->getId(),
+ ]
+ );
- // Process the updated contactpersoon
- $this->processContactpersoon($contactpersoonObject);
+ // Process the updated contactpersoon.
+ $this->processContactpersoon(contactpersoonObject: $contactpersoonObject);
- // Check for role changes and update groups accordingly
+ // Check for role changes and update groups accordingly.
$newData = $contactpersoonObject->getObject();
$oldData = $oldContactpersoonObject->getObject();
$newRoles = $newData['roles'] ?? [];
$oldRoles = $oldData['roles'] ?? [];
- // Ensure both are arrays
- if (!is_array($newRoles)) {
+ // Ensure both are arrays.
+ if (is_array($newRoles) === false) {
$newRoles = [$newRoles];
}
- if (!is_array($oldRoles)) {
+
+ if (is_array($oldRoles) === false) {
$oldRoles = [$oldRoles];
}
- // Check if roles or organization have changed (organization type determines role assignment)
+ // Check if roles or organization have changed (organization type determines role assignment).
$oldOrganization = $oldData['organisation'] ?? $oldData['organisatie'] ?? '';
$newOrganization = $newData['organisation'] ?? $newData['organisatie'] ?? '';
-
+
if ($newRoles !== $oldRoles || $oldOrganization !== $newOrganization) {
$username = $newData['username'] ?? '';
- if (!empty($username)) {
+ if (empty($username) === false) {
$user = $this->_userManager->get($username);
- if ($user) {
+ if (empty($user) === false) {
$this->_logger->info(
'Contact person data changed, updating user groups based on organization type',
[
'contactpersoonId' => $contactpersoonObject->getId(),
- 'username' => $username,
- 'oldRoles' => $oldRoles,
- 'newRoles' => $newRoles,
- 'oldOrganization' => $oldOrganization,
- 'newOrganization' => $newOrganization
+ 'username' => $username,
+ 'oldRoles' => $oldRoles,
+ 'newRoles' => $newRoles,
+ 'oldOrganization' => $oldOrganization,
+ 'newOrganization' => $newOrganization,
]
);
- // Update user groups based on organization type (roles are now ignored)
- $this->updateUserGroupsFromContactData($user, $newData);
+ // Update user groups based on organization type (roles are now ignored).
+ $this->updateUserGroupsFromContactData(user: $user, contactData: $newData);
}
}
- }
-
+ }//end if
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to handle contactpersoon update: ' . $e->getMessage(),
+ 'Failed to handle contactpersoon update: '.$e->getMessage(),
[
- 'objectId' => $contactpersoonObject->getId(),
- 'exception' => $e
+ 'objectId' => $contactpersoonObject->getId(),
+ 'exception' => $e,
]
);
- }
- }
+ }//end try
+ }//end handleContactpersoonUpdate()
/**
* Sends account suspension notification email
*
- * @param \OCP\IUser $user The suspended user
- * @param array $objectData The contact person data
+ * @param \OCP\IUser $user The suspended user
+ * @param array $objectData The contact person data
*
* @return void
*/
private function sendAccountSuspensionEmail(\OCP\IUser $user, array $objectData): void
{
try {
- $this->_logger->info('Sending account suspension email', [
- 'username' => $user->getUID(),
- 'email' => $user->getEMailAddress()
- ]);
-
- // For now, we'll use a simple log message as the PhpEmailService
- // doesn't have a specific suspension email method yet
- // This can be extended later if needed
-
- $this->_logger->info('Account suspension email would be sent here', [
- 'username' => $user->getUID(),
- 'email' => $user->getEMailAddress(),
- 'displayName' => $user->getDisplayName()
- ]);
+ $this->_logger->info(
+ 'Sending account suspension email',
+ [
+ 'username' => $user->getUID(),
+ 'email' => $user->getEMailAddress(),
+ ]
+ );
+ // For now, we'll use a simple log message as the PhpEmailService.
+ // doesn't have a specific suspension email method yet.
+ // This can be extended later if needed.
+ $this->_logger->info(
+ 'Account suspension email would be sent here',
+ [
+ 'username' => $user->getUID(),
+ 'email' => $user->getEMailAddress(),
+ 'displayName' => $user->getDisplayName(),
+ ]
+ );
} catch (\Exception $e) {
- $this->_logger->error('Exception sending account suspension email: ' . $e->getMessage(), [
- 'username' => $user->getUID(),
- 'email' => $user->getEMailAddress(),
- 'exception' => $e
- ]);
- }
- }
+ $this->_logger->error(
+ 'Exception sending account suspension email: '.$e->getMessage(),
+ [
+ 'username' => $user->getUID(),
+ 'email' => $user->getEMailAddress(),
+ 'exception' => $e,
+ ]
+ );
+ }//end try
+ }//end sendAccountSuspensionEmail()
/**
* Checks if a contactpersoon username is in the organization's users list
@@ -1895,66 +2160,70 @@ private function sendAccountSuspensionEmail(\OCP\IUser $user, array $objectData)
public function shouldAddContactpersoonToOrganization(object $contactpersoonObject): bool
{
try {
- $objectData = $contactpersoonObject->getObject();
- $username = $objectData['username'] ?? '';
+ $objectData = $contactpersoonObject->getObject();
+ $username = $objectData['username'] ?? '';
$organizationUuid = $objectData['organisation'] ?? '';
- if (empty($username) || empty($organizationUuid)) {
+ if (empty($username) === true || empty($organizationUuid) === true) {
return false;
}
- $objectService = $this->_getObjectService();
- if (!$objectService) {
+ $objectService = $this->getObjectService();
+ if ($objectService === null) {
return false;
}
- // Get the organization object
- $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
- $registerId = $settingsService->getVoorzieningenRegisterId();
+ // Get the organization object.
+ $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
+ $registerId = $settingsService->getVoorzieningenRegisterId();
$organisatieSchemaId = $settingsService->getSchemaIdForObjectType('organisatie');
- if (!$registerId || !$organisatieSchemaId) {
+ if ($registerId === null || $organisatieSchemaId === false) {
return false;
}
try {
$organizationObject = $objectService->find($organizationUuid, [], false, $registerId, $organisatieSchemaId);
- $organizationData = $organizationObject->getObject();
+ $organizationData = $organizationObject->getObject();
- // Check if the username is already in the organization's users
+ // Check if the username is already in the organization's users.
$organizationUsers = $organizationData['users'] ?? [];
- if (is_array($organizationUsers) && !in_array($username, $organizationUsers)) {
- $this->_logger->info('ContactPersonHandler: Contactpersoon should be added to organization', [
- 'username' => $username,
- 'organizationUuid' => $organizationUuid,
- 'currentUsers' => $organizationUsers
- ]);
+ if (is_array($organizationUsers) === true && in_array($username, $organizationUsers) === false) {
+ $this->_logger->info(
+ 'ContactPersonHandler: Contactpersoon should be added to organization',
+ [
+ 'username' => $username,
+ 'organizationUuid' => $organizationUuid,
+ 'currentUsers' => $organizationUsers,
+ ]
+ );
return true;
}
return false;
-
} catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
- // Organization doesn't exist, so we can't add the user
- $this->_logger->warning('ContactPersonHandler: Organization not found for contactpersoon', [
- 'username' => $username,
- 'organizationUuid' => $organizationUuid
- ]);
+ // Organization doesn't exist, so we can't add the user.
+ $this->_logger->warning(
+ 'ContactPersonHandler: Organization not found for contactpersoon',
+ [
+ 'username' => $username,
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
return false;
- }
-
+ }//end try
} catch (\Exception $e) {
$this->_logger->error(
- 'ContactPersonHandler: Failed to check if contactpersoon should be added to organization: ' . $e->getMessage(),
+ 'ContactPersonHandler: Failed to check if contactpersoon should be added to organization: '.$e->getMessage(),
[
- 'objectId' => $contactpersoonObject->getId(),
- 'exception' => $e->getMessage()
+ 'objectId' => $contactpersoonObject->getId(),
+ 'exception' => $e->getMessage(),
]
);
return false;
- }
- }
+ }//end try
+ }//end shouldAddContactpersoonToOrganization()
/**
* Adds a contactpersoon username to the organization's users list
@@ -1966,49 +2235,52 @@ public function shouldAddContactpersoonToOrganization(object $contactpersoonObje
public function addContactpersoonToOrganization(object $contactpersoonObject): bool
{
try {
- $objectData = $contactpersoonObject->getObject();
- $username = $objectData['username'] ?? '';
+ $objectData = $contactpersoonObject->getObject();
+ $username = $objectData['username'] ?? '';
$organizationUuid = $objectData['organisation'] ?? '';
- if (empty($username) || empty($organizationUuid)) {
- $this->_logger->warning('ContactPersonHandler: Cannot add contactpersoon to organization - missing username or organization', [
- 'username' => $username,
- 'organizationUuid' => $organizationUuid
- ]);
+ if (empty($username) === true || empty($organizationUuid) === true) {
+ $this->_logger->warning(
+ 'ContactPersonHandler: Cannot add contactpersoon to organization - missing username or organization',
+ [
+ 'username' => $username,
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
return false;
}
- $objectService = $this->_getObjectService();
- if (!$objectService) {
+ $objectService = $this->getObjectService();
+ if ($objectService === null) {
$this->_logger->error('ContactPersonHandler: OpenRegister ObjectService not available');
return false;
}
- // Get the organization object
- $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
- $registerId = $settingsService->getVoorzieningenRegisterId();
+ // Get the organization object.
+ $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
+ $registerId = $settingsService->getVoorzieningenRegisterId();
$organisatieSchemaId = $settingsService->getSchemaIdForObjectType('organisatie');
- if (!$registerId || !$organisatieSchemaId) {
+ if ($registerId === null || $organisatieSchemaId === false) {
$this->_logger->error('ContactPersonHandler: Register or schema not configured for organisatie');
return false;
}
try {
$organizationObject = $objectService->find($organizationUuid, [], false, $registerId, $organisatieSchemaId);
- $organizationData = $organizationObject->getObject();
+ $organizationData = $organizationObject->getObject();
- // Add the username to the organization's users list
+ // Add the username to the organization's users list.
$organizationUsers = $organizationData['users'] ?? [];
- if (!is_array($organizationUsers)) {
+ if (is_array($organizationUsers) === false) {
$organizationUsers = [];
}
- if (!in_array($username, $organizationUsers)) {
- $organizationUsers[] = $username;
+ if (in_array($username, $organizationUsers) === false) {
+ $organizationUsers[] = $username;
$organizationData['users'] = $organizationUsers;
- // Update the organization object
+ // Update the organization object.
$updatedOrganization = $objectService->saveObject(
$organizationData,
[],
@@ -2017,43 +2289,51 @@ public function addContactpersoonToOrganization(object $contactpersoonObject): b
$organizationUuid
);
- $this->_logger->info('ContactPersonHandler: Successfully added contactpersoon to organization', [
- 'username' => $username,
- 'organizationUuid' => $organizationUuid,
- 'updatedUsers' => $organizationUsers
- ]);
+ $this->_logger->info(
+ 'ContactPersonHandler: Successfully added contactpersoon to organization',
+ [
+ 'username' => $username,
+ 'organizationUuid' => $organizationUuid,
+ 'updatedUsers' => $organizationUsers,
+ ]
+ );
return true;
} else {
- $this->_logger->debug('ContactPersonHandler: Contactpersoon already in organization', [
- 'username' => $username,
- 'organizationUuid' => $organizationUuid
- ]);
- return true; // Already there, consider it successful
- }
-
+ $this->_logger->debug(
+ 'ContactPersonHandler: Contactpersoon already in organization',
+ [
+ 'username' => $username,
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
+ return true;
+ // Already there, consider it successful.
+ }//end if
} catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
- $this->_logger->error('ContactPersonHandler: Organization not found for contactpersoon', [
- 'username' => $username,
- 'organizationUuid' => $organizationUuid
- ]);
+ $this->_logger->error(
+ 'ContactPersonHandler: Organization not found for contactpersoon',
+ [
+ 'username' => $username,
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
return false;
- }
-
+ }//end try
} catch (\Exception $e) {
$this->_logger->error(
- 'ContactPersonHandler: Failed to add contactpersoon to organization: ' . $e->getMessage(),
+ 'ContactPersonHandler: Failed to add contactpersoon to organization: '.$e->getMessage(),
[
- 'objectId' => $contactpersoonObject->getId(),
+ 'objectId' => $contactpersoonObject->getId(),
'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
]
);
return false;
- }
- }
+ }//end try
+ }//end addContactpersoonToOrganization()
/**
* Ensures contactpersoon is added to organization after user creation/update
@@ -2065,43 +2345,54 @@ public function addContactpersoonToOrganization(object $contactpersoonObject): b
public function ensureContactpersoonInOrganization(object $contactpersoonObject): void
{
try {
- $this->_logger->info('ContactPersonHandler: Ensuring contactpersoon is in organization', [
- 'objectId' => $contactpersoonObject->getId()
- ]);
-
- // Check if user should be added to organization
- if ($this->shouldAddContactpersoonToOrganization($contactpersoonObject)) {
- // Add user to organization
- $result = $this->addContactpersoonToOrganization($contactpersoonObject);
-
- if ($result) {
- $this->_logger->info('ContactPersonHandler: Successfully ensured contactpersoon in organization', [
- 'objectId' => $contactpersoonObject->getId()
- ]);
+ $this->_logger->info(
+ 'ContactPersonHandler: Ensuring contactpersoon is in organization',
+ [
+ 'objectId' => $contactpersoonObject->getId(),
+ ]
+ );
+
+ // Check if user should be added to organization.
+ if ($this->shouldAddContactpersoonToOrganization(contactpersoonObject: $contactpersoonObject) === true) {
+ // Add user to organization.
+ $result = $this->addContactpersoonToOrganization(contactpersoonObject: $contactpersoonObject);
+
+ if (empty($result) === false) {
+ $this->_logger->info(
+ 'ContactPersonHandler: Successfully ensured contactpersoon in organization',
+ [
+ 'objectId' => $contactpersoonObject->getId(),
+ ]
+ );
} else {
- $this->_logger->warning('ContactPersonHandler: Failed to add contactpersoon to organization', [
- 'objectId' => $contactpersoonObject->getId()
- ]);
+ $this->_logger->warning(
+ 'ContactPersonHandler: Failed to add contactpersoon to organization',
+ [
+ 'objectId' => $contactpersoonObject->getId(),
+ ]
+ );
}
} else {
- $this->_logger->debug('ContactPersonHandler: Contactpersoon already in organization or no action needed', [
- 'objectId' => $contactpersoonObject->getId()
- ]);
- }
-
+ $this->_logger->debug(
+ 'ContactPersonHandler: Contactpersoon already in organization or no action needed',
+ [
+ 'objectId' => $contactpersoonObject->getId(),
+ ]
+ );
+ }//end if
} catch (\Exception $e) {
$this->_logger->error(
- 'ContactPersonHandler: Failed to ensure contactpersoon in organization: ' . $e->getMessage(),
+ 'ContactPersonHandler: Failed to ensure contactpersoon in organization: '.$e->getMessage(),
[
- 'objectId' => $contactpersoonObject->getId(),
+ 'objectId' => $contactpersoonObject->getId(),
'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
]
);
- }
- }
+ }//end try
+ }//end ensureContactpersoonInOrganization()
/**
* Adds a user to the organization entity (OpenRegister entity, not object)
@@ -2110,98 +2401,128 @@ public function ensureContactpersoonInOrganization(object $contactpersoonObject)
* before adding the user to it. If the entity doesn't exist, it will be created
* from the organization object data.
*
- * @param object $contactpersoonObject The contactpersoon object
- * @param string $username The username to add
+ * @param object $contactpersoonObject The contactpersoon object
+ * @param string $username The username to add
* @param string|null $organizationUuidOverride Optional organization UUID to use instead of extracting from object
* (useful when organisatie field was removed from object data)
*
* @return void
*/
- public function addUserToOrganizationEntity(object $contactpersoonObject, string $username, ?string $organizationUuidOverride = null): void
- {
+ public function addUserToOrganizationEntity(
+ object $contactpersoonObject,
+ string $username,
+ ?string $organizationUuidOverride=null
+ ): void {
try {
$objectData = $contactpersoonObject->getObject();
- // Use override if provided (useful when organisatie field was removed from object)
+ // Use override if provided (useful when organisatie field was removed from object).
$organizationUuid = $organizationUuidOverride ?? $objectData['organisation'] ?? $objectData['organisatie'] ?? '';
- if (empty($organizationUuid)) {
- $this->_logger->warning('ContactPersonHandler: No organization reference found for contact person', [
- 'objectId' => $contactpersoonObject->getId(),
- 'username' => $username
- ]);
+ if (empty($organizationUuid) === true) {
+ $this->_logger->warning(
+ 'ContactPersonHandler: No organization reference found for contact person',
+ [
+ 'objectId' => $contactpersoonObject->getId(),
+ 'username' => $username,
+ ]
+ );
return;
}
- $this->_logger->info('ContactPersonHandler: Adding user to organization entity', [
- 'objectId' => $contactpersoonObject->getId(),
- 'username' => $username,
- 'organizationUuid' => $organizationUuid
- ]);
+ $this->_logger->info(
+ 'ContactPersonHandler: Adding user to organization entity',
+ [
+ 'objectId' => $contactpersoonObject->getId(),
+ 'username' => $username,
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
try {
$organisationMapper = $this->_container->get('OCA\\OpenRegister\\Db\\OrganisationMapper');
-
- // Try to find the organisation entity
+
+ // Try to find the organisation entity.
try {
$organisation = $organisationMapper->findByUuid($organizationUuid);
-
- $this->_logger->info('ContactPersonHandler: Found existing organization entity', [
- 'organizationUuid' => $organizationUuid,
- 'organizationName' => $organisation->getName()
- ]);
+
+ $this->_logger->info(
+ 'ContactPersonHandler: Found existing organization entity',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'organizationName' => $organisation->getName(),
+ ]
+ );
} catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
- // Organization entity doesn't exist, create it from the object data
- $this->_logger->info('ContactPersonHandler: Organization entity not found, creating it', [
- 'organizationUuid' => $organizationUuid
- ]);
-
- $organisation = $this->ensureOrganizationEntity($organizationUuid);
-
- if (!$organisation) {
- $this->_logger->error('ContactPersonHandler: Failed to create organization entity', [
- 'organizationUuid' => $organizationUuid
- ]);
+ // Organization entity doesn't exist, create it from the object data.
+ $this->_logger->info(
+ 'ContactPersonHandler: Organization entity not found, creating it',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
+
+ $organisation = $this->ensureOrganizationEntity(organizationUuid: $organizationUuid);
+
+ if ($organisation === null) {
+ $this->_logger->error(
+ 'ContactPersonHandler: Failed to create organization entity',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
return;
}
- }
+ }//end try
- // Add user to the organisation entity
+ // Add user to the organisation entity.
$currentUsers = $organisation->getUsers() ?? [];
- if (!in_array($username, $currentUsers)) {
+ if (in_array($username, $currentUsers) === false) {
$currentUsers[] = $username;
$organisation->setUsers($currentUsers);
$organisationMapper->update($organisation);
- $this->_logger->info('ContactPersonHandler: Successfully added user to organization entity', [
- 'objectId' => $contactpersoonObject->getId(),
- 'username' => $username,
- 'organizationUuid' => $organizationUuid,
- 'totalUsers' => count($currentUsers)
- ]);
+ $this->_logger->info(
+ 'ContactPersonHandler: Successfully added user to organization entity',
+ [
+ 'objectId' => $contactpersoonObject->getId(),
+ 'username' => $username,
+ 'organizationUuid' => $organizationUuid,
+ 'totalUsers' => count($currentUsers),
+ ]
+ );
} else {
- $this->_logger->info('ContactPersonHandler: User already in organization entity', [
- 'objectId' => $contactpersoonObject->getId(),
- 'username' => $username,
- 'organizationUuid' => $organizationUuid
- ]);
- }
+ $this->_logger->info(
+ 'ContactPersonHandler: User already in organization entity',
+ [
+ 'objectId' => $contactpersoonObject->getId(),
+ 'username' => $username,
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
+ }//end if
} catch (\Exception $e) {
- $this->_logger->error('ContactPersonHandler: Failed to add user to organization entity', [
- 'objectId' => $contactpersoonObject->getId(),
- 'username' => $username,
- 'organizationUuid' => $organizationUuid,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
- }
+ $this->_logger->error(
+ 'ContactPersonHandler: Failed to add user to organization entity',
+ [
+ 'objectId' => $contactpersoonObject->getId(),
+ 'username' => $username,
+ 'organizationUuid' => $organizationUuid,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+ }//end try
} catch (\Exception $e) {
- $this->_logger->error('ContactPersonHandler: Exception in addUserToOrganizationEntity', [
- 'objectId' => $contactpersoonObject->getId(),
- 'username' => $username,
- 'error' => $e->getMessage()
- ]);
- }
- }
+ $this->_logger->error(
+ 'ContactPersonHandler: Exception in addUserToOrganizationEntity',
+ [
+ 'objectId' => $contactpersoonObject->getId(),
+ 'username' => $username,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end addUserToOrganizationEntity()
/**
* Ensures an organization entity exists in OpenRegister
@@ -2216,16 +2537,32 @@ public function addUserToOrganizationEntity(object $contactpersoonObject, string
private function ensureOrganizationEntity(string $organizationUuid): ?\OCA\OpenRegister\Db\Organisation
{
try {
- // Get the organization object from OpenRegister
- $objectService = $this->_getObjectService();
-
- // Get voorzieningen config for register and schema
- $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
+ // First check if an entity with this UUID already exists (defensive double-check).
+ $organisationMapper = $this->_container->get('OCA\\OpenRegister\\Db\\OrganisationMapper');
+ try {
+ $existing = $organisationMapper->findByUuid($organizationUuid);
+ $this->_logger->info(
+ 'ContactPersonHandler: Organization entity already exists (found by UUID)',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'organizationName' => $existing->getName(),
+ ]
+ );
+ return $existing;
+ } catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
+ // Expected — continue to create.
+ }
+
+ // Get the organization object from OpenRegister.
+ $objectService = $this->getObjectService();
+
+ // Get voorzieningen config for register and schema.
+ $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
$voorzieningenConfig = $settingsService->getVoorzieningenConfig();
- $register = $voorzieningenConfig['register'] ?? '';
- $organizationSchema = $voorzieningenConfig['organisatie_schema'] ?? '';
-
- // Find the organization object by UUID - use find() with register and schema
+ $register = $voorzieningenConfig['register'] ?? '';
+ $organizationSchema = $voorzieningenConfig['organisatie_schema'] ?? '';
+
+ // Find the organization object by UUID - use find() with register and schema.
$organizationObject = $objectService->find(
id: $organizationUuid,
register: $register,
@@ -2233,54 +2570,86 @@ private function ensureOrganizationEntity(string $organizationUuid): ?\OCA\OpenR
_rbac: false,
_multitenancy: false
);
-
- if (!$organizationObject) {
- $this->_logger->error('ContactPersonHandler: Organization object not found in OpenRegister', [
- 'organizationUuid' => $organizationUuid
- ]);
+
+ if ($organizationObject === null) {
+ $this->_logger->error(
+ 'ContactPersonHandler: Organization object not found in OpenRegister',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
return null;
}
-
+
$organizationData = $organizationObject->getObject();
-
- // Get organization name and description
- $organizationName = $organizationData['naam'] ?? $organizationData['name'] ?? 'Unknown Organization';
- $organizationDescription = $organizationData['beschrijving'] ?? $organizationData['beschrijvingLang'] ?? $organizationData['description'] ?? '';
-
- $this->_logger->info('ContactPersonHandler: Creating organization entity from object data', [
- 'organizationUuid' => $organizationUuid,
- 'organizationName' => $organizationName
- ]);
-
- // Create the organization entity using OrganisationService
+
+ // Get organization name and description.
+ // phpcs:ignore Generic.Files.LineLength.TooLong
+ $organizationName = $organizationData['naam'] ?? $organizationData['name'] ?? 'Unknown Organization';
+ $beschrijving = $organizationData['beschrijving'] ?? $organizationData['beschrijvingLang'] ?? null;
+ $organizationDescription = $beschrijving ?? $organizationData['description'] ?? '';
+
+ // Check if an entity with the same slug already exists (prevents unique constraint violation).
+ try {
+ $slug = strtolower(preg_replace('/[^a-z0-9]+/', '-', strtolower($organizationName)));
+ $slug = trim($slug, '-');
+ $existingBySlug = $organisationMapper->findBySlug($slug);
+ $this->_logger->info(
+ 'ContactPersonHandler: Organization entity already exists (found by slug)',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'slug' => $slug,
+ 'existingUuid' => $existingBySlug->getUuid(),
+ 'existingName' => $existingBySlug->getName(),
+ ]
+ );
+ return $existingBySlug;
+ } catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
+ // No existing entity by slug — proceed with creation.
+ }
+
+ $this->_logger->info(
+ 'ContactPersonHandler: Creating organization entity from object data',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'organizationName' => $organizationName,
+ ]
+ );
+
+ // Create the organization entity using OrganisationService.
$organisationService = $this->_container->get('OCA\\OpenRegister\\Service\\OrganisationService');
-
- // Create organisation with specific UUID, without adding current user (as we're in admin context)
+
+ // Create organisation with specific UUID, without adding current user (as we're in admin context).
$organisation = $organisationService->createOrganisation(
name: $organizationName,
description: $organizationDescription,
- addCurrentUser: false, // Don't add current user (admin) to this organisation
+ addCurrentUser: false,
+ // Don't add current user (admin) to this organisation.
uuid: $organizationUuid
);
-
- $this->_logger->info('ContactPersonHandler: Successfully created organization entity', [
- 'organizationUuid' => $organizationUuid,
- 'organizationName' => $organizationName,
- 'organizationId' => $organisation->getId()
- ]);
-
+
+ $this->_logger->info(
+ 'ContactPersonHandler: Successfully created organization entity',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'organizationName' => $organizationName,
+ 'organizationId' => $organisation->getId(),
+ ]
+ );
+
return $organisation;
-
} catch (\Exception $e) {
- $this->_logger->error('ContactPersonHandler: Failed to ensure organization entity', [
- 'organizationUuid' => $organizationUuid,
- 'error' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->_logger->error(
+ 'ContactPersonHandler: Failed to ensure organization entity',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'error' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
return null;
- }
- }
-
-}
+ }//end try
+ }//end ensureOrganizationEntity()
+}//end class
diff --git a/lib/Service/SoftwareCatalogue/GroupHandler.php b/lib/Service/SoftwareCatalogue/GroupHandler.php
index 3e51540f..b6a656fe 100644
--- a/lib/Service/SoftwareCatalogue/GroupHandler.php
+++ b/lib/Service/SoftwareCatalogue/GroupHandler.php
@@ -10,7 +10,7 @@
* @package OCA\SoftwareCatalog\Service\SoftwareCatalogue
* @author Conduction b.v.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
@@ -34,7 +34,7 @@
* @package OCA\SoftwareCatalog\Service\SoftwareCatalogue
* @author Conduction b.v.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
class GroupHandler
@@ -49,12 +49,12 @@ class GroupHandler
/**
* GroupHandler constructor
*
- * @param IGroupManager $_groupManager Group manager interface
- * @param IUserManager $_userManager User manager interface
- * @param IAppConfig $_appConfig App configuration interface
- * @param ContainerInterface $_container Container interface
- * @param IAppManager $_appManager App manager interface
- * @param LoggerInterface $_logger Logger interface
+ * @param IGroupManager $_groupManager Group manager interface
+ * @param IUserManager $_userManager User manager interface
+ * @param IAppConfig $_appConfig App configuration interface
+ * @param ContainerInterface $_container Container interface
+ * @param IAppManager $_appManager App manager interface
+ * @param LoggerInterface $_logger Logger interface
*/
public function __construct(
private readonly IGroupManager $_groupManager,
@@ -64,22 +64,23 @@ public function __construct(
private readonly IAppManager $_appManager,
private readonly LoggerInterface $_logger,
) {
- }
+ }//end __construct()
/**
* Gets the OpenRegister ObjectService if available
*
* @return \OCA\OpenRegister\Service\ObjectService|null ObjectService instance or null
+ *
* @throws \RuntimeException If service is not available
*/
- private function _getObjectService(): ?\OCA\OpenRegister\Service\ObjectService
+ private function getObjectService(): ?\OCA\OpenRegister\Service\ObjectService
{
- if (in_array('openregister', $this->_appManager->getInstalledApps())) {
+ if (in_array(needle: 'openregister', haystack: $this->_appManager->getInstalledApps()) === true) {
return $this->_container->get('OCA\OpenRegister\Service\ObjectService');
}
throw new \RuntimeException('OpenRegister service is not available.');
- }
+ }//end getObjectService()
/**
* Gets the list of generic user groups from configuration
@@ -89,38 +90,42 @@ private function _getObjectService(): ?\OCA\OpenRegister\Service\ObjectService
public function getGenericUserGroups(): array
{
$groupsJson = $this->_appConfig->getValueString(self::APP_NAME, 'generic_user_groups', '');
-
- if (empty($groupsJson)) {
- // Return only truly generic groups as default (not role-specific)
- // Role-specific groups are now assigned based on organization type
+
+ if (empty($groupsJson) === true) {
+ // Return only truly generic groups as default (not role-specific).
+ // Role-specific groups are now assigned based on organization type.
return [
'software-catalog-users'
];
}
$groups = json_decode($groupsJson, true);
- return is_array($groups) ? $groups : [];
- }
+ if (is_array($groups) === true) {
+ return $groups;
+ }
+
+ return [];
+ }//end getGenericUserGroups()
/**
* Sets the list of generic user groups in configuration
*
* @param array $groups Array of generic user groups
- *
+ *
* @return void
*/
public function setGenericUserGroups(array $groups): void
{
$groupsJson = json_encode($groups, JSON_THROW_ON_ERROR);
$this->_appConfig->setValueString(self::APP_NAME, 'generic_user_groups', $groupsJson);
-
+
$this->_logger->info(
'Updated generic user groups configuration',
[
- 'groups' => $groups
+ 'groups' => $groups,
]
);
- }
+ }//end setGenericUserGroups()
/**
* Ensures that all generic user groups exist in the system
@@ -129,13 +134,13 @@ public function setGenericUserGroups(array $groups): void
*/
public function ensureGenericUserGroupsExist(): array
{
- $genericGroups = $this->getGenericUserGroups();
+ $genericGroups = $genericGroups = $this->getGenericUserGroups();
$createdGroups = [];
-
+
foreach ($genericGroups as $groupName) {
- if (!$this->_groupManager->get($groupName)) {
+ if ($this->_groupManager->get($groupName) === null) {
$group = $this->_groupManager->createGroup($groupName);
- if ($group) {
+ if ($group !== null) {
$createdGroups[] = $groupName;
$this->_logger->info(
'Created generic user group',
@@ -144,8 +149,8 @@ public function ensureGenericUserGroupsExist(): array
}
}
}
-
- // Also ensure role-based groups exist
+
+ // Also ensure role-based groups exist.
$roleBasedGroups = [
'aanbod-beheerder',
'gebruik-beheerder',
@@ -153,14 +158,16 @@ public function ensureGenericUserGroupsExist(): array
'functioneel-beheerder',
'vng-raadpleger',
'organisatie-beheerder',
- 'organisaties-beheerder', // Plural form for organization contacts
- 'ambtenaar' // For users from Gemeente organizations
+ // Plural form for organization contacts.
+ 'organisaties-beheerder',
+ // For users from Gemeente organizations.
+ 'ambtenaar',
];
-
+
foreach ($roleBasedGroups as $groupName) {
- if (!$this->_groupManager->get($groupName)) {
+ if ($this->_groupManager->get($groupName) === null) {
$group = $this->_groupManager->createGroup($groupName);
- if ($group) {
+ if ($group !== null) {
$createdGroups[] = $groupName;
$this->_logger->info(
'Created role-based group',
@@ -169,284 +176,283 @@ public function ensureGenericUserGroupsExist(): array
}
}
}
-
+
return $createdGroups;
- }
+ }//end ensureGenericUserGroupsExist()
/**
* Creates a group if it doesn't exist
*
* @param string $groupName The group name to create
- *
+ *
* @return IGroup|null The created or existing group
*/
public function createGroupIfNotExists(string $groupName): ?IGroup
{
$group = $this->_groupManager->get($groupName);
-
- if (!$group) {
+
+ if ($group === null) {
try {
$group = $this->_groupManager->createGroup($groupName);
$this->_logger->info(
'Created new group',
[
- 'groupName' => $groupName
+ 'groupName' => $groupName,
]
);
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to create group: ' . $e->getMessage(),
+ 'Failed to create group: '.$e->getMessage(),
[
'groupName' => $groupName,
- 'exception' => $e
+ 'exception' => $e,
]
);
return null;
}
}
-
+
return $group;
- }
+ }//end createGroupIfNotExists()
/**
* Updates user groups based on contactpersoon data
*
* @param object $contactpersoonObject The contactpersoon object
- * @param string $username The username to update groups for
- *
+ * @param string $username The username to update groups for
+ *
* @return void
*/
public function updateUserGroups(object $contactpersoonObject, string $username): void
{
try {
$user = $this->_userManager->get($username);
- if (!$user) {
+ if ($user === null) {
$this->_logger->warning('User not found for group update', ['username' => $username]);
return;
}
$objectData = $contactpersoonObject->getObject();
-
- // Handle role-based groups
- $this->updateRoleBasedGroups($user, $objectData);
-
- // Handle organization groups
- $this->updateOrganizationGroups($user, $objectData);
-
- // Handle special gemeente groups
- $this->updateGemeenteGroups($user, $objectData);
-
+
+ // Handle role-based groups.
+ $this->updateRoleBasedGroups(user: $user, objectData: $objectData);
+
+ // Handle organization groups.
+ $this->updateOrganizationGroups(user: $user, objectData: $objectData);
+
+ // Handle special gemeente groups.
+ $this->updateGemeenteGroups(user: $user, objectData: $objectData);
+
$this->_logger->info(
'Updated user groups successfully',
[
'username' => $username,
- 'groups' => array_keys($this->_groupManager->getUserGroups($user))
+ 'groups' => array_keys($this->_groupManager->getUserGroups($user)),
]
);
-
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to update user groups: ' . $e->getMessage(),
+ 'Failed to update user groups: '.$e->getMessage(),
[
- 'username' => $username,
- 'exception' => $e
+ 'username' => $username,
+ 'exception' => $e,
]
);
- }
- }
+ }//end try
+ }//end updateUserGroups()
/**
* Updates role-based groups for a user
*
* @param IUser $user The user to update
* @param array $objectData The contactpersoon data
- *
+ *
* @return void
*/
public function updateRoleBasedGroups(IUser $user, array $objectData): void
{
$userRoles = $objectData['roles'] ?? [];
- if (!is_array($userRoles)) {
+ if (is_array($userRoles) === false) {
$userRoles = [];
}
$this->_logger->info(
'Updating role-based groups for user',
[
- 'username' => $user->getUID(),
- 'userRoles' => $userRoles
+ 'username' => $user->getUID(),
+ 'userRoles' => $userRoles,
]
);
- // Get the configured generic user groups
- $genericGroups = $this->getGenericUserGroups();
-
+ // Get the configured generic user groups.
+ $genericGroups = $genericGroups = $this->getGenericUserGroups();
+
foreach ($genericGroups as $groupName) {
- $group = $this->createGroupIfNotExists($groupName);
-
- if ($group) {
- $hasRole = in_array($groupName, $userRoles);
+ $group = $this->createGroupIfNotExists(groupName: $groupName);
+
+ if ($group !== null) {
+ $hasRole = in_array(needle: $groupName, haystack: $userRoles);
$inGroup = $group->inGroup($user);
-
- if ($hasRole && !$inGroup) {
- // Add user to group
+
+ if ($hasRole === true && $inGroup === false) {
+ // Add user to group.
$group->addUser($user);
$this->_logger->info(
'Added user to role-based group',
[
'username' => $user->getUID(),
- 'group' => $groupName,
- 'role' => $groupName
+ 'group' => $groupName,
+ 'role' => $groupName,
]
);
- } elseif (!$hasRole && $inGroup) {
- // Remove user from group (except for system groups)
- // Note: Removed 'ambtenaar' from protected groups since it's no longer automatically assigned
- if (!in_array($groupName, ['software-catalog-users'])) {
+ } else if ($hasRole === false && $inGroup === true) {
+ // Remove user from group (except for system groups).
+ // Note: Removed 'ambtenaar' from protected groups since it's no longer automatically assigned.
+ if (in_array(needle: $groupName, haystack: ['software-catalog-users']) === false) {
$group->removeUser($user);
$this->_logger->info(
'Removed user from role-based group',
[
'username' => $user->getUID(),
- 'group' => $groupName
+ 'group' => $groupName,
]
);
}
- }
- }
- }
- }
+ }//end if
+ }//end if
+ }//end foreach
+ }//end updateRoleBasedGroups()
/**
* Updates organization-based groups for a user
*
* @param IUser $user The user to update
* @param array $objectData The contactpersoon data
- *
+ *
* @return void
*/
public function updateOrganizationGroups(IUser $user, array $objectData): void
{
$organizationUuid = $objectData['organisation'] ?? $objectData['organization'] ?? '';
-
- if (!empty($organizationUuid)) {
+
+ if (empty($organizationUuid) === false) {
try {
- // Get organization object
- $objectService = $this->_getObjectService();
-
- // Get register and schema IDs dynamically from configuration
- $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
- $registerId = $settingsService->getVoorzieningenRegisterId();
+ // Get organization object.
+ $objectService = $objectService = $this->getObjectService();
+
+ // Get register and schema IDs dynamically from configuration.
+ $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
+ $registerId = $settingsService->getVoorzieningenRegisterId();
$organisatieSchemaId = $settingsService->getSchemaIdForObjectType('organisatie');
-
- if (!$registerId || !$organisatieSchemaId) {
- $this->_logger->warning('Register or schema ID not configured for organisatie');
+
+ if ($registerId === null || $organisatieSchemaId === null) {
+ $this->_logger->warning('Register or schema ID not configured for organisatie.');
return;
}
-
+
$organizationObject = $objectService->find($organizationUuid, [], false, $registerId, $organisatieSchemaId);
-
- if ($organizationObject) {
- $orgData = $organizationObject->getObject();
+
+ if ($organizationObject !== null) {
+ $orgData = $organizationObject->getObject();
$actualUuid = $orgData['id'] ?? $organizationUuid;
- $groupId = $orgData['group'] ?? '';
-
+ $groupId = $orgData['group'] ?? '';
+
$this->_logger->info(
'DEBUG: Organization group lookup for user',
[
- 'username' => $user->getUID(),
- 'inputOrganizationUuid' => $organizationUuid,
+ 'username' => $user->getUID(),
+ 'inputOrganizationUuid' => $organizationUuid,
'actualOrganizationUuid' => $actualUuid,
- 'groupId' => $groupId
+ 'groupId' => $groupId,
]
);
-
- if (!empty($groupId)) {
+
+ if (empty($groupId) === false) {
$group = $this->_groupManager->get($groupId);
-
- if ($group && !$group->inGroup($user)) {
+
+ if ($group !== null && $group->inGroup($user) === false) {
$group->addUser($user);
$this->_logger->info(
'Added user to organization group',
[
- 'username' => $user->getUID(),
- 'group' => $groupId,
- 'organizationUuid' => $actualUuid
+ 'username' => $user->getUID(),
+ 'group' => $groupId,
+ 'organizationUuid' => $actualUuid,
]
);
}
}
- }
+ }//end if
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to process organization group: ' . $e->getMessage(),
+ 'Failed to process organization group: '.$e->getMessage(),
[
- 'username' => $user->getUID(),
- 'organizationUuid' => $organizationUuid
+ 'username' => $user->getUID(),
+ 'organizationUuid' => $organizationUuid,
]
);
- }
- }
- }
+ }//end try
+ }//end if
+ }//end updateOrganizationGroups()
/**
* Updates gemeente-specific groups for a user
*
* @param IUser $user The user to update
* @param array $objectData The contactpersoon data
- *
+ *
* @return void
*/
public function updateGemeenteGroups(IUser $user, array $objectData): void
{
$organizationUuid = $objectData['organisation'] ?? $objectData['organization'] ?? '';
-
- if (!empty($organizationUuid)) {
+
+ if (empty($organizationUuid) === false) {
try {
- // Get organization object
- $objectService = $this->_getObjectService();
-
- // Get register and schema IDs dynamically from configuration
- $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
- $registerId = $settingsService->getVoorzieningenRegisterId();
+ // Get organization object.
+ $objectService = $objectService = $this->getObjectService();
+
+ // Get register and schema IDs dynamically from configuration.
+ $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
+ $registerId = $settingsService->getVoorzieningenRegisterId();
$organisatieSchemaId = $settingsService->getSchemaIdForObjectType('organisatie');
-
- if (!$registerId || !$organisatieSchemaId) {
- $this->_logger->warning('Register or schema ID not configured for organisatie');
+
+ if ($registerId === null || $organisatieSchemaId === null) {
+ $this->_logger->warning('Register or schema ID not configured for organisatie.');
return;
}
-
+
$organizationObject = $objectService->find($organizationUuid, [], false, $registerId, $organisatieSchemaId);
-
- if ($organizationObject) {
- $orgData = $organizationObject->getObject();
+
+ if ($organizationObject !== null) {
+ $orgData = $organizationObject->getObject();
$actualUuid = $orgData['id'] ?? $organizationUuid;
- $orgType = strtolower($orgData['type'] ?? $orgData['soort'] ?? '');
-
- // Note: Removed automatic assignment of 'ambtenaar' group for gemeente organizations
- // The 'ambtenaar' group can still be created if needed, but users are not automatically assigned
+ $orgType = strtolower($orgData['type'] ?? $orgData['soort'] ?? '');
+
+ // Note: Removed automatic assignment of 'ambtenaar' group for gemeente organizations.
+ // The 'ambtenaar' group can still be created if needed, but users are not automatically assigned.
if ($orgType === 'gemeente') {
$this->_logger->debug(
'User from gemeente organization (no automatic ambtenaar group assignment)',
[
- 'username' => $user->getUID(),
+ 'username' => $user->getUID(),
'organizationUuid' => $actualUuid,
- 'organizationType' => $orgType
+ 'organizationType' => $orgType,
]
);
}
}
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to process gemeente group: ' . $e->getMessage(),
+ 'Failed to process gemeente group: '.$e->getMessage(),
[
- 'username' => $user->getUID(),
- 'organizationUuid' => $organizationUuid
+ 'username' => $user->getUID(),
+ 'organizationUuid' => $organizationUuid,
]
);
- }
- }
- }
+ }//end try
+ }//end if
+ }//end updateGemeenteGroups()
/**
* Gets all available groups with their information
@@ -455,53 +461,53 @@ public function updateGemeenteGroups(IUser $user, array $objectData): void
*/
public function getAllGroups(): array
{
- $groups = $this->_groupManager->search('');
+ $groups = $this->_groupManager->search('');
$groupInfo = [];
-
+
foreach ($groups as $group) {
$groupInfo[] = [
- 'id' => $group->getGID(),
+ 'id' => $group->getGID(),
'displayName' => $group->getDisplayName(),
'memberCount' => count($group->getUsers()),
- 'isGeneric' => in_array($group->getGID(), $this->getGenericUserGroups())
+ 'isGeneric' => in_array(needle: $group->getGID(), haystack: $this->getGenericUserGroups()),
];
}
-
+
return $groupInfo;
- }
+ }//end getAllGroups()
/**
* Validates a list of group names
*
* @param array $groups Array of group names to validate
- *
+ *
* @return array Array with validation results
*/
public function validateGroups(array $groups): array
{
$results = [
- 'valid' => [],
+ 'valid' => [],
'invalid' => [],
- 'errors' => []
+ 'errors' => [],
];
-
+
foreach ($groups as $groupName) {
- if (empty($groupName) || !is_string($groupName)) {
+ if (empty($groupName) === true || is_string($groupName) === false) {
$results['invalid'][] = $groupName;
- $results['errors'][] = 'Group name cannot be empty';
+ $results['errors'][] = 'Group name cannot be empty';
continue;
}
-
- // Check for invalid characters
- if (preg_match('/[^a-zA-Z0-9._-]/', $groupName)) {
+
+ // Check for invalid characters.
+ if (preg_match('/[^a-zA-Z0-9._-]/', $groupName) === true) {
$results['invalid'][] = $groupName;
- $results['errors'][] = "Group name '{$groupName}' contains invalid characters";
+ $results['errors'][] = "Group name '{$groupName}' contains invalid characters";
continue;
}
-
+
$results['valid'][] = $groupName;
}
-
+
return $results;
- }
-}
\ No newline at end of file
+ }//end validateGroups()
+}//end class
diff --git a/lib/Service/SoftwareCatalogue/HierarchyHandler.php b/lib/Service/SoftwareCatalogue/HierarchyHandler.php
index 02937a3f..8079f861 100644
--- a/lib/Service/SoftwareCatalogue/HierarchyHandler.php
+++ b/lib/Service/SoftwareCatalogue/HierarchyHandler.php
@@ -10,7 +10,7 @@
* @package OCA\SoftwareCatalog\Service\SoftwareCatalogue
* @author Conduction b.v.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
@@ -29,7 +29,7 @@
* @package OCA\SoftwareCatalog\Service\SoftwareCatalogue
* @author Conduction b.v.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
class HierarchyHandler
@@ -37,277 +37,288 @@ class HierarchyHandler
/**
* HierarchyHandler constructor
*
- * @param OrganizationHandler $_organizationHandler Organization handler
- * @param ContactPersonHandler $_contactPersonHandler Contact person handler
- * @param LoggerInterface $_logger Logger interface
+ * @param OrganizationHandler $_organizationHandler Organization handler
+ * @param ContactPersonHandler $_contactPersonHandler Contact person handler
+ * @param LoggerInterface $_logger Logger interface
*/
public function __construct(
private readonly OrganizationHandler $_organizationHandler,
private readonly ContactPersonHandler $_contactPersonHandler,
private readonly LoggerInterface $_logger,
) {
- }
+ }//end __construct()
/**
* Ensures organization has at least one beheerder and manages user hierarchy
*
* @param object $contactgegevensObject The contactgegevens object
* @param string $username The username being processed
- *
+ *
* @return void
*/
public function ensureOrganizationBeheerder(object $contactgegevensObject, string $username): void
{
try {
- $objectData = $contactgegevensObject->getObject();
- $organizationUuid = (string)($objectData['organisation'] ?? $objectData['organization'] ?? '');
-
- if (empty($organizationUuid)) {
- $this->_logger->debug('No organization linked to contactgegevens');
+ $objectData = $contactgegevensObject->getObject();
+ $organizationUuid = (string) ($objectData['organisation'] ?? $objectData['organization'] ?? '');
+
+ if (empty($organizationUuid) === true) {
+ $this->_logger->debug('No organization linked to contactgegevens.');
return;
}
- // Get organization and check for existing beheerders
+ // Get organization and check for existing beheerders.
$organizationBeheerders = $this->_organizationHandler->getOrganizationBeheerders($organizationUuid);
-
- if (empty($organizationBeheerders)) {
- // No beheerders found - make this user the beheerder
- $this->_contactPersonHandler->assignBeheerderRole($contactgegevensObject, $username, $organizationUuid);
- $organizationBeheerders = [$username]; // Update our list
+
+ if (empty($organizationBeheerders) === true) {
+ // No beheerders found - make this user the beheerder.
+ $this->_contactPersonHandler->assignBeheerderRole(
+ contactpersoonObject: $contactgegevensObject,
+ username: $username,
+ organizationUuid: $organizationUuid
+ );
+ $organizationBeheerders = [$username];
+ // Update our list.
}
-
- // Set up manager relationships
- $this->setupManagerRelationships($username, $organizationBeheerders, $organizationUuid);
-
+
+ // Set up manager relationships.
+ $this->setupManagerRelationships(
+ username: $username,
+ organizationBeheerders: $organizationBeheerders,
+ organizationUuid: $organizationUuid
+ );
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to ensure organization beheerder: ' . $e->getMessage(),
+ 'Failed to ensure organization beheerder: '.$e->getMessage(),
[
- 'username' => $username,
- 'exception' => $e
+ 'username' => $username,
+ 'exception' => $e,
]
);
- }
- }
+ }//end try
+ }//end ensureOrganizationBeheerder()
/**
* Sets up manager relationships for users in an organization
*
- * @param string $username The current username being processed
- * @param array $organizationBeheerders Array of beheerder usernames
- * @param string $organizationUuid The organization UUID
- *
+ * @param string $username The current username being processed
+ * @param array $organizationBeheerders Array of beheerder usernames
+ * @param string $organizationUuid The organization UUID
+ *
* @return void
*/
- public function setupManagerRelationships(string $username, array $organizationBeheerders, string $organizationUuid): void
- {
+ public function setupManagerRelationships(
+ string $username,
+ array $organizationBeheerders,
+ string $organizationUuid
+ ): void {
try {
- if (empty($organizationBeheerders)) {
+ if (empty($organizationBeheerders) === true) {
return;
}
-
- // The oldest beheerder becomes the manager
+
+ // The oldest beheerder becomes the manager.
$primaryManager = $organizationBeheerders[0];
-
- // If current user is not a beheerder, set their manager
- if (!in_array($username, $organizationBeheerders)) {
- $this->_contactPersonHandler->setUserManager($username, $primaryManager);
+
+ // If current user is not a beheerder, set their manager.
+ if (in_array(needle: $username, haystack: $organizationBeheerders) === false) {
+ $this->_contactPersonHandler->setUserManager(username: $username, managerUsername: $primaryManager);
}
-
- // If there are multiple beheerders, set the primary as manager for others
+
+ // If there are multiple beheerders, set the primary as manager for others.
if (count($organizationBeheerders) > 1) {
foreach ($organizationBeheerders as $beheerder) {
if ($beheerder !== $primaryManager) {
- $this->_contactPersonHandler->setUserManager($beheerder, $primaryManager);
+ $this->_contactPersonHandler->setUserManager(username: $beheerder, managerUsername: $primaryManager);
}
}
}
-
+
$this->_logger->info(
'Set up manager relationships',
[
- 'organization' => $organizationUuid,
+ 'organization' => $organizationUuid,
'primaryManager' => $primaryManager,
- 'allBeheerders' => $organizationBeheerders,
- 'processedUser' => $username
+ 'allBeheerders' => $organizationBeheerders,
+ 'processedUser' => $username,
]
);
-
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to setup manager relationships: ' . $e->getMessage(),
+ 'Failed to setup manager relationships: '.$e->getMessage(),
[
- 'username' => $username,
+ 'username' => $username,
'organization' => $organizationUuid,
- 'exception' => $e
+ 'exception' => $e,
]
);
- }
- }
+ }//end try
+ }//end setupManagerRelationships()
/**
* Gets organizational hierarchy information for a user
*
* @param string $username The username to get hierarchy for
- *
+ *
* @return array Array containing hierarchy information
*/
public function getUserHierarchy(string $username): array
{
try {
$hierarchy = [
- 'username' => $username,
- 'manager' => null,
- 'subordinates' => [],
- 'organization' => null,
- 'isBeheerder' => false,
- 'isPrimaryManager' => false
+ 'username' => $username,
+ 'manager' => null,
+ 'subordinates' => [],
+ 'organization' => null,
+ 'isBeheerder' => false,
+ 'isPrimaryManager' => false,
];
- // Get user's manager
+ // Get user's manager.
$manager = $this->_contactPersonHandler->getUserManager($username);
- if ($manager) {
+ if ($manager !== null) {
$hierarchy['manager'] = $manager;
}
- // Find subordinates (users who have this user as manager)
- $subordinates = $this->findSubordinates($username);
+ // Find subordinates (users who have this user as manager).
+ $subordinates = $this->findSubordinates(username: $username);
$hierarchy['subordinates'] = $subordinates;
- // Check if user is a beheerder
- $hierarchy['isBeheerder'] = $this->isUserBeheerder($username);
+ // Check if user is a beheerder.
+ $hierarchy['isBeheerder'] = $this->isUserBeheerder(username: $username);
- // Check if user is primary manager (has subordinates and no manager)
- $hierarchy['isPrimaryManager'] = empty($hierarchy['manager']) && !empty($hierarchy['subordinates']);
+ // Check if user is primary manager (has subordinates and no manager).
+ $hierarchy['isPrimaryManager'] = empty($hierarchy['manager']) === true
+ && empty($hierarchy['subordinates']) === false;
return $hierarchy;
-
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to get user hierarchy: ' . $e->getMessage(),
+ 'Failed to get user hierarchy: '.$e->getMessage(),
[
- 'username' => $username,
- 'exception' => $e
+ 'username' => $username,
+ 'exception' => $e,
]
);
return [];
- }
- }
+ }//end try
+ }//end getUserHierarchy()
/**
* Finds all subordinates for a given user
*
* @param string $username The username to find subordinates for
- *
+ *
* @return array Array of subordinate usernames
*/
private function findSubordinates(string $username): array
{
$subordinates = [];
-
+
try {
- // Get all users and check their managers
+ // Get all users and check their managers.
$userManager = \OC::$server->getUserManager();
- $users = $userManager->search('');
-
+ $users = $userManager->search('');
+
foreach ($users as $user) {
$userUsername = $user->getUID();
- $manager = $this->_contactPersonHandler->getUserManager($userUsername);
-
+ $manager = $this->_contactPersonHandler->getUserManager($userUsername);
+
if ($manager === $username) {
$subordinates[] = $userUsername;
}
}
-
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to find subordinates: ' . $e->getMessage(),
+ 'Failed to find subordinates: '.$e->getMessage(),
[
- 'username' => $username,
- 'exception' => $e
+ 'username' => $username,
+ 'exception' => $e,
]
);
- }
-
+ }//end try
+
return $subordinates;
- }
+ }//end findSubordinates()
/**
* Checks if a user is a beheerder
*
* @param string $username The username to check
- *
+ *
* @return bool True if user is a beheerder
*/
private function isUserBeheerder(string $username): bool
{
try {
- $groupManager = \OC::$server->getGroupManager();
+ $groupManager = \OC::$server->getGroupManager();
$beheerderGroup = $groupManager->get('beheerder');
-
- if (!$beheerderGroup) {
+
+ if ($beheerderGroup === null) {
return false;
}
-
+
$userManager = \OC::$server->getUserManager();
- $user = $userManager->get($username);
-
- return $user && $beheerderGroup->inGroup($user);
-
+ $user = $userManager->get($username);
+
+ if ($user === null) {
+ return false;
+ }
+
+ return $beheerderGroup->inGroup($user);
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to check if user is beheerder: ' . $e->getMessage(),
+ 'Failed to check if user is beheerder: '.$e->getMessage(),
[
- 'username' => $username,
- 'exception' => $e
+ 'username' => $username,
+ 'exception' => $e,
]
);
return false;
- }
- }
+ }//end try
+ }//end isUserBeheerder()
/**
* Gets complete organizational structure
*
* @param string $organizationUuid The organization UUID
- *
+ *
* @return array Array containing organizational structure
*/
public function getOrganizationStructure(string $organizationUuid): array
{
try {
$structure = [
- 'organization' => $organizationUuid,
- 'beheerders' => [],
+ 'organization' => $organizationUuid,
+ 'beheerders' => [],
'primaryManager' => null,
- 'hierarchy' => []
+ 'hierarchy' => [],
];
- // Get all beheerders for this organization
+ // Get all beheerders for this organization.
$beheerders = $this->_organizationHandler->getOrganizationBeheerders($organizationUuid);
$structure['beheerders'] = $beheerders;
- if (!empty($beheerders)) {
+ if (empty($beheerders) === false) {
$structure['primaryManager'] = $beheerders[0];
}
- // Build hierarchy tree
+ // Build hierarchy tree.
foreach ($beheerders as $beheerder) {
- $hierarchy = $this->getUserHierarchy($beheerder);
+ $hierarchy = $this->getUserHierarchy(username: $beheerder);
$structure['hierarchy'][] = $hierarchy;
}
return $structure;
-
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to get organization structure: ' . $e->getMessage(),
+ 'Failed to get organization structure: '.$e->getMessage(),
[
'organization' => $organizationUuid,
- 'exception' => $e
+ 'exception' => $e,
]
);
return [];
- }
- }
-}
\ No newline at end of file
+ }//end try
+ }//end getOrganizationStructure()
+}//end class
diff --git a/lib/Service/SoftwareCatalogue/OrganizationHandler.php b/lib/Service/SoftwareCatalogue/OrganizationHandler.php
index 783e9298..4ac8581a 100644
--- a/lib/Service/SoftwareCatalogue/OrganizationHandler.php
+++ b/lib/Service/SoftwareCatalogue/OrganizationHandler.php
@@ -1,7 +1,7 @@
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
@@ -27,25 +27,25 @@
use Psr\Log\LoggerInterface;
/**
- * Handler for organization-related operations
+ * Handler for organization-related operations.
*
* @category Handler
* @package OCA\SoftwareCatalog\Service\SoftwareCatalogue
* @author Conduction b.v.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
+ * @version GIT:
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
class OrganizationHandler
{
/**
- * OrganizationHandler constructor
+ * OrganizationHandler constructor.
*
- * @param IGroupManager $_groupManager Group manager interface
- * @param IUserManager $_userManager User manager interface
- * @param ContainerInterface $_container Container interface
- * @param IAppManager $_appManager App manager interface
- * @param LoggerInterface $_logger Logger interface
+ * @param IGroupManager $_groupManager Group manager interface
+ * @param IUserManager $_userManager User manager interface
+ * @param ContainerInterface $_container Container interface
+ * @param IAppManager $_appManager App manager interface
+ * @param LoggerInterface $_logger Logger interface
*/
public function __construct(
private readonly IGroupManager $_groupManager,
@@ -54,424 +54,487 @@ public function __construct(
private readonly IAppManager $_appManager,
private readonly LoggerInterface $_logger,
) {
- }
+ }//end __construct()
/**
- * Gets the OpenRegister ObjectService if available
+ * Gets the OpenRegister ObjectService if available.
*
* @return \OCA\OpenRegister\Service\ObjectService|null ObjectService instance or null
+ *
* @throws \RuntimeException If service is not available
*/
- private function _getObjectService(): ?\OCA\OpenRegister\Service\ObjectService
+ private function getObjectService(): ?\OCA\OpenRegister\Service\ObjectService
{
- if (in_array('openregister', $this->_appManager->getInstalledApps())) {
+ if (in_array(needle: 'openregister', haystack: $this->_appManager->getInstalledApps()) === true) {
return $this->_container->get('OCA\OpenRegister\Service\ObjectService');
}
throw new \RuntimeException('OpenRegister service is not available.');
- }
+ }//end getObjectService()
/**
- * Processes organization groups and ensures proper group assignment
+ * Processes organization groups and ensures proper group assignment.
*
* @param object $organizationObject The organization object to process
- *
+ *
* @return bool True if processing was successful
+ *
* @throws \Exception If processing fails
*/
public function processOrganization(object $organizationObject): bool
{
try {
- $this->_logger->info('Processing organization object', [
- 'objectId' => $organizationObject->getId()
- ]);
+ $this->_logger->info(
+ 'Processing organization object',
+ [
+ 'objectId' => $organizationObject->getId(),
+ ]
+ );
$objectData = $organizationObject->getObject();
-
- // Check if organization is active (beoordeling = "actief" or "Actief")
+
+ // Check if organization is active (beoordeling = "actief" or "Actief").
$beoordeling = strtolower($objectData['beoordeling'] ?? '');
if ($beoordeling !== 'actief') {
$this->_logger->info(
'Organization not active, skipping processing',
[
'organizationId' => $organizationObject->getId(),
- 'beoordeling' => $beoordeling
+ 'beoordeling' => $beoordeling,
]
);
- return true; // Not an error, just not ready for processing
+ // Not an error, just not ready for processing.
+ return true;
}
-
- // Ensure organization has a unique group
- $groupId = $this->ensureOrganizationGroup($organizationObject, $objectData);
-
- if ($groupId) {
+
+ // Ensure organization has a unique group.
+ $groupId = $this->ensureOrganizationGroup(
+ organizationObject: $organizationObject,
+ objectData: $objectData
+ );
+
+ if ($groupId !== null) {
$this->_logger->info(
- 'Successfully processed organization group',
+ 'Successfully processed organization group',
[
'organizationId' => $organizationObject->getId(),
- 'groupId' => $groupId
+ 'groupId' => $groupId,
]
);
}
-
+
return true;
-
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to process organization object: ' . $e->getMessage(),
+ 'Failed to process organization object: '.$e->getMessage(),
[
'exception' => $e,
- 'objectId' => $organizationObject->getId() ?? 'unknown'
+ 'objectId' => $organizationObject->getId() ?? 'unknown',
]
);
throw $e;
- }
- }
+ }//end try
+ }//end processOrganization()
/**
- * Ensures an organization has a unique group and returns the group ID
+ * Ensures an organization has a unique group and returns the group ID.
+ *
+ * @param object $organizationObject The organization object
+ * @param array $objectData The organization data
*
- * @param object $organizationObject The organization object
- * @param array $objectData The organization data
- *
* @return string|null The group ID or null if failed
*/
public function ensureOrganizationGroup(object $organizationObject, array &$objectData): ?string
{
$groupProperty = $objectData['group'] ?? '';
-
- if (empty($groupProperty)) {
- // Create group with organization name
+
+ if (empty($groupProperty) === true) {
+ // Create group with organization name.
$organizationName = $objectData['naam'] ?? $objectData['name'] ?? 'Organization';
- $groupName = $this->sanitizeGroupName($organizationName);
-
- // Ensure group name is unique
- $groupName = $this->ensureUniqueGroupName($groupName);
-
- $group = $this->createGroupIfNotExists($groupName);
-
- if ($group) {
- // Set the group ID in the organization object
+ $groupName = $this->sanitizeGroupName(name: $organizationName);
+
+ // Ensure group name is unique.
+ $groupName = $this->ensureUniqueGroupName(baseName: $groupName);
+
+ $group = $this->createGroupIfNotExists(groupName: $groupName);
+
+ if ($group !== null) {
+ // Set the group ID in the organization object.
$objectData['group'] = $group->getGID();
$organizationObject->setObject($objectData);
-
- // Save the updated organization with correct register/schema IDs
- $objectService = $this->_getObjectService();
- $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
- $registerId = $settingsService->getVoorzieningenRegisterId();
+
+ // Save the updated organization with correct register/schema IDs.
+ $objectService = $this->getObjectService();
+ $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
+ $registerId = $settingsService->getVoorzieningenRegisterId();
$organizationSchemaId = $settingsService->getSchemaIdForObjectType('organisatie');
-
- if ($registerId && $organizationSchemaId) {
+
+ if ($registerId !== null && $organizationSchemaId !== null) {
$objectService->saveObject(
- $organizationObject,
- [],
- (int) $registerId,
- (int) $organizationSchemaId,
- $organizationObject->getUuid()
+ object: $organizationObject,
+ fields: [],
+ register: (int) $registerId,
+ schema: (int) $organizationSchemaId,
+ uuid: $organizationObject->getUuid()
);
} else {
$this->_logger->warning(
'Missing register or schema ID for organization, using fallback save method',
[
- 'registerId' => $registerId,
- 'organizationSchemaId' => $organizationSchemaId
+ 'registerId' => $registerId,
+ 'organizationSchemaId' => $organizationSchemaId,
]
);
$objectService->saveObject($organizationObject);
}
-
+
$this->_logger->info(
'Created and assigned unique group to organization',
[
'organizationId' => $organizationObject->getId(),
- 'groupName' => $groupName,
- 'groupId' => $group->getGID()
+ 'groupName' => $groupName,
+ 'groupId' => $group->getGID(),
]
);
-
+
return $group->getGID();
- }
+ }//end if
+ }//end if
+
+ if (empty($groupProperty) === false) {
+ return $groupProperty;
}
-
- return $groupProperty ?: null;
- }
+
+ return null;
+ }//end ensureOrganizationGroup()
/**
- * Ensures a group name is unique by appending a counter if necessary
+ * Ensures a group name is unique by appending a counter if necessary.
+ *
+ * @param string $baseName The base group name
*
- * @param string $baseName The base group name
- *
* @return string A unique group name
*/
private function ensureUniqueGroupName(string $baseName): string
{
$groupName = $baseName;
- $counter = 1;
-
+ $counter = 1;
+
while ($this->_groupManager->get($groupName) !== null) {
- $groupName = $baseName . '_' . $counter;
+ $groupName = $baseName.'_'.$counter;
$counter++;
}
-
+
return $groupName;
- }
+ }//end ensureUniqueGroupName()
/**
- * Creates a group if it doesn't exist
+ * Creates a group if it doesn't exist.
+ *
+ * @param string $groupName The group name to create
*
- * @param string $groupName The group name to create
- *
* @return IGroup|null The created or existing group
*/
public function createGroupIfNotExists(string $groupName): ?IGroup
{
$group = $this->_groupManager->get($groupName);
-
- if (!$group) {
+
+ if ($group === null) {
try {
$group = $this->_groupManager->createGroup($groupName);
$this->_logger->info(
'Created new group',
[
- 'groupName' => $groupName
+ 'groupName' => $groupName,
]
);
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to create group: ' . $e->getMessage(),
+ 'Failed to create group: '.$e->getMessage(),
[
'groupName' => $groupName,
- 'exception' => $e
+ 'exception' => $e,
]
);
return null;
}
}
-
+
return $group;
- }
+ }//end createGroupIfNotExists()
/**
- * Sanitizes a group name for safe usage
+ * Sanitizes a group name for safe usage.
+ *
+ * @param string $name The name to sanitize
*
- * @param string $name The name to sanitize
- *
* @return string The sanitized group name
*/
public function sanitizeGroupName(string $name): string
{
- // Convert to lowercase and replace special characters
+ // Convert to lowercase and replace special characters.
$sanitized = strtolower(trim($name));
$sanitized = preg_replace('/[^a-z0-9._-]/', '_', $sanitized);
$sanitized = preg_replace('/_{2,}/', '_', $sanitized);
$sanitized = trim($sanitized, '_');
-
- // Ensure it's not empty
- if (empty($sanitized)) {
- $sanitized = 'organization_' . time();
+
+ // Ensure it's not empty.
+ if (empty($sanitized) === true) {
+ $sanitized = 'organization_'.time();
}
-
+
return $sanitized;
- }
+ }//end sanitizeGroupName()
/**
- * Processes contactpersonen from organization data into Contactgegevens objects
+ * Processes contactpersonen from organization data into Contactgegevens objects.
*
* @param object $organizationObject The organization object
- *
+ *
* @return array Array of created or updated contactgegevens objects
*/
public function processContactpersonen(object $organizationObject): array
{
try {
- $objectData = $organizationObject->getObject();
+ $objectData = $organizationObject->getObject();
$contactpersonen = $objectData['contactpersonen'] ?? [];
- // Get the actual UUID from object data instead of database ID
- $organizationUuid = $objectData['id'] ?? $organizationObject->getId();
+ // Get the actual UUID from object data instead of database ID.
+ $organizationUuid = $objectData['id'] ?? $organizationObject->getId();
$processedContacts = [];
- if (!is_array($contactpersonen) || empty($contactpersonen)) {
- $this->_logger->info('No contactpersonen found in organization', [
- 'organizationId' => $organizationUuid
- ]);
+ if (is_array($contactpersonen) === false || empty($contactpersonen) === true) {
+ $this->_logger->info(
+ 'No contactpersonen found in organization',
+ [
+ 'organizationId' => $organizationUuid,
+ ]
+ );
return $processedContacts;
}
- $objectService = $this->_getObjectService();
+ $objectService = $this->getObjectService();
foreach ($contactpersonen as $index => $contactpersoon) {
try {
- // Get the contactgegevens schema ID from settings
- $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
+ // Get the contactgegevens schema ID from settings.
+ $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
$contactgegevensSchemaId = $settingsService->getSchemaIdForObjectType('contactgegevens');
$registerId = $settingsService->getVoorzieningenRegisterId();
-
- if (!$registerId) {
+
+ if ($registerId === null) {
throw new \Exception('Voorzieningen register ID not configured');
}
-
+
$contactEmail = $contactpersoon['email'] ?? $contactpersoon['e-mailadres'] ?? '';
-
- // Check if contactgegevens object already exists for this email + organization
- $existingContactgegevens = $this->findExistingContactgegevens($contactEmail, $organizationUuid, $objectService, $registerId, $contactgegevensSchemaId);
-
+
+ // Check if contactgegevens object already exists for this email + organization.
+ $existingContactgegevens = $this->findExistingContactgegevens(
+ email: $contactEmail,
+ organizationUuid: $organizationUuid,
+ objectService: $objectService,
+ registerId: $registerId,
+ contactgegevensSchemaId: $contactgegevensSchemaId
+ );
+
+ if ($existingContactgegevens !== null) {
+ $logMessage = 'Updating existing contactgegevens object';
+ } else {
+ $logMessage = 'Creating new contactgegevens object';
+ }
+
+ if ($existingContactgegevens !== null) {
+ $existingId = $existingContactgegevens->getUuid();
+ } else {
+ $existingId = null;
+ }
+
$this->_logger->info(
- $existingContactgegevens ? 'Updating existing contactgegevens object' : 'Creating new contactgegevens object',
+ $logMessage,
[
'contactgegevensSchemaId' => $contactgegevensSchemaId,
- 'organizationUuid' => $organizationUuid,
- 'contactpersoonIndex' => $index,
- 'email' => $contactEmail,
- 'existingId' => $existingContactgegevens ? $existingContactgegevens->getUuid() : null
+ 'organizationUuid' => $organizationUuid,
+ 'contactpersoonIndex' => $index,
+ 'email' => $contactEmail,
+ 'existingId' => $existingId,
]
);
-
- // Generate title from name components
- $titleParts = array_filter([
- $contactpersoon['voornaam'] ?? '',
- $contactpersoon['tussenvoegsel'] ?? '',
- $contactpersoon['achternaam'] ?? ''
- ]);
- $title = !empty($titleParts) ? implode(' ', $titleParts) : ($contactpersoon['email'] ?? 'Contact Person');
-
- // Create contactgegevens object with proper schema
+
+ // Generate title from name components.
+ $titleParts = array_filter(
+ [
+ $contactpersoon['voornaam'] ?? '',
+ $contactpersoon['tussenvoegsel'] ?? '',
+ $contactpersoon['achternaam'] ?? '',
+ ]
+ );
+
+ if (empty($titleParts) === false) {
+ $title = implode(' ', $titleParts);
+ } else {
+ $title = $contactpersoon['email'] ?? 'Contact Person';
+ }
+
+ // Create contactgegevens object with proper schema.
+ $contactFunctie = $contactpersoon['functie'] ?? '';
+ $contactRoles = $this->mapFunctieToRoles(
+ functie: $contactFunctie,
+ isFirstContact: ($index === 0)
+ );
$contactgegevensData = [
- 'title' => $title, // Required by OpenRegister
- 'voornaam' => $contactpersoon['voornaam'] ?? '',
+ // Required by OpenRegister.
+ 'title' => $title,
+ 'voornaam' => $contactpersoon['voornaam'] ?? '',
'tussenvoegsel' => $contactpersoon['tussenvoegsel'] ?? '',
- 'achternaam' => $contactpersoon['achternaam'] ?? '',
- 'telefoon' => $contactpersoon['telefoon'] ?? '',
- 'email' => $contactEmail,
- 'functie' => $contactpersoon['functie'] ?? '',
- 'organisation' => $organizationUuid, // Link to organization
- 'roles' => $this->mapFunctieToRoles($contactpersoon['functie'] ?? '', $index === 0),
- 'username' => '', // Will be set when user is created
+ 'achternaam' => $contactpersoon['achternaam'] ?? '',
+ 'telefoon' => $contactpersoon['telefoon'] ?? '',
+ 'email' => $contactEmail,
+ 'functie' => $contactFunctie,
+ // Link to organization.
+ 'organisation' => $organizationUuid,
+ 'roles' => $contactRoles,
+ // Will be set when user is created.
+ 'username' => '',
];
- // If updating existing contactgegevens, preserve the username if it exists
- if ($existingContactgegevens) {
+ // If updating existing contactgegevens, preserve the username if it exists.
+ if ($existingContactgegevens !== null) {
$existingData = $existingContactgegevens->getObject();
$contactgegevensData['username'] = $existingData['username'] ?? '';
}
- // Create or update the contactgegevens object via ObjectService
- if ($existingContactgegevens) {
- // Update existing contactgegevens object
+ // Create or update the contactgegevens object via ObjectService.
+ if ($existingContactgegevens !== null) {
+ // Update existing contactgegevens object.
$contactgegevensObject = $objectService->saveObject(
- $contactgegevensData,
- [],
- $registerId, // Dynamic register ID from configuration
- $contactgegevensSchemaId, // Schema ID from configuration
- $existingContactgegevens->getUuid() // Pass existing UUID to update
+ object: $contactgegevensData,
+ fields: [],
+ register: $registerId,
+ schema: $contactgegevensSchemaId,
+ uuid: $existingContactgegevens->getUuid()
);
} else {
- // Create new contactgegevens object
+ // Create new contactgegevens object.
$contactgegevensObject = $objectService->saveObject(
- $contactgegevensData,
- [],
- $registerId, // Dynamic register ID from configuration
- $contactgegevensSchemaId // Schema ID from configuration
+ object: $contactgegevensData,
+ fields: [],
+ register: $registerId,
+ schema: $contactgegevensSchemaId
);
- }
-
- if ($contactgegevensObject) {
+ }//end if
+
+ if ($contactgegevensObject !== null) {
$processedContacts[] = $contactgegevensObject;
-
+
+ if ($existingContactgegevens !== null) {
+ $actionLogMessage = 'Updated existing contactgegevens from contactpersoon';
+ $actionValue = 'update';
+ } else {
+ $actionLogMessage = 'Created new contactgegevens from contactpersoon';
+ $actionValue = 'create';
+ }
+
$this->_logger->info(
- $existingContactgegevens ? 'Updated existing contactgegevens from contactpersoon' : 'Created new contactgegevens from contactpersoon',
+ $actionLogMessage,
[
- 'organizationId' => $organizationUuid,
- 'contactgegevensId' => $contactgegevensObject->getId(),
+ 'organizationId' => $organizationUuid,
+ 'contactgegevensId' => $contactgegevensObject->getId(),
'contactpersoonIndex' => $index,
- 'email' => $contactgegevensData['email'],
- 'action' => $existingContactgegevens ? 'update' : 'create'
+ 'email' => $contactgegevensData['email'],
+ 'action' => $actionValue,
]
);
- }
-
+ }//end if
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to process contactpersoon: ' . $e->getMessage(),
+ 'Failed to process contactpersoon: '.$e->getMessage(),
[
- 'organizationId' => $organizationUuid,
+ 'organizationId' => $organizationUuid,
'contactpersoonIndex' => $index,
- 'contactpersoon' => $contactpersoon,
- 'exception' => $e
+ 'contactpersoon' => $contactpersoon,
+ 'exception' => $e,
]
);
- }
- }
+ }//end try
+ }//end foreach
return $processedContacts;
-
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to process contactpersonen: ' . $e->getMessage(),
+ 'Failed to process contactpersonen: '.$e->getMessage(),
[
'organizationId' => $organizationObject->getId(),
- 'exception' => $e
+ 'exception' => $e,
]
);
return [];
- }
- }
+ }//end try
+ }//end processContactpersonen()
/**
- * Finds existing contactgegevens object for a given email and organization
+ * Finds existing contactgegevens object for a given email and organization.
+ *
+ * @param string $email The email address to search for
+ * @param string $organizationUuid The organization UUID
+ * @param \OCA\OpenRegister\Service\ObjectService $objectService The object service
+ * @param int $registerId The register ID
+ * @param int $contactgegevensSchemaId The contactgegevens schema ID
*
- * @param string $email The email address to search for
- * @param string $organizationUuid The organization UUID
- * @param \OCA\OpenRegister\Service\ObjectService $objectService The object service
- * @param int $registerId The register ID
- * @param int $contactgegevensSchemaId The contactgegevens schema ID
- *
* @return object|null The existing contactgegevens object or null if not found
*/
- private function findExistingContactgegevens(string $email, string $organizationUuid, \OCA\OpenRegister\Service\ObjectService $objectService, int $registerId, int $contactgegevensSchemaId): ?object
- {
+ private function findExistingContactgegevens(
+ string $email,
+ string $organizationUuid,
+ \OCA\OpenRegister\Service\ObjectService $objectService,
+ int $registerId,
+ int $contactgegevensSchemaId
+ ): ?object {
try {
- if (empty($email) || empty($organizationUuid)) {
+ if (empty($email) === true || empty($organizationUuid) === true) {
return null;
}
- // Search for existing contactgegevens with this email and organization
+ // Search for existing contactgegevens with this email and organization.
$searchFilters = [
- 'email' => $email,
- 'organisation' => $organizationUuid
+ 'email' => $email,
+ 'organisation' => $organizationUuid,
];
- $existingObjects = $objectService->findAll($searchFilters, $registerId, $contactgegevensSchemaId);
+ $existingObjects = $objectService->findAll(
+ filters: $searchFilters,
+ register: $registerId,
+ schema: $contactgegevensSchemaId
+ );
- if (!empty($existingObjects)) {
+ if (empty($existingObjects) === false) {
$this->_logger->info(
'Found existing contactgegevens object',
[
- 'email' => $email,
+ 'email' => $email,
'organizationUuid' => $organizationUuid,
- 'existingId' => $existingObjects[0]->getUuid(),
- 'totalFound' => count($existingObjects)
+ 'existingId' => $existingObjects[0]->getUuid(),
+ 'totalFound' => count($existingObjects),
]
);
- return $existingObjects[0]; // Return the first match
+ // Return the first match.
+ return $existingObjects[0];
}
return null;
-
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to find existing contactgegevens: ' . $e->getMessage(),
+ 'Failed to find existing contactgegevens: '.$e->getMessage(),
[
- 'email' => $email,
+ 'email' => $email,
'organizationUuid' => $organizationUuid,
- 'exception' => $e
+ 'exception' => $e,
]
);
return null;
- }
- }
+ }//end try
+ }//end findExistingContactgegevens()
/**
- * Gets all available roles in the system
+ * Gets all available roles in the system.
*
* @return array Array of all available roles
*/
@@ -483,198 +546,221 @@ private function getAllAvailableRoles(): array
'Gebruik-beheerder',
'Gebruik-raadpleger',
'VNG-raadpleger',
- 'beheerder' // Add beheerder role for group assignment
+ // Add beheerder role for group assignment.
+ 'beheerder',
];
- }
+ }//end getAllAvailableRoles()
/**
- * Maps functie (job function) to appropriate roles
+ * Maps functie (job function) to appropriate roles.
*
- * @param string $functie The job function
+ * @param string $functie The job function
* @param bool $isFirstContact Whether this is the first contact in the organization
- *
+ *
* @return array Array of roles
*/
- private function mapFunctieToRoles(string $functie, bool $isFirstContact = false): array
+ private function mapFunctieToRoles(string $functie, bool $isFirstContact=false): array
{
- // If this is the first contact, give them all available roles
- if ($isFirstContact) {
+ // If this is the first contact, give them all available roles.
+ if ($isFirstContact === true) {
$this->_logger->info('Assigning all roles to first contact', ['functie' => $functie]);
return $this->getAllAvailableRoles();
}
-
+
$functie = strtolower(trim($functie));
-
- // Default role mappings based on common job functions
+
+ // Default role mappings based on common job functions.
$roleMapping = [
- 'ceo' => ['Functioneel-beheerder', 'Aanbod-beheerder'],
- 'manager' => ['Functioneel-beheerder', 'Gebruik-beheerder'],
- 'beheerder' => ['Gebruik-beheerder', 'beheerder'],
+ 'ceo' => ['Functioneel-beheerder', 'Aanbod-beheerder'],
+ 'manager' => ['Functioneel-beheerder', 'Gebruik-beheerder'],
+ 'beheerder' => ['Gebruik-beheerder', 'beheerder'],
'administrator' => ['Functioneel-beheerder'],
- 'inkoper' => ['Gebruik-beheerder'],
- 'procurement' => ['Gebruik-beheerder'],
- 'raadpleger' => ['Gebruik-raadpleger'],
- 'viewer' => ['Gebruik-raadpleger'],
- 'vng' => ['VNG-raadpleger']
+ 'inkoper' => ['Gebruik-beheerder'],
+ 'procurement' => ['Gebruik-beheerder'],
+ 'raadpleger' => ['Gebruik-raadpleger'],
+ 'viewer' => ['Gebruik-raadpleger'],
+ 'vng' => ['VNG-raadpleger'],
];
- // Check for specific matches
+ // Check for specific matches.
foreach ($roleMapping as $key => $roles) {
- if (strpos($functie, $key) !== false) {
+ if (strpos(haystack: $functie, needle: $key) !== false) {
return $roles;
}
}
- // Default role for unknown functions
+ // Default role for unknown functions.
return ['Gebruik-raadpleger'];
- }
+ }//end mapFunctieToRoles()
/**
- * Handles new organization creation with contactpersonen processing
+ * Handles new organization creation with contactpersonen processing.
+ *
+ * @param object $organizationObject The organization object
*
- * @param object $organizationObject The organization object
- *
* @return void
*/
public function handleNewOrganization(object $organizationObject): void
{
try {
- $this->_logger->info('Handling new organization', [
- 'objectId' => $organizationObject->getId()
- ]);
-
- // First process the organization to ensure it has proper group structure
- $processed = $this->processOrganization($organizationObject);
-
- if ($processed) {
- // Then process contactpersonen if organization is active
- $objectData = $organizationObject->getObject();
+ $this->_logger->info(
+ 'Handling new organization',
+ [
+ 'objectId' => $organizationObject->getId(),
+ ]
+ );
+
+ // First process the organization to ensure it has proper group structure.
+ $processed = $this->processOrganization(organizationObject: $organizationObject);
+
+ if ($processed === true) {
+ // Then process contactpersonen if organization is active.
+ $objectData = $organizationObject->getObject();
$beoordeling = strtolower($objectData['beoordeling'] ?? '');
-
+
if ($beoordeling === 'actief') {
- $processedContacts = $this->processContactpersonen($organizationObject);
-
+ $processedContacts = $this->processContactpersonen(organizationObject: $organizationObject);
+
$this->_logger->info(
'Processed organization and contactgegevens',
[
- 'organizationId' => $organizationObject->getId(),
- 'contactgegevensCount' => count($processedContacts)
+ 'organizationId' => $organizationObject->getId(),
+ 'contactgegevensCount' => count($processedContacts),
]
);
}
}
-
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to handle new organization: ' . $e->getMessage(),
+ 'Failed to handle new organization: '.$e->getMessage(),
[
- 'objectId' => $organizationObject->getId(),
- 'exception' => $e
+ 'objectId' => $organizationObject->getId(),
+ 'exception' => $e,
]
);
- }
- }
+ }//end try
+ }//end handleNewOrganization()
/**
- * Gets all beheerders for an organization
+ * Gets all beheerders for an organization.
+ *
+ * @param string $organizationUuid The organization UUID
*
- * @param string $organizationUuid The organization UUID
- *
* @return array Array of usernames who are beheerders in this organization
*/
public function getOrganizationBeheerders(string $organizationUuid): array
{
try {
- $beheerders = [];
+ $beheerders = [];
$beheerderGroup = $this->_groupManager->get('beheerder');
-
- if (!$beheerderGroup) {
+
+ if ($beheerderGroup === null) {
return [];
}
-
- // Get all users in beheerder group
+
+ // Get all users in beheerder group.
$beheerderUsers = $beheerderGroup->getUsers();
-
- // Filter users who belong to this organization
+
+ // Filter users who belong to this organization.
foreach ($beheerderUsers as $user) {
- if ($this->userBelongsToOrganization($user, $organizationUuid)) {
+ if ($this->userBelongsToOrganization(user: $user, organizationUuid: $organizationUuid) === true) {
$beheerders[] = $user->getUID();
}
}
-
- // Sort by user creation date (oldest first)
- usort($beheerders, function($a, $b) {
- $userA = $this->_userManager->get($a);
- $userB = $this->_userManager->get($b);
-
- // Get user creation timestamps (fallback to 0 if not available)
- $timeA = $userA ? ($userA->getLastLogin() ?: 0) : 0;
- $timeB = $userB ? ($userB->getLastLogin() ?: 0) : 0;
-
- return $timeA <=> $timeB;
- });
-
+
+ // Sort by user creation date (oldest first).
+ usort(
+ $beheerders,
+ function ($a, $b) {
+ $userA = $this->_userManager->get($a);
+ $userB = $this->_userManager->get($b);
+
+ // Get user creation timestamps (fallback to 0 if not available).
+ if ($userA !== null) {
+ $lastLoginA = $userA->getLastLogin();
+ if ($lastLoginA !== 0 && $lastLoginA !== null && $lastLoginA !== false) {
+ $timeA = $lastLoginA;
+ } else {
+ $timeA = 0;
+ }
+ } else {
+ $timeA = 0;
+ }
+
+ if ($userB !== null) {
+ $lastLoginB = $userB->getLastLogin();
+ if ($lastLoginB !== 0 && $lastLoginB !== null && $lastLoginB !== false) {
+ $timeB = $lastLoginB;
+ } else {
+ $timeB = 0;
+ }
+ } else {
+ $timeB = 0;
+ }
+
+ return $timeA <=> $timeB;
+ }
+ );
+
$this->_logger->info(
'Found organization beheerders',
[
'organization' => $organizationUuid,
- 'beheerders' => $beheerders
+ 'beheerders' => $beheerders,
]
);
-
+
return $beheerders;
-
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to get organization beheerders: ' . $e->getMessage(),
+ 'Failed to get organization beheerders: '.$e->getMessage(),
[
'organization' => $organizationUuid,
- 'exception' => $e
+ 'exception' => $e,
]
);
return [];
- }
- }
+ }//end try
+ }//end getOrganizationBeheerders()
/**
- * Checks if a user belongs to an organization
+ * Checks if a user belongs to an organization.
+ *
+ * @param IUser $user The user to check
+ * @param string $organizationUuid The organization UUID
*
- * @param IUser $user The user to check
- * @param string $organizationUuid The organization UUID
- *
* @return bool True if user belongs to organization
*/
public function userBelongsToOrganization(IUser $user, string $organizationUuid): bool
{
try {
- // Check if user is in the organization-specific group
- $organizationGroupName = $this->sanitizeGroupName($organizationUuid);
- $organizationGroup = $this->_groupManager->get($organizationGroupName);
-
- if ($organizationGroup && $organizationGroup->inGroup($user)) {
+ // Check if user is in the organization-specific group.
+ $organizationGroupName = $this->sanitizeGroupName(name: $organizationUuid);
+ $organizationGroup = $this->_groupManager->get($organizationGroupName);
+
+ if ($organizationGroup !== null && $organizationGroup->inGroup($user) === true) {
return true;
}
-
- // Alternative approach: check user's groups for organization-specific groups
+
+ // Alternative approach: check user's groups for organization-specific groups.
$userGroups = $this->_groupManager->getUserGroups($user);
foreach ($userGroups as $group) {
- // Check if any group name contains the organization UUID
- if (strpos($group->getGID(), $organizationUuid) !== false) {
+ // Check if any group name contains the organization UUID.
+ if (strpos(haystack: $group->getGID(), needle: $organizationUuid) !== false) {
return true;
}
}
-
+
return false;
-
} catch (\Exception $e) {
$this->_logger->error(
- 'Failed to check user organization membership: ' . $e->getMessage(),
+ 'Failed to check user organization membership: '.$e->getMessage(),
[
- 'username' => $user->getUID(),
- 'organization' => $organizationUuid
+ 'username' => $user->getUID(),
+ 'organization' => $organizationUuid,
]
);
return false;
- }
- }
-}
\ No newline at end of file
+ }//end try
+ }//end userBelongsToOrganization()
+}//end class
diff --git a/lib/Service/SoftwareCatalogueService.php b/lib/Service/SoftwareCatalogueService.php
index c4e2608d..2c6b4bec 100644
--- a/lib/Service/SoftwareCatalogueService.php
+++ b/lib/Service/SoftwareCatalogueService.php
@@ -3,14 +3,13 @@
/**
* Software Catalogue Service
*
- * Service for handling software catalog specific operations including
+ * Service for handling software catalog specific operations including
* user management, contact processing, and object lifecycle management.
*
* @category Service
* @package OCA\SoftwareCatalog\Service
* @author Conduction b.v.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
@@ -28,7 +27,7 @@
use OCP\App\IAppManager;
/**
- * Service for handling software catalog operations
+ * Service for handling software catalog operations.
*
* Provides functionality for user management, contact processing,
* email notifications, and object lifecycle management.
@@ -37,27 +36,29 @@
* @package OCA\SoftwareCatalog\Service
* @author Conduction b.v.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
- * @version 1.0.0
* @link https://github.com/ConductionNL/SoftwareCatalog
*/
class SoftwareCatalogueService
{
+
/**
* The name of the app
*
* @var string
*/
- private string $_appName;
+ private string $appName;
/**
* SoftwareCatalogueService constructor
*
- * @param OrganizationHandler $_organizationHandler Organization handler
- * @param ContactPersonHandler $_contactPersonHandler Contact person handler
- * @param GroupHandler $_groupHandler Group handler
- * @param HierarchyHandler $_hierarchyHandler Hierarchy handler
- * @param SymfonyEmailService $_emailService Email service
- * @param LoggerInterface $_logger Logger interface
+ * @param OrganizationHandler $_organizationHandler Organization handler.
+ * @param ContactPersonHandler $_contactPersonHandler Contact person handler.
+ * @param GroupHandler $_groupHandler Group handler.
+ * @param HierarchyHandler $_hierarchyHandler Hierarchy handler.
+ * @param SymfonyEmailService $_emailService Email service.
+ * @param LoggerInterface $_logger Logger interface.
+ * @param ContainerInterface $_container Container interface.
+ * @param IAppManager $_appManager App manager interface.
*/
public function __construct(
private readonly OrganizationHandler $_organizationHandler,
@@ -70,45 +71,45 @@ public function __construct(
private readonly IAppManager $_appManager,
) {
$this->_appName = 'softwarecatalog';
- }
+ }//end __construct()
/**
* Gets the ObjectService instance
*
* @return \OCA\OpenRegister\Service\ObjectService|null
*/
- private function _getObjectService(): ?\OCA\OpenRegister\Service\ObjectService
+ private function getObjectService(): ?\OCA\OpenRegister\Service\ObjectService
{
- if (!$this->_appManager->isEnabledForUser('openregister')) {
+ if ($this->_appManager->isEnabledForUser(appId: 'openregister') === false) {
return null;
}
try {
return $this->_container->get('OCA\\OpenRegister\\Service\\ObjectService');
} catch (\Exception $e) {
- $this->_logger->error('Failed to get ObjectService: ' . $e->getMessage());
+ $this->_logger->error('Failed to get ObjectService: '.$e->getMessage());
return null;
}
- }
+ }//end getObjectService()
/**
* Gets the OrganisationService instance
*
* @return \OCA\OpenRegister\Service\OrganisationService|null
*/
- private function _getOrganisationService(): ?\OCA\OpenRegister\Service\OrganisationService
+ private function getOrganisationService(): ?\OCA\OpenRegister\Service\OrganisationService
{
- if (!$this->_appManager->isEnabledForUser('openregister')) {
+ if ($this->_appManager->isEnabledForUser(appId: 'openregister') === false) {
return null;
}
try {
return $this->_container->get('OCA\\OpenRegister\\Service\\OrganisationService');
} catch (\Exception $e) {
- $this->_logger->error('Failed to get OrganisationService: ' . $e->getMessage());
+ $this->_logger->error('Failed to get OrganisationService: '.$e->getMessage());
return null;
}
- }
+ }//end getOrganisationService()
/**
* Processes a contactpersoon object to create an inactive user
@@ -118,159 +119,202 @@ private function _getOrganisationService(): ?\OCA\OpenRegister\Service\Organisat
*
* @param object $contactpersoonObject The contactpersoon object to process
* @param bool $isUpdate Whether this is an update operation (defaults to false)
- *
+ *
* @return bool True if processing was successful
* @throws \Exception If processing fails
*/
- public function processContactpersoon(object $contactpersoonObject, bool $isUpdate = false): bool
+ public function processContactpersoon(object $contactpersoonObject, bool $isUpdate=false): bool
{
$startTime = microtime(true);
-
+
try {
- $objectId = $contactpersoonObject->getId();
+ $objectId = $contactpersoonObject->getId();
$objectData = $contactpersoonObject->getObject();
-
- $this->_logger->info('SoftwareCatalogueService: Starting contactpersoon processing', [
- 'objectId' => $objectId,
- 'objectData' => $objectData,
- 'timestamp' => date('Y-m-d H:i:s')
- ]);
-
- // Delegate to contact person handler
- $this->_logger->debug('SoftwareCatalogueService: Delegating to ContactPersonHandler for contactpersoon processing', [
- 'objectId' => $objectId
- ]);
-
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Starting contactpersoon processing',
+ [
+ 'objectId' => $objectId,
+ 'objectData' => $objectData,
+ 'timestamp' => date('Y-m-d H:i:s'),
+ ]
+ );
+
+ // Delegate to contact person handler.
+ $this->_logger->debug(
+ 'SoftwareCatalogueService: Delegating to ContactPersonHandler for contactpersoon processing',
+ [
+ 'objectId' => $objectId,
+ ]
+ );
+
$result = $this->_contactPersonHandler->processContactpersoon($contactpersoonObject, $isUpdate);
-
- $this->_logger->info('SoftwareCatalogueService: ContactPersonHandler processing completed', [
- 'objectId' => $objectId,
- 'result' => $result,
- 'processingTime' => round((microtime(true) - $startTime) * 1000, 2) . 'ms'
- ]);
-
- if ($result) {
- // Get the username from the processed object
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: ContactPersonHandler processing completed',
+ [
+ 'objectId' => $objectId,
+ 'result' => $result,
+ 'processingTime' => round((microtime(true) - $startTime) * 1000, 2).'ms',
+ ]
+ );
+
+ if (empty($result) === false) {
+ // Get the username from the processed object.
$updatedObjectData = $contactpersoonObject->getObject();
- $username = $updatedObjectData['username'] ?? '';
-
- $this->_logger->info('SoftwareCatalogueService: Username extracted from processed object', [
- 'objectId' => $objectId,
- 'username' => $username,
- 'hasUsername' => !empty($username)
- ]);
-
- if (!empty($username)) {
- // NOTE: Group assignment is already handled by ContactPersonHandler.assignUserGroups()
- // during user creation, so we don't need to call GroupHandler.updateUserGroups() here
+ $username = $updatedObjectData['username'] ?? '';
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Username extracted from processed object',
+ [
+ 'objectId' => $objectId,
+ 'username' => $username,
+ 'hasUsername' => empty($username) === false,
+ ]
+ );
+
+ if (empty($username) === false) {
+ // NOTE: Group assignment is already handled by ContactPersonHandler.assignUserGroups().
+ // during user creation, so we don't need to call GroupHandler.updateUserGroups() here.
// as it would overwrite the correct group assignments.
-
- // Ensure organization has beheerder and set up manager relationships
- $this->_logger->debug('SoftwareCatalogueService: Ensuring organization beheerder', [
- 'objectId' => $objectId,
- 'username' => $username
- ]);
-
+ // Ensure organization has beheerder and set up manager relationships.
+ $this->_logger->debug(
+ 'SoftwareCatalogueService: Ensuring organization beheerder',
+ [
+ 'objectId' => $objectId,
+ 'username' => $username,
+ ]
+ );
+
$this->_hierarchyHandler->ensureOrganizationBeheerder($contactpersoonObject, $username);
-
- // Set user to inactive initially
- $this->_logger->debug('SoftwareCatalogueService: Setting user to inactive', [
- 'objectId' => $objectId,
- 'username' => $username
- ]);
-
+
+ // Set user to inactive initially.
+ $this->_logger->debug(
+ 'SoftwareCatalogueService: Setting user to inactive',
+ [
+ 'objectId' => $objectId,
+ 'username' => $username,
+ ]
+ );
+
$this->_contactPersonHandler->setUserInactive($username);
-
- $this->_logger->info('SoftwareCatalogueService: User setup completed', [
- 'objectId' => $objectId,
- 'username' => $username,
- 'totalProcessingTime' => round((microtime(true) - $startTime) * 1000, 2) . 'ms'
- ]);
-
- // Add the newly created user to the organization entity
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: User setup completed',
+ [
+ 'objectId' => $objectId,
+ 'username' => $username,
+ 'totalProcessingTime' => round((microtime(true) - $startTime) * 1000, 2).'ms',
+ ]
+ );
+
+ // Add the newly created user to the organization entity.
$organisatie = $objectData['organisatie'] ?? null;
- if ($organisatie) {
- $this->_logger->info('SoftwareCatalogueService: Adding user to organization entity', [
- 'objectId' => $objectId,
- 'username' => $username,
- 'organisatie' => $organisatie
- ]);
-
+ if (empty($organisatie) === false) {
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Adding user to organization entity',
+ [
+ 'objectId' => $objectId,
+ 'username' => $username,
+ 'organisatie' => $organisatie,
+ ]
+ );
+
try {
$organisationMapper = $this->_container->get('OCA\\OpenRegister\\Db\\OrganisationMapper');
- $organisation = $organisationMapper->findByUuid($organisatie);
-
- if ($organisation) {
+ $organisation = $organisationMapper->findByUuid($organisatie);
+
+ if (empty($organisation) === false) {
$currentUsers = $organisation->getUsers() ?? [];
- if (!in_array($username, $currentUsers)) {
+ if (in_array($username, $currentUsers) === false) {
$currentUsers[] = $username;
$organisation->setUsers($currentUsers);
$organisationMapper->save($organisation);
-
- $this->_logger->info('SoftwareCatalogueService: Successfully added user to organization entity', [
- 'objectId' => $objectId,
- 'username' => $username,
- 'organisatie' => $organisatie,
- 'totalUsers' => count($currentUsers)
- ]);
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Successfully added user to organization entity',
+ [
+ 'objectId' => $objectId,
+ 'username' => $username,
+ 'organisatie' => $organisatie,
+ 'totalUsers' => count($currentUsers),
+ ]
+ );
} else {
- $this->_logger->info('SoftwareCatalogueService: User already in organization entity', [
- 'objectId' => $objectId,
- 'username' => $username,
- 'organisatie' => $organisatie
- ]);
- }
+ $this->_logger->info(
+ 'SoftwareCatalogueService: User already in organization entity',
+ [
+ 'objectId' => $objectId,
+ 'username' => $username,
+ 'organisatie' => $organisatie,
+ ]
+ );
+ }//end if
} else {
- $this->_logger->warning('SoftwareCatalogueService: Organization entity not found', [
- 'objectId' => $objectId,
- 'username' => $username,
- 'organisatie' => $organisatie
- ]);
- }
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: Organization entity not found',
+ [
+ 'objectId' => $objectId,
+ 'username' => $username,
+ 'organisatie' => $organisatie,
+ ]
+ );
+ }//end if
} catch (\Exception $e) {
- $this->_logger->error('SoftwareCatalogueService: Failed to add user to organization entity', [
- 'objectId' => $objectId,
- 'username' => $username,
- 'organisatie' => $organisatie,
- 'error' => $e->getMessage()
- ]);
- }
+ $this->_logger->error(
+ 'SoftwareCatalogueService: Failed to add user to organization entity',
+ [
+ 'objectId' => $objectId,
+ 'username' => $username,
+ 'organisatie' => $organisatie,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
} else {
- $this->_logger->warning('SoftwareCatalogueService: No organisation reference found for contact person', [
- 'objectId' => $objectId,
- 'username' => $username
- ]);
- }
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: No organisation reference found for contact person',
+ [
+ 'objectId' => $objectId,
+ 'username' => $username,
+ ]
+ );
+ }//end if
} else {
- $this->_logger->warning('SoftwareCatalogueService: No username generated for contactpersoon', [
- 'objectId' => $objectId,
- 'objectData' => $updatedObjectData
- ]);
- }
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: No username generated for contactpersoon',
+ [
+ 'objectId' => $objectId,
+ 'objectData' => $updatedObjectData,
+ ]
+ );
+ }//end if
} else {
- $this->_logger->warning('SoftwareCatalogueService: ContactPersonHandler returned false', [
- 'objectId' => $objectId,
- 'processingTime' => round((microtime(true) - $startTime) * 1000, 2) . 'ms'
- ]);
- }
-
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: ContactPersonHandler returned false',
+ [
+ 'objectId' => $objectId,
+ 'processingTime' => round((microtime(true) - $startTime) * 1000, 2).'ms',
+ ]
+ );
+ }//end if
+
return $result;
-
} catch (\Exception $e) {
$this->_logger->error(
- 'SoftwareCatalogueService: Failed to process contactpersoon object: ' . $e->getMessage(),
+ 'SoftwareCatalogueService: Failed to process contactpersoon object: '.$e->getMessage(),
[
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString(),
- 'objectId' => $contactpersoonObject->getId() ?? 'unknown',
- 'processingTime' => round((microtime(true) - $startTime) * 1000, 2) . 'ms'
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
+ 'objectId' => $contactpersoonObject->getId() ?? 'unknown',
+ 'processingTime' => round((microtime(true) - $startTime) * 1000, 2).'ms',
]
);
throw $e;
- }
- }
+ }//end try
+ }//end processContactpersoon()
/**
* Processes a contactpersoon object to ensure it has a username
@@ -279,712 +323,668 @@ public function processContactpersoon(object $contactpersoonObject, bool $isUpda
* this method will create a user account and set the username property.
*
* @param object $contactpersoonObject The contactpersoon object to process
- *
+ *
* @return bool True if processing was successful
* @throws \Exception If processing fails
*/
-
/**
* Processes organization without contactpersonen processing
- *
- * @deprecated This method is disabled to prevent organization duplication
- * @param object $organizationObject The organization object to process
- *
- * @return bool True if processing was successful
- * @throws \Exception If processing fails
+ *
+ * @param object $organizationObject The organization object to process.
+ *
+ * @return bool True if processing was successful.
+ * @throws \Exception If processing fails.
+ *
+ * @deprecated This method is disabled to prevent organization duplication.
*/
public function processOrganization(object $organizationObject): bool
{
- // DISABLED: Organization processing is disabled to prevent duplication
+ // DISABLED: Organization processing is disabled to prevent duplication.
$this->_logger->info(
'Organization processing is disabled to prevent duplication',
[
- 'organizationId' => $organizationObject->getId()
+ 'organizationId' => $organizationObject->getId(),
]
);
-
+
return false;
-
- /*
- try {
- // Delegate to organization handler for basic processing
- $processed = $this->_organizationHandler->processOrganization($organizationObject);
-
- $this->_logger->info(
- 'Successfully processed organization without contactpersonen',
- [
- 'organizationId' => $organizationObject->getId()
- ]
- );
-
- return $processed;
-
- } catch (\Exception $e) {
- $this->_logger->error(
- 'Failed to process organization: ' . $e->getMessage(),
- [
- 'organizationId' => $organizationObject->getId(),
- 'exception' => $e
- ]
- );
- throw $e;
- }
- */
- }
+
+ // Disabled: organization processing logic removed.
+ }//end processOrganization()
/**
* Updates user groups based on contactpersoon data
*
* @param object $contactpersoonObject The contactpersoon object
- * @param string $username The username to update groups for
- *
+ * @param string $username The username to update groups for
+ *
* @return void
*/
public function updateUserGroups(object $contactpersoonObject, string $username): void
{
- // Use the new organization type-based logic instead of old role-based logic
- $user = $this->_userManager->get($username);
- if ($user) {
+ // Use the new organization type-based logic instead of old role-based logic.
+ $user = $this->_container->get(\OCP\IUserManager::class)->get($username);
+ if (empty($user) === false) {
$contactData = $contactpersoonObject->getObject();
$this->_contactPersonHandler->updateUserGroupsFromContactData($user, $contactData);
} else {
$this->_logger->warning('User not found for group update', ['username' => $username]);
}
- }
+ }//end updateUserGroups()
/**
* Ensures organization has at least one beheerder and manages user hierarchy
*
* @param object $contactpersoonObject The contactpersoon object
- * @param string $username The username being processed
- *
+ * @param string $username The username being processed
+ *
* @return void
*/
public function ensureOrganizationBeheerder(object $contactpersoonObject, string $username): void
{
- // Delegate to hierarchy handler
+ // Delegate to hierarchy handler.
$this->_hierarchyHandler->ensureOrganizationBeheerder($contactpersoonObject, $username);
- }
+ }//end ensureOrganizationBeheerder()
/**
* Gets a user's manager
*
* @param string $username The username
- *
+ *
* @return string|null The manager's username or null if not set
*/
public function getUserManager(string $username): ?string
{
- // Delegate to contact person handler
+ // Delegate to contact person handler.
return $this->_contactPersonHandler->getUserManager($username);
- }
+ }//end getUserManager()
/**
* Handles new organization creation - syncs with OpenRegister and processes organization
*
* @param object $organizationObject The new organization object
- *
+ *
* @return void
*/
public function handleNewOrganization(object $organizationObject): void
{
try {
- $this->_logger->info('SoftwareCatalogueService: Handling new organization', [
- 'objectId' => $organizationObject->getId()
- ]);
-
- // First, sync the organization with OpenRegister
- $syncResult = $this->syncOrganizationWithOpenRegister($organizationObject);
-
- if ($syncResult) {
- $this->_logger->info('SoftwareCatalogueService: Successfully synced organization with OpenRegister', [
- 'objectId' => $organizationObject->getId()
- ]);
-
- // Update organization references on objects to point to the newly created organization entity
- $this->updateOrganizationReferences($organizationObject);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Handling new organization',
+ [
+ 'objectId' => $organizationObject->getId(),
+ ]
+ );
+
+ // First, sync the organization with OpenRegister.
+ $syncResult = $this->syncOrganizationWithOpenRegister(organizationObject: $organizationObject);
+
+ if (empty($syncResult) === false) {
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Successfully synced organization with OpenRegister',
+ [
+ 'objectId' => $organizationObject->getId(),
+ ]
+ );
+
+ // Update organization references on objects to point to the newly created organization entity.
+ $this->updateOrganizationReferences(organizationObject: $organizationObject);
} else {
- $this->_logger->warning('SoftwareCatalogueService: Failed to sync organization with OpenRegister', [
- 'objectId' => $organizationObject->getId()
- ]);
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: Failed to sync organization with OpenRegister',
+ [
+ 'objectId' => $organizationObject->getId(),
+ ]
+ );
}
- // Process the organization (existing functionality) - this creates users
- $this->processOrganization($organizationObject);
-
- // Add all admin group users to the organization
- $objectData = $organizationObject->getObject();
+ // Process the organization (existing functionality) - this creates users.
+ $this->processOrganization(organizationObject: $organizationObject);
+
+ // Add all admin group users to the organization.
+ $objectData = $organizationObject->getObject();
$organizationUuid = $objectData['id'] ?? $organizationObject->getId();
- $this->addAdminGroupUsersToOrganization($organizationUuid);
-
- // Handle ownership assignment for anonymous user registrations AFTER user creation
- $this->handleOwnershipAssignment($organizationObject);
-
- // Send welcome email for new organization
- $this->sendOrganizationWelcomeEmail($organizationObject);
-
- // If organization is active, send activation email too
- $objectData = $organizationObject->getObject();
+ $this->addAdminGroupUsersToOrganization(organizationUuid: $organizationUuid);
+
+ // Handle ownership assignment for anonymous user registrations AFTER user creation.
+ $this->handleOwnershipAssignment(organizationObject: $organizationObject);
+
+ // Send welcome email for new organization.
+ $this->sendOrganizationWelcomeEmail(organizationObject: $organizationObject);
+
+ // If organization is active, send activation email too.
+ $objectData = $organizationObject->getObject();
$beoordeling = strtolower($objectData['beoordeling'] ?? '');
-
+
if ($beoordeling === 'actief') {
try {
$success = $this->_emailService->sendOrganizationActivationEmail($objectData);
- $this->_logger->info('Organization activation email sent', [
- 'objectId' => $organizationObject->getId(),
- 'success' => $success
- ]);
+ $this->_logger->info(
+ 'Organization activation email sent',
+ [
+ 'objectId' => $organizationObject->getId(),
+ 'success' => $success,
+ ]
+ );
} catch (\Exception $e) {
- $this->_logger->error('Failed to send organization activation email: ' . $e->getMessage(), [
- 'objectId' => $organizationObject->getId(),
- 'exception' => $e
- ]);
+ $this->_logger->error(
+ 'Failed to send organization activation email: '.$e->getMessage(),
+ [
+ 'objectId' => $organizationObject->getId(),
+ 'exception' => $e,
+ ]
+ );
}
}
-
- // Process nested contact persons and add their users to the organization entity
+
+ // Process nested contact persons and add their users to the organization entity.
$contactpersonen = $objectData['contactpersonen'] ?? [];
- if (!empty($contactpersonen)) {
- $this->_logger->info('SoftwareCatalogueService: Processing nested contact persons', [
- 'objectId' => $organizationObject->getId(),
- 'contactPersonCount' => count($contactpersonen)
- ]);
-
+ if (empty($contactpersonen) === false) {
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Processing nested contact persons',
+ [
+ 'objectId' => $organizationObject->getId(),
+ 'contactPersonCount' => count($contactpersonen),
+ ]
+ );
+
$organizationUuid = $objectData['id'] ?? $organizationObject->getId();
- $objectService = $this->_getObjectService();
-
- if ($objectService) {
- $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
+ $objectService = $this->getObjectService();
+
+ if (empty($objectService) === false) {
+ $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
$voorzieningenConfig = $settingsService->getVoorzieningenConfig();
- $contactSchemaId = $voorzieningenConfig['contactpersoon_schema'] ?? null;
-
- if (!$contactSchemaId) {
+ $contactSchemaId = $voorzieningenConfig['contactpersoon_schema'] ?? null;
+
+ if ($contactSchemaId === null) {
$this->_logger->warning('SoftwareCatalogueService: Missing contactpersoon schema configuration');
return;
}
+
$organisationMapper = $this->_container->get('OCA\\OpenRegister\\Db\\OrganisationMapper');
- $organisation = $organisationMapper->findByUuid($organizationUuid);
-
- if ($organisation) {
+ $organisation = $organisationMapper->findByUuid($organizationUuid);
+
+ if (empty($organisation) === false) {
$currentUsers = $organisation->getUsers() ?? [];
- $addedUsers = [];
-
+ $addedUsers = [];
+
foreach ($contactpersonen as $contactPersonId) {
try {
$contactPersonObject = $objectService->find($contactPersonId);
- $contactData = $contactPersonObject->getObject();
+ $contactData = $contactPersonObject->getObject();
$email = $contactData['email'] ?? null;
-
- if ($email && !in_array($email, $currentUsers)) {
+
+ if ($email !== false && in_array($email, $currentUsers) === false) {
$currentUsers[] = $email;
- $addedUsers[] = $email;
-
- $this->_logger->info('SoftwareCatalogueService: Added nested contact person user to organization', [
- 'objectId' => $organizationObject->getId(),
- 'contactPersonId' => $contactPersonId,
- 'username' => $email,
- 'organizationUuid' => $organizationUuid
- ]);
+ $addedUsers[] = $email;
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Added nested contact person user to organization',
+ [
+ 'objectId' => $organizationObject->getId(),
+ 'contactPersonId' => $contactPersonId,
+ 'username' => $email,
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
}
} catch (\Exception $e) {
- $this->_logger->warning('SoftwareCatalogueService: Failed to process nested contact person', [
- 'objectId' => $organizationObject->getId(),
- 'contactPersonId' => $contactPersonId,
- 'error' => $e->getMessage()
- ]);
- }
- }
-
- if (!empty($addedUsers)) {
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: Failed to process nested contact person',
+ [
+ 'objectId' => $organizationObject->getId(),
+ 'contactPersonId' => $contactPersonId,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end foreach
+
+ if (empty($addedUsers) === false) {
$organisation->setUsers($currentUsers);
$organisationMapper->save($organisation);
-
- $this->_logger->info('SoftwareCatalogueService: Successfully updated organization with nested contact person users', [
- 'objectId' => $organizationObject->getId(),
- 'organizationUuid' => $organizationUuid,
- 'addedUsers' => $addedUsers,
- 'totalUsers' => count($currentUsers)
- ]);
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Successfully updated organization with nested contact person users',
+ [
+ 'objectId' => $organizationObject->getId(),
+ 'organizationUuid' => $organizationUuid,
+ 'addedUsers' => $addedUsers,
+ 'totalUsers' => count($currentUsers),
+ ]
+ );
}
- }
- }
- }
-
- // Final synchronization: ensure all contact persons associated with this organization are in the users array
- // This handles cases where contact persons were created separately and not as nested objects
- $objectData = $organizationObject->getObject();
+ }//end if
+ }//end if
+ }//end if
+
+ // Final synchronization: ensure all contact persons associated with this organization are in the users array.
+ // This handles cases where contact persons were created separately and not as nested objects.
+ $objectData = $organizationObject->getObject();
$organizationUuid = $objectData['id'] ?? $organizationObject->getId();
- $this->syncContactPersonUsernamesWithOrganization($organizationUuid);
-
- $this->_logger->info('SoftwareCatalogueService: Completed final contact person synchronization for new organization', [
- 'objectId' => $organizationObject->getId(),
- 'organizationUuid' => $organizationUuid
- ]);
-
+ $this->syncContactPersonUsernamesWithOrganization(organizationUuid: $organizationUuid);
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Completed final contact person synchronization for new organization',
+ [
+ 'objectId' => $organizationObject->getId(),
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
} catch (\Exception $e) {
$this->_logger->error(
- 'SoftwareCatalogueService: Failed to handle new organization: ' . $e->getMessage(),
+ 'SoftwareCatalogueService: Failed to handle new organization: '.$e->getMessage(),
[
- 'objectId' => $organizationObject->getId(),
+ 'objectId' => $organizationObject->getId(),
'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
]
);
- }
- }
+ }//end try
+ }//end handleNewOrganization()
/**
* Handles organization updates - syncs with OpenRegister and manages user status based on organization status
*
* @param object $organizationObject The updated organization object
* @param object $oldOrganizationObject The previous organization object
- *
+ *
* @return void
*/
public function handleOrganizationUpdate(object $organizationObject, object $oldOrganizationObject): void
{
try {
- $this->_logger->info('SoftwareCatalogueService: Handling organization update', [
- 'objectId' => $organizationObject->getId()
- ]);
-
- $newData = $organizationObject->getObject();
- $oldData = $oldOrganizationObject->getObject();
-
- // Check both 'beoordeling' and 'status' fields (different schemas use different field names)
- $newBeoordeling = strtolower($newData['beoordeling'] ?? $newData['status'] ?? '');
- $oldBeoordeling = strtolower($oldData['beoordeling'] ?? $oldData['status'] ?? '');
-
- // Sync the organization with OpenRegister
- $syncResult = $this->syncOrganizationWithOpenRegister($organizationObject);
-
- if ($syncResult) {
- $this->_logger->info('SoftwareCatalogueService: Successfully synced organization with OpenRegister', [
- 'objectId' => $organizationObject->getId()
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Handling organization update',
+ [
+ 'objectId' => $organizationObject->getId(),
+ ]
+ );
+
+ $newData = $organizationObject->getObject();
+ $oldData = $oldOrganizationObject->getObject();
+
+ // Check both 'beoordeling' and 'status' fields (different schemas use different field names).
+ $newBeoordeling = strtolower($newData['beoordeling'] ?? $newData['status'] ?? '');
+ $oldBeoordeling = strtolower($oldData['beoordeling'] ?? $oldData['status'] ?? '');
+
+ // Sync the organization with OpenRegister.
+ $syncResult = $this->syncOrganizationWithOpenRegister(organizationObject: $organizationObject);
+
+ if (empty($syncResult) === false) {
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Successfully synced organization with OpenRegister',
+ [
+ 'objectId' => $organizationObject->getId(),
+ ]
+ );
} else {
- $this->_logger->warning('SoftwareCatalogueService: Failed to sync organization with OpenRegister', [
- 'objectId' => $organizationObject->getId()
- ]);
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: Failed to sync organization with OpenRegister',
+ [
+ 'objectId' => $organizationObject->getId(),
+ ]
+ );
}
-
- // Add all admin group users to the organization (ensure they're always included)
+
+ // Add all admin group users to the organization (ensure they're always included).
$organizationUuid = $newData['id'] ?? $organizationObject->getId();
- $this->addAdminGroupUsersToOrganization($organizationUuid);
-
- // Check if organization status changed to active
+ $this->addAdminGroupUsersToOrganization(organizationUuid: $organizationUuid);
+
+ // Check if organization status changed to active.
if ($newBeoordeling === 'actief') {
$becameActive = ($oldBeoordeling !== 'actief');
-
+
+ if ($becameActive === true) {
+ $activeMessage = 'Organization became active, activating users';
+ } else {
+ $activeMessage = 'Organization is active';
+ }
+
$this->_logger->info(
- $becameActive ? 'Organization became active, activating users' : 'Organization is active',
+ $activeMessage,
[
'organizationId' => $organizationObject->getId(),
'oldBeoordeling' => $oldBeoordeling,
'newBeoordeling' => $newBeoordeling,
- 'becameActive' => $becameActive
+ 'becameActive' => $becameActive,
]
);
-
- if ($becameActive) {
+
+ if (empty($becameActive) === false) {
$organizationUuid = $newData['id'] ?? $organizationObject->getId();
-
- $this->_logger->info('SoftwareCatalogueService: Organization became active - creating users from contactpersonen', [
- 'organizationUuid' => $organizationUuid
- ]);
-
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Organization became active - creating users from contactpersonen',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
+
// Process the organization to create users from contactpersonen.
- // This is crucial when an organization is activated for the first time
+ // This is crucial when an organization is activated for the first time.
// and contactpersonen were added before activation.
- $this->processOrganization($organizationObject);
-
- // Activate SoftwareCatalog-specific users in this organization
- $this->activateSoftwareCatalogUsersForOrganization($organizationUuid);
-
- // Send activation email
+ $this->processOrganization(organizationObject: $organizationObject);
+
+ // Activate SoftwareCatalog-specific users in this organization.
+ $this->activateSoftwareCatalogUsersForOrganization(organizationUuid: $organizationUuid);
+
+ // Send activation email.
try {
$success = $this->_emailService->sendOrganizationActivationEmail($newData);
- $this->_logger->info('Organization activation email sent', [
- 'objectId' => $organizationObject->getId(),
- 'success' => $success
- ]);
+ $this->_logger->info(
+ 'Organization activation email sent',
+ [
+ 'objectId' => $organizationObject->getId(),
+ 'success' => $success,
+ ]
+ );
} catch (\Exception $e) {
- $this->_logger->error('Failed to send organization activation email: ' . $e->getMessage(), [
- 'objectId' => $organizationObject->getId(),
- 'exception' => $e
- ]);
+ $this->_logger->error(
+ 'Failed to send organization activation email: '.$e->getMessage(),
+ [
+ 'objectId' => $organizationObject->getId(),
+ 'exception' => $e,
+ ]
+ );
}
- }
- }
-
- // Check if organization status changed to inactive
+ }//end if
+ }//end if
+
+ // Check if organization status changed to inactive.
if ($newBeoordeling === 'inactief' || $newBeoordeling === 'deactief') {
$becameInactive = ($oldBeoordeling === 'actief');
-
+
+ if ($becameInactive === true) {
+ $inactiveMessage = 'Organization became inactive, deactivating users';
+ } else {
+ $inactiveMessage = 'Organization is inactive';
+ }
+
$this->_logger->info(
- $becameInactive ? 'Organization became inactive, deactivating users' : 'Organization is inactive',
+ $inactiveMessage,
[
'organizationId' => $organizationObject->getId(),
'oldBeoordeling' => $oldBeoordeling,
'newBeoordeling' => $newBeoordeling,
- 'becameInactive' => $becameInactive
+ 'becameInactive' => $becameInactive,
]
);
-
- if ($becameInactive) {
- // Deactivate SoftwareCatalog-specific users in this organization
+
+ if (empty($becameInactive) === false) {
+ // Deactivate SoftwareCatalog-specific users in this organization.
$organizationUuid = $newData['id'] ?? $organizationObject->getId();
- $this->deactivateSoftwareCatalogUsersForOrganization($organizationUuid);
+ $this->deactivateSoftwareCatalogUsersForOrganization(organizationUuid: $organizationUuid);
}
- }
-
+ }//end if
} catch (\Exception $e) {
$this->_logger->error(
- 'SoftwareCatalogueService: Failed to handle organization update: ' . $e->getMessage(),
+ 'SoftwareCatalogueService: Failed to handle organization update: '.$e->getMessage(),
[
- 'objectId' => $organizationObject->getId(),
+ 'objectId' => $organizationObject->getId(),
'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
]
);
- }
- }
+ }//end try
+ }//end handleOrganizationUpdate()
/**
- * Activates contactpersonen users for an organization
- *
- * @deprecated This method is disabled to prevent organization duplication
- * @param string $organizationId The organization ID
- *
+ * Activates contactpersonen users for an organization.
+ *
+ * @param string $organizationId The organization ID.
+ *
* @return void
+ *
+ * @deprecated This method is disabled to prevent organization duplication.
*/
private function activateContactpersonenForOrganization(string $organizationId): void
{
- // DISABLED: Organization handling is disabled to prevent duplication
+ // DISABLED: Organization handling is disabled to prevent duplication.
$this->_logger->info(
'Organization contactpersonen activation is disabled to prevent duplication',
[
- 'organizationId' => $organizationId
+ 'organizationId' => $organizationId,
]
);
-
+
return;
-
- /*
- try {
- $this->_logger->info('Activating contactpersonen for organization', [
- 'organizationId' => $organizationId
- ]);
-
- // Get ObjectService to find contactpersonen
- $objectService = $this->_getObjectService();
- if (!$objectService) {
- $this->_logger->error('ObjectService not available');
- return;
- }
-
- // Get settings service to get schema IDs
- $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
- $voorzieningenConfig = $settingsService->getVoorzieningenConfig();
- $registerId = $voorzieningenConfig['register'] ?? null;
-
- // Find contactpersonen related to this organization
- $contactpersoonSchemaId = $voorzieningenConfig['contactpersoon_schema'] ?? null;
-
- // Skip if no proper configuration is available
- if (!$registerId || !$contactpersoonSchemaId) {
- $this->_logger->warning('SoftwareCatalogueService: Missing Voorzieningen configuration for contactpersonen', [
- 'organizationId' => $organizationId,
- 'registerId' => $registerId,
- 'contactpersoonSchemaId' => $contactpersoonSchemaId
- ]);
- return $organizationData;
- }
-
- $this->_logger->info('Schema IDs for contactpersonen search', [
- 'organizationId' => $organizationId,
- 'registerId' => $registerId,
- 'contactpersoonSchemaId' => $contactpersoonSchemaId
- ]);
-
- $activatedUsers = [];
-
- // Check contactpersoon objects (new data model)
- if ($contactpersoonSchemaId) {
- $contactpersoonObjects = $objectService->findAll(
- (int) $registerId,
- (int) $contactpersoonSchemaId,
- ['organisation' => $organizationId]
- );
-
- foreach ($contactpersoonObjects as $contactpersoonObject) {
- $contactData = $contactpersoonObject->getObject();
- $username = $contactData['username'] ?? '';
-
- if (!empty($username)) {
- $success = $this->_contactPersonHandler->setUserActive($username);
- if ($success) {
- $activatedUsers[] = $username;
- $this->_logger->info('Activated contactpersoon user', [
- 'username' => $username,
- 'organizationId' => $organizationId,
- 'contactpersoonId' => $contactpersoonObject->getId()
- ]);
- }
- }
- }
- }
-
- // Only use contactpersoon objects now - contactgegevens is deprecated
-
- $this->_logger->info('Completed contactpersonen activation for organization', [
- 'organizationId' => $organizationId,
- 'activatedUsers' => $activatedUsers,
- 'totalActivated' => count($activatedUsers)
- ]);
-
- } catch (\Exception $e) {
- $this->_logger->error(
- 'Failed to activate contactpersonen for organization: ' . $e->getMessage(),
- [
- 'organizationId' => $organizationId,
- 'exception' => $e
- ]
- );
- }
- */
- }
+
+ // Disabled: organization contactpersonen activation logic removed.
+ }//end activateContactpersonenForOrganization()
/**
- * Sends welcome email to organization
- *
- * @deprecated This method is disabled to prevent organization duplication
- * @param object $organizationObject The organization object
- *
+ * Sends welcome email to organization.
+ *
+ * @param object $organizationObject The organization object.
+ *
* @return void
+ *
+ * @deprecated This method is disabled to prevent organization duplication.
*/
public function sendOrganizationWelcomeEmail(object $organizationObject): void
{
- // DISABLED: Organization handling is disabled to prevent duplication
+ // DISABLED: Organization handling is disabled to prevent duplication.
$this->_logger->info(
'Organization welcome email sending is disabled to prevent duplication',
[
- 'organizationId' => $organizationObject->getId()
+ 'organizationId' => $organizationObject->getId(),
]
);
-
+
return;
-
- /*
- try {
- $this->_logger->info('Sending organization welcome email', [
- 'objectId' => $organizationObject->getId()
- ]);
-
- $objectData = $organizationObject->getObject();
-
- // Send organization registration email
- $success = $this->_emailService->sendOrganizationRegistrationEmail($objectData);
-
- if ($success) {
- $this->_logger->info('Organization welcome email sent successfully', [
- 'objectId' => $organizationObject->getId()
- ]);
- } else {
- $this->_logger->warning('Failed to send organization welcome email', [
- 'objectId' => $organizationObject->getId()
- ]);
- }
- } catch (\Exception $e) {
- $this->_logger->error('Exception sending organization welcome email: ' . $e->getMessage(), [
- 'objectId' => $organizationObject->getId(),
- 'exception' => $e
- ]);
- }
- */
- }
+
+ // Disabled: organization welcome email logic removed.
+ }//end sendOrganizationWelcomeEmail()
/**
* Handles new contact creation
*
* @param object $contactObject The contact object
- *
+ *
* @return void
*/
public function handleNewContact(object $contactObject): void
{
- // Delegate to contact person handler
+ // Delegate to contact person handler.
$this->_contactPersonHandler->handleNewContact($contactObject);
- }
+ }//end handleNewContact()
/**
* Creates user for contact if not exists
*
* @param object $contactObject The contact object
- *
+ *
* @return void
*/
public function createUserForContactIfNotExists(object $contactObject): void
{
- // Implementation for creating user from contact
- $this->_logger->info('Creating user for contact if not exists', [
- 'objectId' => $contactObject->getId()
- ]);
- }
+ // Implementation for creating user from contact.
+ $this->_logger->info(
+ 'Creating user for contact if not exists',
+ [
+ 'objectId' => $contactObject->getId(),
+ ]
+ );
+ }//end createUserForContactIfNotExists()
/**
* Handles new gebruiker creation
*
* @param object $gebruikerObject The gebruiker object
- *
+ *
* @return void
*/
public function handleNewGebruiker(object $gebruikerObject): void
{
- // Implementation for handling new gebruiker
- $this->_logger->info('Handling new gebruiker', [
- 'objectId' => $gebruikerObject->getId()
- ]);
- }
+ // Implementation for handling new gebruiker.
+ $this->_logger->info(
+ 'Handling new gebruiker',
+ [
+ 'objectId' => $gebruikerObject->getId(),
+ ]
+ );
+ }//end handleNewGebruiker()
/**
* Sends welcome email to gebruiker
*
* @param object $gebruikerObject The gebruiker object
- *
+ *
* @return void
*/
public function sendGebruikerWelcomeEmail(object $gebruikerObject): void
{
- // Implementation for sending gebruiker welcome email
- $this->_logger->info('Sending gebruiker welcome email', [
- 'objectId' => $gebruikerObject->getId()
- ]);
- }
+ // Implementation for sending gebruiker welcome email.
+ $this->_logger->info(
+ 'Sending gebruiker welcome email',
+ [
+ 'objectId' => $gebruikerObject->getId(),
+ ]
+ );
+ }//end sendGebruikerWelcomeEmail()
/**
* Handles contact update
*
* @param object $contactObject The contact object
- *
+ *
* @return void
*/
public function handleContactUpdate(object $contactObject): void
{
- // Delegate to contact person handler
+ // Delegate to contact person handler.
$this->_contactPersonHandler->handleContactUpdate($contactObject);
- }
+ }//end handleContactUpdate()
/**
* Handles gebruiker update
*
* @param object $gebruikerObject The new gebruiker object
* @param object $oldGebruikerObject The old gebruiker object
- *
+ *
* @return void
*/
public function handleGebruikerUpdate(object $gebruikerObject, object $oldGebruikerObject): void
{
- // Implementation for handling gebruiker updates
- $this->_logger->info('Handling gebruiker update', [
- 'objectId' => $gebruikerObject->getId()
- ]);
- }
+ // Implementation for handling gebruiker updates.
+ $this->_logger->info(
+ 'Handling gebruiker update',
+ [
+ 'objectId' => $gebruikerObject->getId(),
+ ]
+ );
+ }//end handleGebruikerUpdate()
/**
* Handles contact deletion
*
* @param object $contactObject The contact object
- *
+ *
* @return void
*/
public function handleContactDeletion(object $contactObject): void
{
- // Delegate to contact person handler
+ // Delegate to contact person handler.
$this->_contactPersonHandler->handleContactDeletion($contactObject);
- }
+ }//end handleContactDeletion()
/**
* Blocks user for gebruiker
*
* @param object $gebruikerObject The gebruiker object
- *
+ *
* @return void
*/
public function blockUserForGebruiker(object $gebruikerObject): void
{
- // Implementation for blocking user
- $this->_logger->info('Blocking user for gebruiker', [
- 'objectId' => $gebruikerObject->getId()
- ]);
- }
-
- /**
+ // Implementation for blocking user.
+ $this->_logger->info(
+ 'Blocking user for gebruiker',
+ [
+ 'objectId' => $gebruikerObject->getId(),
+ ]
+ );
+ }//end blockUserForGebruiker()
+
+ /**
* Temporarily blocks user for gebruiker
*
* @param object $gebruikerObject The gebruiker object
- *
+ *
* @return void
*/
public function temporarilyBlockUserForGebruiker(object $gebruikerObject): void
{
- // Implementation for temporarily blocking user
- $this->_logger->info('Temporarily blocking user for gebruiker', [
- 'objectId' => $gebruikerObject->getId()
- ]);
- }
+ // Implementation for temporarily blocking user.
+ $this->_logger->info(
+ 'Temporarily blocking user for gebruiker',
+ [
+ 'objectId' => $gebruikerObject->getId(),
+ ]
+ );
+ }//end temporarilyBlockUserForGebruiker()
/**
* Restores user access for gebruiker
*
* @param object $gebruikerObject The gebruiker object
- *
+ *
* @return void
*/
public function restoreUserAccessForGebruiker(object $gebruikerObject): void
{
- // Implementation for restoring user access
- $this->_logger->info('Restoring user access for gebruiker', [
- 'objectId' => $gebruikerObject->getId()
- ]);
- }
+ // Implementation for restoring user access.
+ $this->_logger->info(
+ 'Restoring user access for gebruiker',
+ [
+ 'objectId' => $gebruikerObject->getId(),
+ ]
+ );
+ }//end restoreUserAccessForGebruiker()
/**
* Syncs user with reverted contact
*
* @param object $contactObject The contact object
* @param mixed $revertPoint The revert point
- *
+ *
* @return void
*/
public function syncUserWithRevertedContact(object $contactObject, mixed $revertPoint): void
{
- // Implementation for syncing user with reverted contact
- $this->_logger->info('Syncing user with reverted contact', [
- 'objectId' => $contactObject->getId()
- ]);
- }
+ // Implementation for syncing user with reverted contact.
+ $this->_logger->info(
+ 'Syncing user with reverted contact',
+ [
+ 'objectId' => $contactObject->getId(),
+ ]
+ );
+ }//end syncUserWithRevertedContact()
/**
* Updates user from reverted gebruiker
*
* @param object $gebruikerObject The gebruiker object
* @param mixed $revertPoint The revert point
- *
+ *
* @return void
*/
public function updateUserFromRevertedGebruiker(object $gebruikerObject, mixed $revertPoint): void
{
- // Implementation for updating user from reverted gebruiker
- $this->_logger->info('Updating user from reverted gebruiker', [
- 'objectId' => $gebruikerObject->getId()
- ]);
- }
+ // Implementation for updating user from reverted gebruiker.
+ $this->_logger->info(
+ 'Updating user from reverted gebruiker',
+ [
+ 'objectId' => $gebruikerObject->getId(),
+ ]
+ );
+ }//end updateUserFromRevertedGebruiker()
/**
* Gets the list of generic user groups
@@ -994,19 +994,19 @@ public function updateUserFromRevertedGebruiker(object $gebruikerObject, mixed $
public function getGenericUserGroups(): array
{
return $this->_groupHandler->getGenericUserGroups();
- }
+ }//end getGenericUserGroups()
/**
* Sets the list of generic user groups
*
* @param array $groups Array of generic user groups
- *
+ *
* @return void
*/
public function setGenericUserGroups(array $groups): void
{
$this->_groupHandler->setGenericUserGroups($groups);
- }
+ }//end setGenericUserGroups()
/**
* Ensures all generic user groups exist
@@ -1016,365 +1016,440 @@ public function setGenericUserGroups(array $groups): void
public function ensureGenericUserGroupsExist(): array
{
return $this->_groupHandler->ensureGenericUserGroupsExist();
- }
+ }//end ensureGenericUserGroupsExist()
/**
* Gets organizational hierarchy information for a user
*
* @param string $username The username to get hierarchy for
- *
+ *
* @return array Array containing hierarchy information
*/
public function getUserHierarchy(string $username): array
{
return $this->_hierarchyHandler->getUserHierarchy($username);
- }
+ }//end getUserHierarchy()
/**
* Gets complete organizational structure
*
* @param string $organizationUuid The organization UUID
- *
+ *
* @return array Array containing organizational structure
*/
public function getOrganizationStructure(string $organizationUuid): array
{
return $this->_hierarchyHandler->getOrganizationStructure($organizationUuid);
- }
+ }//end getOrganizationStructure()
/**
* Handles contactpersoon updates, particularly role changes
*
* @param object $contactpersoonObject The updated contactpersoon object
* @param object $oldContactpersoonObject The previous contactpersoon object (optional)
- *
+ *
* @return void
*/
- public function handleContactpersoonUpdate(object $contactpersoonObject, object $oldContactpersoonObject = null): void
+ public function handleContactpersoonUpdate(object $contactpersoonObject, object $oldContactpersoonObject=null): void
{
$startTime = microtime(true);
-
+
try {
$objectId = $contactpersoonObject->getId();
- $this->_logger->info('SoftwareCatalogueService: Starting contactpersoon update handling', [
- 'objectId' => $objectId,
- 'hasOldObject' => $oldContactpersoonObject !== null,
- 'timestamp' => date('Y-m-d H:i:s')
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Starting contactpersoon update handling',
+ [
+ 'objectId' => $objectId,
+ 'hasOldObject' => $oldContactpersoonObject !== null,
+ 'timestamp' => date('Y-m-d H:i:s'),
+ ]
+ );
- // Get current and old data for comparison
+ // Get current and old data for comparison.
$newData = $contactpersoonObject->getObject();
- $oldData = $oldContactpersoonObject ? $oldContactpersoonObject->getObject() : [];
-
+ if ($oldContactpersoonObject !== null) {
+ $oldData = $oldContactpersoonObject->getObject();
+ } else {
+ $oldData = [];
+ }
+
$newRoles = $newData['roles'] ?? [];
$oldRoles = $oldData['roles'] ?? [];
-
- $this->_logger->debug('SoftwareCatalogueService: Comparing roles for contactpersoon update', [
- 'objectId' => $objectId,
- 'newRoles' => $newRoles,
- 'oldRoles' => $oldRoles,
- 'newRolesType' => gettype($newRoles),
- 'oldRolesType' => gettype($oldRoles)
- ]);
-
- // Ensure both are arrays
- if (!is_array($newRoles)) {
+
+ $this->_logger->debug(
+ 'SoftwareCatalogueService: Comparing roles for contactpersoon update',
+ [
+ 'objectId' => $objectId,
+ 'newRoles' => $newRoles,
+ 'oldRoles' => $oldRoles,
+ 'newRolesType' => gettype($newRoles),
+ 'oldRolesType' => gettype($oldRoles),
+ ]
+ );
+
+ // Ensure both are arrays.
+ if (is_array($newRoles) === false) {
$newRoles = [$newRoles];
- $this->_logger->debug('SoftwareCatalogueService: Converted newRoles to array', [
- 'objectId' => $objectId,
- 'newRoles' => $newRoles
- ]);
+ $this->_logger->debug(
+ 'SoftwareCatalogueService: Converted newRoles to array',
+ [
+ 'objectId' => $objectId,
+ 'newRoles' => $newRoles,
+ ]
+ );
}
- if (!is_array($oldRoles)) {
+
+ if (is_array($oldRoles) === false) {
$oldRoles = [$oldRoles];
- $this->_logger->debug('SoftwareCatalogueService: Converted oldRoles to array', [
- 'objectId' => $objectId,
- 'oldRoles' => $oldRoles
- ]);
+ $this->_logger->debug(
+ 'SoftwareCatalogueService: Converted oldRoles to array',
+ [
+ 'objectId' => $objectId,
+ 'oldRoles' => $oldRoles,
+ ]
+ );
}
-
- // For updates, we need to handle differently based on whether roles changed
+
+ // For updates, we need to handle differently based on whether roles changed.
if ($newRoles !== $oldRoles) {
- // Roles changed - use role-based group assignment instead of generic group assignment
+ // Roles changed - use role-based group assignment instead of generic group assignment.
$this->_logger->info(
'SoftwareCatalogueService: Roles changed for contactpersoon, using role-based group assignment',
[
'contactpersoonId' => $objectId,
- 'oldRoles' => $oldRoles,
- 'newRoles' => $newRoles,
- 'addedRoles' => array_diff($newRoles, $oldRoles),
- 'removedRoles' => array_diff($oldRoles, $newRoles)
+ 'oldRoles' => $oldRoles,
+ 'newRoles' => $newRoles,
+ 'addedRoles' => array_diff($newRoles, $oldRoles),
+ 'removedRoles' => array_diff($oldRoles, $newRoles),
]
);
-
- // Ensure user exists (but don't assign generic groups)
+
+ // Ensure user exists (but don't assign generic groups).
$username = $newData['username'] ?? '';
- if (empty($username)) {
- // Generate username and create user if needed
+ if (empty($username) === true) {
+ // Generate username and create user if needed.
$result = $this->_contactPersonHandler->processContactpersoon($contactpersoonObject, true);
- if ($result) {
+ if (empty($result) === false) {
$updatedData = $contactpersoonObject->getObject();
- $username = $updatedData['username'] ?? '';
+ $username = $updatedData['username'] ?? '';
}
}
-
- if (!empty($username)) {
+
+ if (empty($username) === false) {
$user = $this->_container->get(\OCP\IUserManager::class)->get($username);
- if ($user) {
- // Use new organization type-based logic instead of old role-based logic
+ if (empty($user) === false) {
+ // Use new organization type-based logic instead of old role-based logic.
$contactData = $contactpersoonObject->getObject();
$this->_contactPersonHandler->updateUserGroupsFromContactData($user, $contactData);
-
- $this->_logger->info('SoftwareCatalogueService: Organization type-based group updates completed', [
- 'username' => $username,
- 'objectId' => $objectId,
- 'newRoles' => $newRoles
- ]);
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Organization type-based group updates completed',
+ [
+ 'username' => $username,
+ 'objectId' => $objectId,
+ 'newRoles' => $newRoles,
+ ]
+ );
} else {
- $this->_logger->warning('SoftwareCatalogueService: User not found for role-based group updates', [
- 'username' => $username,
- 'objectId' => $objectId
- ]);
- }
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: User not found for role-based group updates',
+ [
+ 'username' => $username,
+ 'objectId' => $objectId,
+ ]
+ );
+ }//end if
} else {
- $this->_logger->warning('SoftwareCatalogueService: No username available for role-based group updates', [
- 'objectId' => $objectId,
- 'newData' => $newData
- ]);
- }
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: No username available for role-based group updates',
+ [
+ 'objectId' => $objectId,
+ 'newData' => $newData,
+ ]
+ );
+ }//end if
} else {
- // No role changes - use standard processing (assigns generic groups)
- $this->_logger->debug('SoftwareCatalogueService: No role changes, using standard contactpersoon processing', [
- 'objectId' => $objectId,
- 'roles' => $newRoles
- ]);
-
- $result = $this->processContactpersoon($contactpersoonObject, true);
-
- $this->_logger->info('SoftwareCatalogueService: Standard contactpersoon processing completed', [
- 'objectId' => $objectId,
- 'result' => $result,
- 'processingTime' => round((microtime(true) - $startTime) * 1000, 2) . 'ms'
- ]);
- }
-
- $this->_logger->info('SoftwareCatalogueService: Contactpersoon update handling completed', [
- 'objectId' => $objectId,
- 'totalProcessingTime' => round((microtime(true) - $startTime) * 1000, 2) . 'ms'
- ]);
-
+ // No role changes - use standard processing (assigns generic groups).
+ $this->_logger->debug(
+ 'SoftwareCatalogueService: No role changes, using standard contactpersoon processing',
+ [
+ 'objectId' => $objectId,
+ 'roles' => $newRoles,
+ ]
+ );
+
+ $result = $this->processContactpersoon(contactpersoonObject: $contactpersoonObject, isUpdate: true);
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Standard contactpersoon processing completed',
+ [
+ 'objectId' => $objectId,
+ 'result' => $result,
+ 'processingTime' => round((microtime(true) - $startTime) * 1000, 2).'ms',
+ ]
+ );
+ }//end if
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Contactpersoon update handling completed',
+ [
+ 'objectId' => $objectId,
+ 'totalProcessingTime' => round((microtime(true) - $startTime) * 1000, 2).'ms',
+ ]
+ );
} catch (\Exception $e) {
$this->_logger->error(
- 'SoftwareCatalogueService: Failed to handle contactpersoon update: ' . $e->getMessage(),
+ 'SoftwareCatalogueService: Failed to handle contactpersoon update: '.$e->getMessage(),
[
- 'objectId' => $contactpersoonObject->getId(),
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString(),
- 'processingTime' => round((microtime(true) - $startTime) * 1000, 2) . 'ms'
+ 'objectId' => $contactpersoonObject->getId(),
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
+ 'processingTime' => round((microtime(true) - $startTime) * 1000, 2).'ms',
]
);
- }
- }
-
+ }//end try
+ }//end handleContactpersoonUpdate()
/**
* Handles organization deletion - deactivates all users in the organization
*
* @param object $organizationObject The organization object being deleted
- *
+ *
* @return void
*/
public function handleOrganizationDeletion(object $organizationObject): void
{
try {
- $this->_logger->info('SoftwareCatalogueService: Handling organization deletion', [
- 'objectId' => $organizationObject->getId()
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Handling organization deletion',
+ [
+ 'objectId' => $organizationObject->getId(),
+ ]
+ );
- $objectData = $organizationObject->getObject();
+ $objectData = $organizationObject->getObject();
$organizationUuid = $objectData['id'] ?? $organizationObject->getId();
- // Deactivate all users in this organization
- $this->deactivateUsersForOrganization($organizationUuid);
+ // Deactivate all users in this organization.
+ $this->deactivateUsersForOrganization(organizationUuid: $organizationUuid);
$this->_logger->info(
'SoftwareCatalogueService: Successfully handled organization deletion',
[
'organizationId' => $organizationUuid,
- 'timestamp' => date('Y-m-d H:i:s')
+ 'timestamp' => date('Y-m-d H:i:s'),
]
);
-
} catch (\Exception $e) {
$this->_logger->error(
- 'SoftwareCatalogueService: Failed to handle organization deletion: ' . $e->getMessage(),
+ 'SoftwareCatalogueService: Failed to handle organization deletion: '.$e->getMessage(),
[
- 'objectId' => $organizationObject->getId(),
+ 'objectId' => $organizationObject->getId(),
'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
]
);
- }
- }
+ }//end try
+ }//end handleOrganizationDeletion()
/**
* Syncs organization data with OpenRegister
*
* @param object $organizationObject The organization object to sync
- *
+ *
* @return bool True if sync was successful
*/
public function syncOrganizationWithOpenRegister(object $organizationObject): bool
{
try {
- $this->_logger->info('SoftwareCatalogueService: SYNC_STEP_1 - Starting syncOrganizationWithOpenRegister', [
- 'objectId' => $organizationObject->getId(),
- 'objectClass' => get_class($organizationObject)
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: SYNC_STEP_1 - Starting syncOrganizationWithOpenRegister',
+ [
+ 'objectId' => $organizationObject->getId(),
+ 'objectClass' => get_class($organizationObject),
+ ]
+ );
- $objectData = $organizationObject->getObject();
+ $objectData = $organizationObject->getObject();
$organizationUuid = $objectData['id'] ?? $organizationObject->getId();
-
- $this->_logger->info('SoftwareCatalogueService: SYNC_STEP_2 - Extracted organization data', [
- 'organizationUuid' => $organizationUuid,
- 'objectDataKeys' => array_keys($objectData),
- 'hasId' => isset($objectData['id'])
- ]);
-
- // Get OpenRegister OrganisationService for proper organization entity management
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: SYNC_STEP_2 - Extracted organization data',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'objectDataKeys' => array_keys($objectData),
+ 'hasId' => isset($objectData['id']) === true,
+ ]
+ );
+
+ // Get OpenRegister OrganisationService for proper organization entity management.
$this->_logger->info('SoftwareCatalogueService: SYNC_STEP_3 - Getting OrganisationService');
- $organisationService = $this->_getOrganisationService();
- if (!$organisationService) {
+ $organisationService = $this->getOrganisationService();
+ if ($organisationService === null) {
$this->_logger->error('SoftwareCatalogueService: SYNC_STEP_3 - OpenRegister OrganisationService not available');
return false;
}
- $this->_logger->info('SoftwareCatalogueService: SYNC_STEP_3 - OrganisationService retrieved', [
- 'serviceClass' => get_class($organisationService)
- ]);
- $this->_logger->info('SoftwareCatalogueService: SYNC_STEP_4 - OpenRegister configuration', [
- 'organizationUuid' => $organizationUuid,
- 'organizationName' => $objectData['naam'] ?? 'Unknown'
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: SYNC_STEP_3 - OrganisationService retrieved',
+ [
+ 'serviceClass' => get_class($organisationService),
+ ]
+ );
- // Check if organization already exists in OpenRegister
+ $this->_logger->info(
+ 'SoftwareCatalogueService: SYNC_STEP_4 - OpenRegister configuration',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'organizationName' => $objectData['naam'] ?? 'Unknown',
+ ]
+ );
+
+ // Check if organization already exists in OpenRegister.
$this->_logger->info('SoftwareCatalogueService: SYNC_STEP_5 - Checking if organization exists in OpenRegister');
try {
$this->_logger->info('SoftwareCatalogueService: SYNC_STEP_5A - Getting OrganisationMapper for lookup');
$organisationMapper = $this->_container->get('OCA\\OpenRegister\\Db\\OrganisationMapper');
- $this->_logger->info('SoftwareCatalogueService: SYNC_STEP_5B - Calling findByUuid', [
- 'uuid' => $organizationUuid
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: SYNC_STEP_5B - Calling findByUuid',
+ [
+ 'uuid' => $organizationUuid,
+ ]
+ );
$existingOrganisation = $organisationMapper->findByUuid($organizationUuid);
-
- // Organization exists - update it
- $this->_logger->info('SoftwareCatalogueService: SYNC_STEP_6 - Organization exists in OpenRegister, updating', [
- 'organizationId' => $organizationUuid,
- 'existingOrganisationClass' => get_class($existingOrganisation)
- ]);
- // Map status from SoftwareCatalog to OpenRegister
+ // Organization exists - update it.
+ $this->_logger->info(
+ 'SoftwareCatalogueService: SYNC_STEP_6 - Organization exists in OpenRegister, updating',
+ [
+ 'organizationId' => $organizationUuid,
+ 'existingOrganisationClass' => get_class($existingOrganisation),
+ ]
+ );
+
+ // Map status from SoftwareCatalog to OpenRegister.
$this->_logger->info('SoftwareCatalogueService: SYNC_STEP_7 - Mapping organization data');
- $mappedData = $this->mapOrganizationDataForOpenRegister($objectData);
-
- // Update the organization using OrganisationService
- $updatedOrganisation = $this->updateOrganisationInOpenRegister($organisationService, $existingOrganisation, $mappedData);
+ $mappedData = $this->mapOrganizationDataForOpenRegister(objectData: $objectData);
- $this->_logger->info('SoftwareCatalogueService: Successfully updated organization in OpenRegister', [
- 'organizationId' => $organizationUuid,
- 'openRegisterId' => $updatedOrganisation->getUuid()
- ]);
+ // Update the organization using OrganisationService.
+ $updatedOrganisation = $this->updateOrganisationInOpenRegister(
+ organisationService: $organisationService,
+ existingOrganisation: $existingOrganisation,
+ mappedData: $mappedData
+ );
- return true;
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Successfully updated organization in OpenRegister',
+ [
+ 'organizationId' => $organizationUuid,
+ 'openRegisterId' => $updatedOrganisation->getUuid(),
+ ]
+ );
+ return true;
} catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
- // Organization doesn't exist - create it
- $this->_logger->info('SoftwareCatalogueService: SYNC_STEP_8 - Organization not found in OpenRegister, creating', [
- 'organizationId' => $organizationUuid,
- 'exception' => $e->getMessage()
- ]);
+ // Organization doesn't exist - create it.
+ $this->_logger->info(
+ 'SoftwareCatalogueService: SYNC_STEP_8 - Organization not found in OpenRegister, creating',
+ [
+ 'organizationId' => $organizationUuid,
+ 'exception' => $e->getMessage(),
+ ]
+ );
- // Map status from SoftwareCatalog to OpenRegister
+ // Map status from SoftwareCatalog to OpenRegister.
$this->_logger->info('SoftwareCatalogueService: SYNC_STEP_9 - Mapping organization data for creation');
- $mappedData = $this->mapOrganizationDataForOpenRegister($objectData);
-
- // Create the organization using OrganisationService
+ $mappedData = $this->mapOrganizationDataForOpenRegister(objectData: $objectData);
+
+ // Create the organization using OrganisationService.
$this->_logger->info('SoftwareCatalogueService: SYNC_STEP_10 - Calling createOrganisationInOpenRegister');
- $createdOrganisation = $this->createOrganisationInOpenRegisterInternal($organisationService, $mappedData, $organizationUuid);
+ $createdOrganisation = $this->createOrganisationInOpenRegisterInternal(
+ organisationService: $organisationService,
+ mappedData: $mappedData,
+ organizationUuid: $organizationUuid
+ );
- $this->_logger->info('SoftwareCatalogueService: SYNC_STEP_11 - Successfully created organization in OpenRegister', [
- 'organizationId' => $organizationUuid,
- 'openRegisterId' => $createdOrganisation->getUuid(),
- 'createdOrganisationClass' => get_class($createdOrganisation)
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: SYNC_STEP_11 - Successfully created organization in OpenRegister',
+ [
+ 'organizationId' => $organizationUuid,
+ 'openRegisterId' => $createdOrganisation->getUuid(),
+ 'createdOrganisationClass' => get_class($createdOrganisation),
+ ]
+ );
return true;
- }
-
+ }//end try
} catch (\Exception $e) {
$this->_logger->error(
- 'SoftwareCatalogueService: Failed to sync organization with OpenRegister: ' . $e->getMessage(),
+ 'SoftwareCatalogueService: Failed to sync organization with OpenRegister: '.$e->getMessage(),
[
- 'objectId' => $organizationObject->getId(),
+ 'objectId' => $organizationObject->getId(),
'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
]
);
return false;
- }
- }
+ }//end try
+ }//end syncOrganizationWithOpenRegister()
/**
* Public wrapper for creating organization in OpenRegister (used by background job)
*
* @param array $objectData The organization object data
- *
+ *
* @return object|null The created organisation entity or null on failure
*/
public function createOrganisationInOpenRegister(array $objectData): ?object
{
try {
$organizationUuid = $objectData['id'] ?? null;
- if (!$organizationUuid) {
+ if ($organizationUuid === null) {
$this->_logger->error('SoftwareCatalogueService: No organization UUID provided for creation');
return null;
}
-
- // Map the data
+
+ // Map the data.
$mappedData = [
- 'naam' => $objectData['naam'] ?? 'Unknown',
- 'type' => $objectData['type'] ?? '',
- 'website' => $objectData['website'] ?? '',
- 'active' => $this->mapStatus($objectData['beoordeling'] ?? 'actief'),
+ 'naam' => $objectData['naam'] ?? 'Unknown',
+ 'type' => $objectData['type'] ?? '',
+ 'website' => $objectData['website'] ?? '',
+ 'active' => $this->mapStatus(status: $objectData['beoordeling'] ?? 'actief'),
'contactpersonen' => $objectData['contactpersonen'] ?? [],
- 'deelnemers' => $objectData['deelnemers'] ?? []
+ 'deelnemers' => $objectData['deelnemers'] ?? [],
];
-
- // Get organisation service
+
+ // Get organisation service.
$organisationService = $this->_container->get('OCA\OpenRegister\Service\OrganisationService');
-
- return $this->createOrganisationInOpenRegisterInternal($organisationService, $mappedData, $organizationUuid);
-
+
+ return $this->createOrganisationInOpenRegisterInternal(
+ organisationService: $organisationService,
+ mappedData: $mappedData,
+ organizationUuid: $organizationUuid
+ );
} catch (\Exception $e) {
- $this->_logger->error('SoftwareCatalogueService: Error in public createOrganisationInOpenRegister', [
- 'error' => $e->getMessage(),
- 'objectData' => $objectData
- ]);
+ $this->_logger->error(
+ 'SoftwareCatalogueService: Error in public createOrganisationInOpenRegister',
+ [
+ 'error' => $e->getMessage(),
+ 'objectData' => $objectData,
+ ]
+ );
return null;
- }
- }
+ }//end try
+ }//end createOrganisationInOpenRegister()
/**
* Map status from Software Catalog to OpenRegister format
*
* @param string $status The status from Software Catalog
- *
+ *
* @return bool The mapped active status for OpenRegister
*/
private function mapStatus(string $status): bool
@@ -1385,17 +1460,18 @@ private function mapStatus(string $status): bool
case 'inactief':
return false;
default:
- return true; // Default to active
+ return true;
+ // Default to active.
}
- }
+ }//end mapStatus()
/**
* Creates an organization in OpenRegister using OrganisationService
*
* @param \OCA\OpenRegister\Service\OrganisationService $organisationService The OpenRegister organisation service
- * @param array $mappedData The mapped organization data
- * @param string $organizationUuid The organization UUID to use
- *
+ * @param array $mappedData The mapped organization data
+ * @param string $organizationUuid The organization UUID to use
+ *
* @return \OCA\OpenRegister\Db\Organisation The created organization
*/
private function createOrganisationInOpenRegisterInternal(
@@ -1403,197 +1479,263 @@ private function createOrganisationInOpenRegisterInternal(
array $mappedData,
string $organizationUuid
): \OCA\OpenRegister\Db\Organisation {
- $this->_logger->info('SoftwareCatalogueService: STEP 1 - Starting createOrganisationInOpenRegister', [
- 'organizationUuid' => $organizationUuid,
- 'name' => $mappedData['naam'] ?? 'Unknown',
- 'mappedDataKeys' => array_keys($mappedData)
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: STEP 1 - Starting createOrganisationInOpenRegister',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'name' => $mappedData['naam'] ?? 'Unknown',
+ 'mappedDataKeys' => array_keys($mappedData),
+ ]
+ );
- // Check if we're in an anonymous context (no logged-in user)
+ // Check if we're in an anonymous context (no logged-in user).
$userSession = \OC::$server->getUserSession();
$currentUser = $userSession->getUser();
-
- $this->_logger->info('SoftwareCatalogueService: STEP 2 - Checking user context', [
- 'hasUserSession' => $userSession !== null,
- 'currentUser' => $currentUser ? $currentUser->getUID() : 'null',
- 'isAnonymous' => $currentUser === null
- ]);
-
- if (!$currentUser) {
- $this->_logger->info('SoftwareCatalogueService: STEP 3A - Anonymous path: No user logged in, creating organization directly via mapper', [
- 'organizationUuid' => $organizationUuid
- ]);
-
- // Keep the original UUID format - no conversion needed
- $this->_logger->info('SoftwareCatalogueService: STEP 3B - Using original UUID format for OpenRegister (anonymous)', [
- 'organizationUuid' => $organizationUuid
- ]);
-
- // Create organization directly via mapper to avoid user context requirements
+
+ if ($currentUser !== null) {
+ $currentUserValue = $currentUser->getUID();
+ } else {
+ $currentUserValue = 'null';
+ }
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: STEP 2 - Checking user context',
+ [
+ 'hasUserSession' => $userSession !== null,
+ 'currentUser' => $currentUserValue,
+ 'isAnonymous' => $currentUser === null,
+ ]
+ );
+
+ if ($currentUser === null) {
+ $this->_logger->info(
+ 'SoftwareCatalogueService: STEP 3A - Anonymous path: No user logged in, creating organization directly via mapper',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
+
+ // Keep the original UUID format - no conversion needed.
+ $this->_logger->info(
+ 'SoftwareCatalogueService: STEP 3B - Using original UUID format for OpenRegister (anonymous)',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
+
+ // Create organization directly via mapper to avoid user context requirements.
$this->_logger->info('SoftwareCatalogueService: STEP 3C - Getting OrganisationMapper from container');
$organisationMapper = $this->_container->get('OCA\\OpenRegister\\Db\\OrganisationMapper');
- $this->_logger->info('SoftwareCatalogueService: STEP 3D - OrganisationMapper retrieved', [
- 'mapperClass' => get_class($organisationMapper)
- ]);
-
- // Create a new Organisation entity
+ $this->_logger->info(
+ 'SoftwareCatalogueService: STEP 3D - OrganisationMapper retrieved',
+ [
+ 'mapperClass' => get_class($organisationMapper),
+ ]
+ );
+
+ // Create a new Organisation entity.
$this->_logger->info('SoftwareCatalogueService: STEP 3E - Creating new Organisation entity');
$organisation = new \OCA\OpenRegister\Db\Organisation();
-
- $this->_logger->info('SoftwareCatalogueService: STEP 3F - Setting organisation properties', [
- 'name' => $mappedData['naam'] ?? 'Unknown Organization',
- 'description' => $mappedData['website'] ?? '',
- 'uuid' => $organizationUuid
- ]);
-
- // Collect all contact person usernames for this organization
- $contactPersonUsernames = $this->collectContactPersonUsernames($organizationUuid, $mappedData);
-
- // Start with admin user and add all contact person usernames
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: STEP 3F - Setting organisation properties',
+ [
+ 'name' => $mappedData['naam'] ?? 'Unknown Organization',
+ 'description' => $mappedData['website'] ?? '',
+ 'uuid' => $organizationUuid,
+ ]
+ );
+
+ // Collect all contact person usernames for this organization.
+ $contactPersonUsernames = $this->collectContactPersonUsernames(organizationUuid: $organizationUuid, objectData: $mappedData);
+
+ // Start with admin user and add all contact person usernames.
$allUsernames = array_merge(['admin'], $contactPersonUsernames);
$allUsernames = array_unique($allUsernames);
-
- $this->_logger->info('SoftwareCatalogueService: STEP 3F_2 - Collected usernames for organization', [
- 'organizationUuid' => $organizationUuid,
- 'totalUsernames' => count($allUsernames),
- 'usernames' => $allUsernames
- ]);
-
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: STEP 3F_2 - Collected usernames for organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'totalUsernames' => count($allUsernames),
+ 'usernames' => $allUsernames,
+ ]
+ );
+
$organisation->setName($mappedData['naam'] ?? 'Unknown Organization');
- $organisation->setDescription($mappedData['website'] ?? ''); // Use website as description
+ $organisation->setDescription($mappedData['website'] ?? '');
+ // Use website as description.
$organisation->setUuid($organizationUuid);
$organisation->setUsers($allUsernames);
- $organisation->setOwner('admin'); // Set admin as owner for anonymous registrations
- $organisation->setActive($mappedData['active'] ?? true); // Set active status based on organization beoordeling
-
- // Debug: Check if UUID was set correctly
- $this->_logger->info('SoftwareCatalogueService: STEP 3G - Debug - UUID before save', [
- 'setUuid' => $organizationUuid,
- 'getUuid' => $organisation->getUuid(),
- 'uuidMatches' => $organisation->getUuid() === $organizationUuid,
- 'organisationClass' => get_class($organisation)
- ]);
-
- // Save the organization
+ $organisation->setOwner('admin');
+ // Set admin as owner for anonymous registrations.
+ $organisation->setActive($mappedData['active'] ?? true);
+ // Set active status based on organization beoordeling.
+ // Debug: Check if UUID was set correctly.
+ $this->_logger->info(
+ 'SoftwareCatalogueService: STEP 3G - Debug - UUID before save',
+ [
+ 'setUuid' => $organizationUuid,
+ 'getUuid' => $organisation->getUuid(),
+ 'uuidMatches' => $organisation->getUuid() === $organizationUuid,
+ 'organisationClass' => get_class($organisation),
+ ]
+ );
+
+ // Save the organization.
$this->_logger->info('SoftwareCatalogueService: STEP 3H - Calling organisationMapper->save()');
try {
$savedOrganisation = $organisationMapper->save($organisation);
$this->_logger->info('SoftwareCatalogueService: STEP 3I - organisationMapper->save() completed successfully');
} catch (\Exception $e) {
- $this->_logger->error('SoftwareCatalogueService: STEP 3I - organisationMapper->save() failed', [
- 'error' => $e->getMessage(),
- 'errorClass' => get_class($e),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->_logger->error(
+ 'SoftwareCatalogueService: STEP 3I - organisationMapper->save() failed',
+ [
+ 'error' => $e->getMessage(),
+ 'errorClass' => get_class($e),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
throw $e;
}
-
- $this->_logger->info('SoftwareCatalogueService: Successfully created organization in OpenRegister via mapper', [
- 'organizationUuid' => $organizationUuid,
- 'openRegisterId' => $savedOrganisation->getUuid(),
- 'savedUuid' => $savedOrganisation->getUuid(),
- 'expectedUuid' => $organizationUuid
- ]);
-
- // Verify the UUID was preserved
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Successfully created organization in OpenRegister via mapper',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'openRegisterId' => $savedOrganisation->getUuid(),
+ 'savedUuid' => $savedOrganisation->getUuid(),
+ 'expectedUuid' => $organizationUuid,
+ ]
+ );
+
+ // Verify the UUID was preserved.
if ($savedOrganisation->getUuid() !== $organizationUuid) {
- $this->_logger->warning('SoftwareCatalogueService: UUID mismatch after saving organization', [
- 'expectedUuid' => $organizationUuid,
- 'actualUuid' => $savedOrganisation->getUuid()
- ]);
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: UUID mismatch after saving organization',
+ [
+ 'expectedUuid' => $organizationUuid,
+ 'actualUuid' => $savedOrganisation->getUuid(),
+ ]
+ );
}
-
+
return $savedOrganisation;
} else {
- $this->_logger->info('SoftwareCatalogueService: STEP 4A - Authenticated path: User logged in, creating organization via mapper', [
- 'organizationUuid' => $organizationUuid,
- 'currentUser' => $currentUser->getUID()
- ]);
-
- // Keep the original UUID format - no conversion needed
- $this->_logger->info('SoftwareCatalogueService: STEP 4B - Using original UUID format for OpenRegister', [
- 'organizationUuid' => $organizationUuid
- ]);
-
- // Create organization directly via mapper to avoid service issues
+ $this->_logger->info(
+ 'SoftwareCatalogueService: STEP 4A - Authenticated path: User logged in, creating organization via mapper',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'currentUser' => $currentUser->getUID(),
+ ]
+ );
+
+ // Keep the original UUID format - no conversion needed.
+ $this->_logger->info(
+ 'SoftwareCatalogueService: STEP 4B - Using original UUID format for OpenRegister',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
+
+ // Create organization directly via mapper to avoid service issues.
$this->_logger->info('SoftwareCatalogueService: STEP 4C - Getting OrganisationMapper from container');
$organisationMapper = $this->_container->get('OCA\\OpenRegister\\Db\\OrganisationMapper');
- $this->_logger->info('SoftwareCatalogueService: STEP 4D - OrganisationMapper retrieved', [
- 'mapperClass' => get_class($organisationMapper)
- ]);
-
- // Debug: Check UUID before creating
- // Collect all contact person usernames for this organization
- $contactPersonUsernames = $this->collectContactPersonUsernames($organizationUuid, $mappedData);
-
- // Start with current user and add all contact person usernames
+ $this->_logger->info(
+ 'SoftwareCatalogueService: STEP 4D - OrganisationMapper retrieved',
+ [
+ 'mapperClass' => get_class($organisationMapper),
+ ]
+ );
+
+ // Debug: Check UUID before creating.
+ // Collect all contact person usernames for this organization.
+ $contactPersonUsernames = $this->collectContactPersonUsernames(organizationUuid: $organizationUuid, objectData: $mappedData);
+
+ // Start with current user and add all contact person usernames.
$allUsernames = array_merge([$currentUser->getUID()], $contactPersonUsernames);
$allUsernames = array_unique($allUsernames);
-
- $this->_logger->info('SoftwareCatalogueService: STEP 4E - Debug - UUID before createWithUuid', [
- 'organizationUuid' => $organizationUuid,
- 'uuidLength' => strlen($organizationUuid),
- 'uuidIsEmpty' => empty($organizationUuid),
- 'name' => $mappedData['naam'] ?? 'Unknown Organization',
- 'description' => $mappedData['website'] ?? '',
- 'owner' => $currentUser->getUID(),
- 'users' => $allUsernames,
- 'contactPersonUsernames' => $contactPersonUsernames
- ]);
-
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: STEP 4E - Debug - UUID before createWithUuid',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'uuidLength' => strlen($organizationUuid),
+ 'uuidIsEmpty' => empty($organizationUuid) === true,
+ 'name' => $mappedData['naam'] ?? 'Unknown Organization',
+ 'description' => $mappedData['website'] ?? '',
+ 'owner' => $currentUser->getUID(),
+ 'users' => $allUsernames,
+ 'contactPersonUsernames' => $contactPersonUsernames,
+ ]
+ );
+
$this->_logger->info('SoftwareCatalogueService: STEP 4F - Calling organisationMapper->createWithUuid()');
try {
- // Debug: Log the exact parameters being passed
- $this->_logger->info('SoftwareCatalogueService: STEP 4F_DEBUG - Parameters for createWithUuid', [
- 'name' => $mappedData['naam'] ?? 'Unknown Organization',
- 'description' => $mappedData['website'] ?? '',
- 'uuid' => $organizationUuid,
- 'owner' => $currentUser->getUID(),
- 'users' => $allUsernames,
- 'isDefault' => false,
- 'uuidLength' => strlen($organizationUuid),
- 'uuidIsEmpty' => empty($organizationUuid)
- ]);
-
+ // Debug: Log the exact parameters being passed.
+ $this->_logger->info(
+ 'SoftwareCatalogueService: STEP 4F_DEBUG - Parameters for createWithUuid',
+ [
+ 'name' => $mappedData['naam'] ?? 'Unknown Organization',
+ 'description' => $mappedData['website'] ?? '',
+ 'uuid' => $organizationUuid,
+ 'owner' => $currentUser->getUID(),
+ 'users' => $allUsernames,
+ 'isDefault' => false,
+ 'uuidLength' => strlen($organizationUuid),
+ 'uuidIsEmpty' => empty($organizationUuid) === true,
+ ]
+ );
+
$organisation = $organisationMapper->createWithUuid(
$mappedData['naam'] ?? 'Unknown Organization',
- $mappedData['website'] ?? '', // Use website as description
- $organizationUuid, // Pass the original UUID
- $currentUser->getUID(), // Set current user as owner
- $allUsernames, // Add all users including contact persons
- false // Not default
+ $mappedData['website'] ?? '',
+ // Use website as description.
+ $organizationUuid,
+ // Pass the original UUID.
+ $currentUser->getUID(),
+ // Set current user as owner.
+ $allUsernames,
+ // Add all users including contact persons.
+ false
+ // Not default.
);
$this->_logger->info('SoftwareCatalogueService: STEP 4G - organisationMapper->createWithUuid() completed successfully');
} catch (\Exception $e) {
- $this->_logger->error('SoftwareCatalogueService: STEP 4G - organisationMapper->createWithUuid() failed', [
- 'error' => $e->getMessage(),
- 'errorClass' => get_class($e),
- 'trace' => $e->getTraceAsString()
- ]);
+ $this->_logger->error(
+ 'SoftwareCatalogueService: STEP 4G - organisationMapper->createWithUuid() failed',
+ [
+ 'error' => $e->getMessage(),
+ 'errorClass' => get_class($e),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
throw $e;
- }
-
- // Note: OpenRegister Organisation entity doesn't have status or type fields
- // These are managed in the SoftwareCatalog object, not in the OpenRegister organisation
+ }//end try
- $this->_logger->info('SoftwareCatalogueService: Successfully created organization in OpenRegister via service', [
- 'organizationUuid' => $organizationUuid,
- 'openRegisterId' => $organisation->getUuid(),
- 'savedUuid' => $organisation->getUuid(),
- 'expectedUuid' => $organizationUuid
- ]);
+ // Note: OpenRegister Organisation entity doesn't have status or type fields.
+ // These are managed in the SoftwareCatalog object, not in the OpenRegister organisation.
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Successfully created organization in OpenRegister via service',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'openRegisterId' => $organisation->getUuid(),
+ 'savedUuid' => $organisation->getUuid(),
+ 'expectedUuid' => $organizationUuid,
+ ]
+ );
return $organisation;
- }
- }
+ }//end if
+ }//end createOrganisationInOpenRegisterInternal()
/**
* Updates an organization in OpenRegister using OrganisationService
*
- * @param \OCA\OpenRegister\Service\OrganisationService $organisationService The OpenRegister organisation service
- * @param \OCA\OpenRegister\Db\Organisation $existingOrganisation The existing organization
- * @param array $mappedData The mapped organization data
- *
+ * @param \OCA\OpenRegister\Service\OrganisationService $organisationService The OpenRegister organisation service
+ * @param \OCA\OpenRegister\Db\Organisation $existingOrganisation The existing organization
+ * @param array $mappedData The mapped organization data
+ *
* @return \OCA\OpenRegister\Db\Organisation The updated organization
*/
private function updateOrganisationInOpenRegister(
@@ -1601,232 +1743,277 @@ private function updateOrganisationInOpenRegister(
\OCA\OpenRegister\Db\Organisation $existingOrganisation,
array $mappedData
): \OCA\OpenRegister\Db\Organisation {
- $this->_logger->info('SoftwareCatalogueService: Updating organization in OpenRegister', [
- 'organizationUuid' => $existingOrganisation->getUuid(),
- 'name' => $mappedData['name'] ?? 'Unknown'
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Updating organization in OpenRegister',
+ [
+ 'organizationUuid' => $existingOrganisation->getUuid(),
+ 'name' => $mappedData['name'] ?? 'Unknown',
+ ]
+ );
- // Update organization fields (only those that exist on the Organisation entity)
- if (isset($mappedData['name'])) {
+ // Update organization fields (only those that exist on the Organisation entity).
+ if (isset($mappedData['name']) === true) {
$existingOrganisation->setName($mappedData['name']);
}
-
- if (isset($mappedData['description'])) {
+
+ if (isset($mappedData['description']) === true) {
$existingOrganisation->setDescription($mappedData['description']);
}
-
- // Note: OpenRegister Organisation entity doesn't have status or type fields
- // These are managed in the SoftwareCatalog object, not in the OpenRegister organisation
- // Save the updated organization
- $organisationMapper = $this->_container->get('OCA\\OpenRegister\\Db\\OrganisationMapper');
+ // Note: OpenRegister Organisation entity doesn't have status or type fields.
+ // These are managed in the SoftwareCatalog object, not in the OpenRegister organisation.
+ // Save the updated organization.
+ $organisationMapper = $this->_container->get('OCA\\OpenRegister\\Db\\OrganisationMapper');
$updatedOrganisation = $organisationMapper->save($existingOrganisation);
- $this->_logger->info('SoftwareCatalogueService: Successfully updated organization in OpenRegister', [
- 'organizationUuid' => $existingOrganisation->getUuid(),
- 'openRegisterId' => $updatedOrganisation->getUuid()
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Successfully updated organization in OpenRegister',
+ [
+ 'organizationUuid' => $existingOrganisation->getUuid(),
+ 'openRegisterId' => $updatedOrganisation->getUuid(),
+ ]
+ );
return $updatedOrganisation;
- }
+ }//end updateOrganisationInOpenRegister()
/**
- * Collects all contact person usernames associated with an organization
- *
- * @param string $organizationUuid The organization UUID
- * @param array $objectData The organization object data (for nested contact persons)
- *
- * @return array Array of usernames
- */
- private function collectContactPersonUsernames(string $organizationUuid, array $objectData = []): array
+ * Collects all contact person usernames associated with an organization
+ *
+ * @param string $organizationUuid The organization UUID
+ * @param array $objectData The organization object data (for nested contact persons)
+ *
+ * @return array Array of usernames
+ */
+ private function collectContactPersonUsernames(string $organizationUuid, array $objectData=[]): array
{
$usernames = [];
-
- // Focus on nested contact persons in the organization object data
- // These are available immediately when the organization is created
+
+ // Focus on nested contact persons in the organization object data.
+ // These are available immediately when the organization is created.
$nestedContactPersons = $objectData['contactpersonen'] ?? [];
- $this->_logger->info('SoftwareCatalogueService: Processing nested contact persons', [
- 'organizationUuid' => $organizationUuid,
- 'nestedContactPersonCount' => count($nestedContactPersons)
- ]);
-
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Processing nested contact persons',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'nestedContactPersonCount' => count($nestedContactPersons),
+ ]
+ );
+
foreach ($nestedContactPersons as $contactPerson) {
- if (is_array($contactPerson) && isset($contactPerson['email'])) {
+ if (is_array($contactPerson) === true && isset($contactPerson['email']) === true) {
$usernames[] = $contactPerson['email'];
- $this->_logger->info('SoftwareCatalogueService: Added nested contact person username', [
- 'username' => $contactPerson['email'],
- 'contactPersonData' => $contactPerson
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Added nested contact person username',
+ [
+ 'username' => $contactPerson['email'],
+ 'contactPersonData' => $contactPerson,
+ ]
+ );
}
}
-
- // Also try to find existing contact persons by their organisatie field
- // This is useful for updates or when contact persons were created separately
- $objectService = $this->_getObjectService();
- if ($objectService) {
- $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
+
+ // Also try to find existing contact persons by their organisatie field.
+ // This is useful for updates or when contact persons were created separately.
+ $objectService = $this->getObjectService();
+ if (empty($objectService) === false) {
+ $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
$voorzieningenConfig = $settingsService->getVoorzieningenConfig();
- $contactSchemaId = $voorzieningenConfig['contactpersoon_schema'] ?? null;
-
- if (!$contactSchemaId) {
+ $contactSchemaId = $voorzieningenConfig['contactpersoon_schema'] ?? null;
+
+ if ($contactSchemaId === null) {
$this->_logger->warning('SoftwareCatalogueService: Missing contactpersoon schema configuration for username extraction');
return $usernames;
}
-
+
try {
- // Try multiple approaches to find contact persons
+ // Try multiple approaches to find contact persons.
$contactPersons = [];
-
- // Approach 1: Find by organisatie field
+
+ // Approach 1: Find by organisatie field.
try {
- $contactPersons = $objectService->findAll([
- 'filters' => [
- 'register' => $objectData['register'] ?? '6',
- 'schema' => $contactSchemaId,
- 'organisatie' => $organizationUuid
- ]
- ]);
+ $contactPersons = $objectService->findAll(
+ [
+ 'filters' => [
+ 'register' => $objectData['register'] ?? '6',
+ 'schema' => $contactSchemaId,
+ 'organisatie' => $organizationUuid,
+ ],
+ ]
+ );
} catch (\Exception $e) {
- $this->_logger->info('SoftwareCatalogueService: Approach 1 failed, trying approach 2', [
- 'organizationUuid' => $organizationUuid,
- 'error' => $e->getMessage()
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Approach 1 failed, trying approach 2',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'error' => $e->getMessage(),
+ ]
+ );
}
-
- // Approach 2: If approach 1 fails, try to find all contact persons and filter by organisatie
- if (empty($contactPersons)) {
+
+ // Approach 2: If approach 1 fails, try to find all contact persons and filter by organisatie.
+ if (empty($contactPersons) === true) {
try {
- $allContactPersons = $objectService->findAll([
- 'filters' => [
- 'register' => $objectData['register'] ?? '6',
- 'schema' => $contactSchemaId
- ]
- ]);
-
+ $allContactPersons = $objectService->findAll(
+ [
+ 'filters' => [
+ 'register' => $objectData['register'] ?? '6',
+ 'schema' => $contactSchemaId,
+ ],
+ ]
+ );
+
foreach ($allContactPersons as $contactPerson) {
- $contactData = $contactPerson->getObject();
+ $contactData = $contactPerson->getObject();
$contactOrganisatie = $contactData['organisatie'] ?? null;
if ($contactOrganisatie === $organizationUuid) {
$contactPersons[] = $contactPerson;
}
}
} catch (\Exception $e) {
- $this->_logger->info('SoftwareCatalogueService: Approach 2 also failed', [
- 'organizationUuid' => $organizationUuid,
- 'error' => $e->getMessage()
- ]);
- }
- }
-
- $this->_logger->info('SoftwareCatalogueService: Found existing contact persons for organization', [
- 'organizationUuid' => $organizationUuid,
- 'contactPersonCount' => count($contactPersons),
- 'contactPersonIds' => array_map(function($cp) { return $cp->getId(); }, $contactPersons)
- ]);
-
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Approach 2 also failed',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end if
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Found existing contact persons for organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'contactPersonCount' => count($contactPersons),
+ 'contactPersonIds' => array_map(
+ function ($cp) {
+ return $cp->getId();
+ },
+ $contactPersons
+ ),
+ ]
+ );
+
foreach ($contactPersons as $contactPerson) {
$contactData = $contactPerson->getObject();
- $email = $contactData['email'] ?? null;
- if ($email) {
+ $email = $contactData['email'] ?? null;
+ if (empty($email) === false) {
$usernames[] = $email;
- $this->_logger->info('SoftwareCatalogueService: Added existing contact person username', [
- 'username' => $email,
- 'contactPersonId' => $contactPerson->getId()
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Added existing contact person username',
+ [
+ 'username' => $email,
+ 'contactPersonId' => $contactPerson->getId(),
+ ]
+ );
}
}
-
} catch (\Exception $e) {
- $this->_logger->error('SoftwareCatalogueService: Error collecting existing contact person usernames', [
- 'organizationUuid' => $organizationUuid,
- 'error' => $e->getMessage()
- ]);
- }
- }
-
- // Remove duplicates and return
+ $this->_logger->error(
+ 'SoftwareCatalogueService: Error collecting existing contact person usernames',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end if
+
+ // Remove duplicates and return.
$uniqueUsernames = array_unique($usernames);
- $this->_logger->info('SoftwareCatalogueService: Collected contact person usernames', [
- 'organizationUuid' => $organizationUuid,
- 'totalUsernames' => count($uniqueUsernames),
- 'usernames' => $uniqueUsernames
- ]);
-
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Collected contact person usernames',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'totalUsernames' => count($uniqueUsernames),
+ 'usernames' => $uniqueUsernames,
+ ]
+ );
+
return $uniqueUsernames;
- }
+ }//end collectContactPersonUsernames()
/**
* Maps organization data from SoftwareCatalog format to OpenRegister format
- *
+ *
* @param array $objectData The organization data from SoftwareCatalog
- *
+ *
* @return array The mapped data for OpenRegister
*/
private function mapOrganizationDataForOpenRegister(array $objectData): array
{
$mappedData = [
- 'naam' => $objectData['naam'] ?? $objectData['name'] ?? '',
- 'type' => $objectData['type'] ?? '',
- 'website' => $objectData['website'] ?? '',
- 'active' => false, // Default to inactive for new organizations
+ 'naam' => $objectData['naam'] ?? $objectData['name'] ?? '',
+ 'type' => $objectData['type'] ?? '',
+ 'website' => $objectData['website'] ?? '',
+ 'active' => false,
+ // Default to inactive for new organizations.
'contactpersonen' => [],
- 'deelnemers' => []
+ 'deelnemers' => [],
];
- // Map status from SoftwareCatalog to OpenRegister
+ // Map status from SoftwareCatalog to OpenRegister.
$beoordeling = strtolower($objectData['beoordeling'] ?? '');
if ($beoordeling === 'actief') {
$mappedData['active'] = true;
- } elseif ($beoordeling === 'inactief' || $beoordeling === 'deactief') {
+ } else if ($beoordeling === 'inactief' || $beoordeling === 'deactief') {
$mappedData['active'] = false;
}
- // Map other fields if they exist
- if (isset($objectData['adres'])) {
+ // Map other fields if they exist.
+ if (isset($objectData['adres']) === true) {
$mappedData['adres'] = $objectData['adres'];
}
- if (isset($objectData['postcode'])) {
+
+ if (isset($objectData['postcode']) === true) {
$mappedData['postcode'] = $objectData['postcode'];
}
- if (isset($objectData['plaats'])) {
+
+ if (isset($objectData['plaats']) === true) {
$mappedData['plaats'] = $objectData['plaats'];
}
- if (isset($objectData['telefoon'])) {
+
+ if (isset($objectData['telefoon']) === true) {
$mappedData['telefoon'] = $objectData['telefoon'];
}
- if (isset($objectData['email'])) {
+
+ if (isset($objectData['email']) === true) {
$mappedData['email'] = $objectData['email'];
}
return $mappedData;
- }
+ }//end mapOrganizationDataForOpenRegister()
/**
* Activates all users in an organization when the organization becomes active
*
* @param string $organizationUuid The organization UUID
- *
+ *
* @return void
*/
private function activateUsersForOrganization(string $organizationUuid): void
{
try {
- $this->_logger->info('SoftwareCatalogueService: Activating users for organization', [
- 'organizationUuid' => $organizationUuid
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Activating users for organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
- $objectService = $this->_getObjectService();
- if (!$objectService) {
+ $objectService = $this->getObjectService();
+ if ($objectService === null) {
$this->_logger->error('SoftwareCatalogueService: OpenRegister ObjectService not available');
return;
}
- // Get all contactpersonen for this organization
+ // Get all contactpersonen for this organization.
$settingsService = $this->_container->get(SettingsService::class);
- $registerId = $settingsService->getVoorzieningenRegisterId();
+ $registerId = $settingsService->getVoorzieningenRegisterId();
$contactpersoonSchemaId = $settingsService->getSchemaIdForObjectType('contactpersoon');
- if (!$registerId || !$contactpersoonSchemaId) {
+ if ($registerId === null || $contactpersoonSchemaId === false) {
$this->_logger->error('SoftwareCatalogueService: Register or schema not configured for contactpersonen');
return;
}
@@ -1837,73 +2024,81 @@ private function activateUsersForOrganization(string $organizationUuid): void
$contactpersoonSchemaId
);
- $userManager = $this->_container->get(\OCP\IUserManager::class);
+ $userManager = $this->_container->get(\OCP\IUserManager::class);
$activatedCount = 0;
foreach ($contactpersonen as $contactpersoon) {
$contactData = $contactpersoon->getObject();
- $username = $contactData['username'] ?? '';
+ $username = $contactData['username'] ?? '';
- if (!empty($username)) {
+ if (empty($username) === false) {
$user = $userManager->get($username);
- if ($user && !$user->isEnabled()) {
+ if ($user !== false && $user->isEnabled() === false) {
$user->setEnabled(true);
$activatedCount++;
- $this->_logger->info('SoftwareCatalogueService: Activated user for organization', [
- 'username' => $username,
- 'organizationUuid' => $organizationUuid
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Activated user for organization',
+ [
+ 'username' => $username,
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
}
}
}
- $this->_logger->info('SoftwareCatalogueService: Completed user activation for organization', [
- 'organizationUuid' => $organizationUuid,
- 'totalContactpersonen' => count($contactpersonen),
- 'activatedUsers' => $activatedCount
- ]);
-
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Completed user activation for organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'totalContactpersonen' => count($contactpersonen),
+ 'activatedUsers' => $activatedCount,
+ ]
+ );
} catch (\Exception $e) {
$this->_logger->error(
- 'SoftwareCatalogueService: Failed to activate users for organization: ' . $e->getMessage(),
+ 'SoftwareCatalogueService: Failed to activate users for organization: '.$e->getMessage(),
[
'organizationUuid' => $organizationUuid,
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
]
);
- }
- }
+ }//end try
+ }//end activateUsersForOrganization()
/**
* Deactivates all users in an organization when the organization becomes inactive
*
* @param string $organizationUuid The organization UUID
- *
+ *
* @return void
*/
private function deactivateUsersForOrganization(string $organizationUuid): void
{
try {
- $this->_logger->info('SoftwareCatalogueService: Deactivating users for organization', [
- 'organizationUuid' => $organizationUuid
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Deactivating users for organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
- $objectService = $this->_getObjectService();
- if (!$objectService) {
+ $objectService = $this->getObjectService();
+ if ($objectService === null) {
$this->_logger->error('SoftwareCatalogueService: OpenRegister ObjectService not available');
return;
}
- // Get all contactpersonen for this organization
+ // Get all contactpersonen for this organization.
$settingsService = $this->_container->get(SettingsService::class);
- $registerId = $settingsService->getVoorzieningenRegisterId();
+ $registerId = $settingsService->getVoorzieningenRegisterId();
$contactpersoonSchemaId = $settingsService->getSchemaIdForObjectType('contactpersoon');
- if (!$registerId || !$contactpersoonSchemaId) {
+ if ($registerId === null || $contactpersoonSchemaId === false) {
$this->_logger->error('SoftwareCatalogueService: Register or schema not configured for contactpersonen');
return;
}
@@ -1914,283 +2109,355 @@ private function deactivateUsersForOrganization(string $organizationUuid): void
$contactpersoonSchemaId
);
- $userManager = $this->_container->get(\OCP\IUserManager::class);
+ $userManager = $this->_container->get(\OCP\IUserManager::class);
$deactivatedCount = 0;
foreach ($contactpersonen as $contactpersoon) {
$contactData = $contactpersoon->getObject();
- $username = $contactData['username'] ?? '';
+ $username = $contactData['username'] ?? '';
- if (!empty($username)) {
+ if (empty($username) === false) {
$user = $userManager->get($username);
- if ($user && $user->isEnabled()) {
+ if ($user !== false && $user->isEnabled() === true) {
$user->setEnabled(false);
$deactivatedCount++;
- $this->_logger->info('SoftwareCatalogueService: Deactivated user for organization', [
- 'username' => $username,
- 'organizationUuid' => $organizationUuid
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Deactivated user for organization',
+ [
+ 'username' => $username,
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
}
}
}
- $this->_logger->info('SoftwareCatalogueService: Completed user deactivation for organization', [
- 'organizationUuid' => $organizationUuid,
- 'totalContactpersonen' => count($contactpersonen),
- 'deactivatedUsers' => $deactivatedCount
- ]);
-
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Completed user deactivation for organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'totalContactpersonen' => count($contactpersonen),
+ 'deactivatedUsers' => $deactivatedCount,
+ ]
+ );
} catch (\Exception $e) {
$this->_logger->error(
- 'SoftwareCatalogueService: Failed to deactivate users for organization: ' . $e->getMessage(),
+ 'SoftwareCatalogueService: Failed to deactivate users for organization: '.$e->getMessage(),
[
'organizationUuid' => $organizationUuid,
- 'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
+ 'exception' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
]
);
- }
- }
+ }//end try
+ }//end deactivateUsersForOrganization()
/**
* Activates SoftwareCatalog-specific users for an organization
* Only affects users from contactpersoon objects, not admin group users
*
* @param string $organizationUuid The organization UUID
- *
+ *
* @return void
*/
private function activateSoftwareCatalogUsersForOrganization(string $organizationUuid): void
{
try {
- $this->_logger->info('SoftwareCatalogueService: Activating SoftwareCatalog users for organization', [
- 'organizationUuid' => $organizationUuid
- ]);
-
- // Get SoftwareCatalog-specific users (from contactpersonen)
- $softwareCatalogUsers = $this->getSoftwareCatalogUsersForOrganization($organizationUuid);
-
- if (empty($softwareCatalogUsers)) {
- $this->_logger->info('SoftwareCatalogueService: No SoftwareCatalog users found for organization', [
- 'organizationUuid' => $organizationUuid
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Activating SoftwareCatalog users for organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
+
+ // Get SoftwareCatalog-specific users (from contactpersonen).
+ $softwareCatalogUsers = $this->getSoftwareCatalogUsersForOrganization(organizationUuid: $organizationUuid);
+
+ if (empty($softwareCatalogUsers) === true) {
+ $this->_logger->info(
+ 'SoftwareCatalogueService: No SoftwareCatalog users found for organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
return;
}
- $this->_logger->info('SoftwareCatalogueService: Found SoftwareCatalog users to activate', [
- 'organizationUuid' => $organizationUuid,
- 'userCount' => count($softwareCatalogUsers),
- 'users' => $softwareCatalogUsers
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Found SoftwareCatalog users to activate',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'userCount' => count($softwareCatalogUsers),
+ 'users' => $softwareCatalogUsers,
+ ]
+ );
- // Get the user manager
- $userManager = \OC::$server->getUserManager();
+ // Get the user manager.
+ $userManager = \OC::$server->getUserManager();
$activatedUsers = [];
- $failedUsers = [];
+ $failedUsers = [];
foreach ($softwareCatalogUsers as $username) {
try {
$user = $userManager->get($username);
- if ($user && !$user->isEnabled()) {
+ if ($user !== false && $user->isEnabled() === false) {
$user->setEnabled(true);
$activatedUsers[] = $username;
- $this->_logger->debug('SoftwareCatalogueService: Activated SoftwareCatalog user', [
- 'organizationUuid' => $organizationUuid,
- 'username' => $username
- ]);
- } elseif ($user && $user->isEnabled()) {
- $this->_logger->debug('SoftwareCatalogueService: SoftwareCatalog user already active', [
- 'organizationUuid' => $organizationUuid,
- 'username' => $username
- ]);
+ $this->_logger->debug(
+ 'SoftwareCatalogueService: Activated SoftwareCatalog user',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'username' => $username,
+ ]
+ );
+ } else if ($user !== false && $user->isEnabled() === true) {
+ $this->_logger->debug(
+ 'SoftwareCatalogueService: SoftwareCatalog user already active',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'username' => $username,
+ ]
+ );
} else {
$failedUsers[] = $username;
- $this->_logger->warning('SoftwareCatalogueService: SoftwareCatalog user not found', [
- 'organizationUuid' => $organizationUuid,
- 'username' => $username
- ]);
- }
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: SoftwareCatalog user not found',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'username' => $username,
+ ]
+ );
+ }//end if
} catch (\Exception $e) {
$failedUsers[] = $username;
- $this->_logger->error('SoftwareCatalogueService: Failed to activate SoftwareCatalog user', [
- 'organizationUuid' => $organizationUuid,
- 'username' => $username,
- 'error' => $e->getMessage()
- ]);
- }
- }
-
- $this->_logger->info('SoftwareCatalogueService: SoftwareCatalog user activation complete', [
- 'organizationUuid' => $organizationUuid,
- 'activatedUsers' => $activatedUsers,
- 'failedUsers' => $failedUsers,
- 'totalProcessed' => count($softwareCatalogUsers)
- ]);
+ $this->_logger->error(
+ 'SoftwareCatalogueService: Failed to activate SoftwareCatalog user',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'username' => $username,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end foreach
+ $this->_logger->info(
+ 'SoftwareCatalogueService: SoftwareCatalog user activation complete',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'activatedUsers' => $activatedUsers,
+ 'failedUsers' => $failedUsers,
+ 'totalProcessed' => count($softwareCatalogUsers),
+ ]
+ );
} catch (\Exception $e) {
- $this->_logger->error('SoftwareCatalogueService: Error activating SoftwareCatalog users for organization', [
- 'organizationUuid' => $organizationUuid,
- 'error' => $e->getMessage()
- ]);
- }
- }
+ $this->_logger->error(
+ 'SoftwareCatalogueService: Error activating SoftwareCatalog users for organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end activateSoftwareCatalogUsersForOrganization()
/**
* Deactivates SoftwareCatalog-specific users for an organization
* Only affects users from contactpersoon objects, not admin group users
*
* @param string $organizationUuid The organization UUID
- *
+ *
* @return void
*/
private function deactivateSoftwareCatalogUsersForOrganization(string $organizationUuid): void
{
try {
- $this->_logger->info('SoftwareCatalogueService: Deactivating SoftwareCatalog users for organization', [
- 'organizationUuid' => $organizationUuid
- ]);
-
- // Get SoftwareCatalog-specific users (from contactpersonen)
- $softwareCatalogUsers = $this->getSoftwareCatalogUsersForOrganization($organizationUuid);
-
- if (empty($softwareCatalogUsers)) {
- $this->_logger->info('SoftwareCatalogueService: No SoftwareCatalog users found for organization', [
- 'organizationUuid' => $organizationUuid
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Deactivating SoftwareCatalog users for organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
+
+ // Get SoftwareCatalog-specific users (from contactpersonen).
+ $softwareCatalogUsers = $this->getSoftwareCatalogUsersForOrganization(organizationUuid: $organizationUuid);
+
+ if (empty($softwareCatalogUsers) === true) {
+ $this->_logger->info(
+ 'SoftwareCatalogueService: No SoftwareCatalog users found for organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
return;
}
- $this->_logger->info('SoftwareCatalogueService: Found SoftwareCatalog users to deactivate', [
- 'organizationUuid' => $organizationUuid,
- 'userCount' => count($softwareCatalogUsers),
- 'users' => $softwareCatalogUsers
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Found SoftwareCatalog users to deactivate',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'userCount' => count($softwareCatalogUsers),
+ 'users' => $softwareCatalogUsers,
+ ]
+ );
- // Get the user manager
- $userManager = \OC::$server->getUserManager();
+ // Get the user manager.
+ $userManager = \OC::$server->getUserManager();
$deactivatedUsers = [];
- $failedUsers = [];
+ $failedUsers = [];
foreach ($softwareCatalogUsers as $username) {
try {
$user = $userManager->get($username);
- if ($user && $user->isEnabled()) {
+ if ($user !== false && $user->isEnabled() === true) {
$user->setEnabled(false);
$deactivatedUsers[] = $username;
- $this->_logger->debug('SoftwareCatalogueService: Deactivated SoftwareCatalog user', [
- 'organizationUuid' => $organizationUuid,
- 'username' => $username
- ]);
- } elseif ($user && !$user->isEnabled()) {
- $this->_logger->debug('SoftwareCatalogueService: SoftwareCatalog user already inactive', [
- 'organizationUuid' => $organizationUuid,
- 'username' => $username
- ]);
+ $this->_logger->debug(
+ 'SoftwareCatalogueService: Deactivated SoftwareCatalog user',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'username' => $username,
+ ]
+ );
+ } else if ($user !== false && $user->isEnabled() === false) {
+ $this->_logger->debug(
+ 'SoftwareCatalogueService: SoftwareCatalog user already inactive',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'username' => $username,
+ ]
+ );
} else {
$failedUsers[] = $username;
- $this->_logger->warning('SoftwareCatalogueService: SoftwareCatalog user not found', [
- 'organizationUuid' => $organizationUuid,
- 'username' => $username
- ]);
- }
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: SoftwareCatalog user not found',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'username' => $username,
+ ]
+ );
+ }//end if
} catch (\Exception $e) {
$failedUsers[] = $username;
- $this->_logger->error('SoftwareCatalogueService: Failed to deactivate SoftwareCatalog user', [
- 'organizationUuid' => $organizationUuid,
- 'username' => $username,
- 'error' => $e->getMessage()
- ]);
- }
- }
-
- $this->_logger->info('SoftwareCatalogueService: SoftwareCatalog user deactivation complete', [
- 'organizationUuid' => $organizationUuid,
- 'deactivatedUsers' => $deactivatedUsers,
- 'failedUsers' => $failedUsers,
- 'totalProcessed' => count($softwareCatalogUsers)
- ]);
+ $this->_logger->error(
+ 'SoftwareCatalogueService: Failed to deactivate SoftwareCatalog user',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'username' => $username,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end foreach
+ $this->_logger->info(
+ 'SoftwareCatalogueService: SoftwareCatalog user deactivation complete',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'deactivatedUsers' => $deactivatedUsers,
+ 'failedUsers' => $failedUsers,
+ 'totalProcessed' => count($softwareCatalogUsers),
+ ]
+ );
} catch (\Exception $e) {
- $this->_logger->error('SoftwareCatalogueService: Error deactivating SoftwareCatalog users for organization', [
- 'organizationUuid' => $organizationUuid,
- 'error' => $e->getMessage()
- ]);
- }
- }
+ $this->_logger->error(
+ 'SoftwareCatalogueService: Error deactivating SoftwareCatalog users for organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end deactivateSoftwareCatalogUsersForOrganization()
/**
* Gets SoftwareCatalog-specific users for an organization
* These are users from contactpersoon objects, excluding admin group users
*
* @param string $organizationUuid The organization UUID
- *
+ *
* @return array Array of usernames
*/
private function getSoftwareCatalogUsersForOrganization(string $organizationUuid): array
{
try {
- $this->_logger->debug('SoftwareCatalogueService: Getting SoftwareCatalog users for organization', [
- 'organizationUuid' => $organizationUuid
- ]);
+ $this->_logger->debug(
+ 'SoftwareCatalogueService: Getting SoftwareCatalog users for organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
- // Get the object service to find contactpersonen
- $objectService = $this->_getObjectService();
- if (!$objectService) {
+ // Get the object service to find contactpersonen.
+ $objectService = $this->getObjectService();
+ if ($objectService === null) {
$this->_logger->error('SoftwareCatalogueService: ObjectService not available for getting SoftwareCatalog users');
return [];
}
- // Find all contactpersonen for this organization
- $contactpersonen = $objectService->findAll([
- 'filters' => [
- 'register' => 6, // Voorzieningen register
- 'schema' => 38 // Contactpersoon schema
- ]
- ]);
+ // Find all contactpersonen for this organization.
+ $contactpersonen = $objectService->findAll(
+ [
+ 'filters' => [
+ 'register' => 6,
+ // Voorzieningen register.
+ 'schema' => 38,
+ // Contactpersoon schema.
+ ],
+ ]
+ );
$softwareCatalogUsers = [];
- $adminGroupUsers = $this->getAdminGroupUsernames();
+ $adminGroupUsers = $this->getAdminGroupUsernames();
foreach ($contactpersonen as $contactpersoonObject) {
- $contactData = $contactpersoonObject->getObject();
+ $contactData = $contactpersoonObject->getObject();
$contactOrganisatie = $contactData['organisatie'] ?? null;
-
- // Check if this contactpersoon belongs to our organization
+
+ // Check if this contactpersoon belongs to our organization.
if ($contactOrganisatie === $organizationUuid) {
- // Extract username from contactpersoon object data
+ // Extract username from contactpersoon object data.
$contactData = $contactpersoonObject->getObject();
- $username = $contactData['username'] ?? null;
-
- if ($username && !in_array($username, $adminGroupUsers)) {
+ $username = $contactData['username'] ?? null;
+
+ if ($username !== false && in_array($username, $adminGroupUsers) === false) {
$softwareCatalogUsers[] = $username;
- $this->_logger->debug('SoftwareCatalogueService: Found SoftwareCatalog user', [
- 'organizationUuid' => $organizationUuid,
- 'username' => $username,
- 'contactpersoonId' => $contactpersoonObject->getId()
- ]);
+ $this->_logger->debug(
+ 'SoftwareCatalogueService: Found SoftwareCatalog user',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'username' => $username,
+ 'contactpersoonId' => $contactpersoonObject->getId(),
+ ]
+ );
}
}
- }
+ }//end foreach
- $this->_logger->info('SoftwareCatalogueService: Found SoftwareCatalog users for organization', [
- 'organizationUuid' => $organizationUuid,
- 'userCount' => count($softwareCatalogUsers),
- 'users' => $softwareCatalogUsers
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Found SoftwareCatalog users for organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'userCount' => count($softwareCatalogUsers),
+ 'users' => $softwareCatalogUsers,
+ ]
+ );
return $softwareCatalogUsers;
-
} catch (\Exception $e) {
- $this->_logger->error('SoftwareCatalogueService: Error getting SoftwareCatalog users for organization', [
- 'organizationUuid' => $organizationUuid,
- 'error' => $e->getMessage()
- ]);
+ $this->_logger->error(
+ 'SoftwareCatalogueService: Error getting SoftwareCatalog users for organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'error' => $e->getMessage(),
+ ]
+ );
return [];
- }
- }
+ }//end try
+ }//end getSoftwareCatalogUsersForOrganization()
/**
* Gets all usernames from the admin group
@@ -2201,284 +2468,334 @@ private function getAdminGroupUsernames(): array
{
try {
$groupManager = \OC::$server->getGroupManager();
- $adminGroup = $groupManager->get('admin');
-
- if (!$adminGroup) {
+ $adminGroup = $groupManager->get('admin');
+
+ if ($adminGroup === null) {
$this->_logger->warning('SoftwareCatalogueService: Admin group not found');
return [];
}
- $adminUsers = $adminGroup->getUsers();
+ $adminUsers = $adminGroup->getUsers();
$adminUsernames = [];
-
+
foreach ($adminUsers as $adminUser) {
$adminUsernames[] = $adminUser->getUID();
}
- $this->_logger->debug('SoftwareCatalogueService: Found admin group users', [
- 'adminUserCount' => count($adminUsernames),
- 'adminUsers' => $adminUsernames
- ]);
+ $this->_logger->debug(
+ 'SoftwareCatalogueService: Found admin group users',
+ [
+ 'adminUserCount' => count($adminUsernames),
+ 'adminUsers' => $adminUsernames,
+ ]
+ );
return $adminUsernames;
-
} catch (\Exception $e) {
- $this->_logger->error('SoftwareCatalogueService: Error getting admin group usernames', [
- 'error' => $e->getMessage()
- ]);
+ $this->_logger->error(
+ 'SoftwareCatalogueService: Error getting admin group usernames',
+ [
+ 'error' => $e->getMessage(),
+ ]
+ );
return [];
- }
- }
+ }//end try
+ }//end getAdminGroupUsernames()
/**
* Adds all users from the admin group to the organization entity
*
* @param string $organizationUuid The organization UUID
- *
+ *
* @return void
*/
private function addAdminGroupUsersToOrganization(string $organizationUuid): void
{
try {
- $this->_logger->info('SoftwareCatalogueService: Adding admin group users to organization entity', [
- 'organizationUuid' => $organizationUuid
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Adding admin group users to organization entity',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
- // Get the group manager to access admin group users
+ // Get the group manager to access admin group users.
$groupManager = \OC::$server->getGroupManager();
- $adminGroup = $groupManager->get('admin');
-
- if (!$adminGroup) {
+ $adminGroup = $groupManager->get('admin');
+
+ if ($adminGroup === null) {
$this->_logger->warning('SoftwareCatalogueService: Admin group not found');
return;
}
$adminUsers = $adminGroup->getUsers();
- $this->_logger->info('SoftwareCatalogueService: Found admin group users', [
- 'organizationUuid' => $organizationUuid,
- 'adminUserCount' => count($adminUsers)
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Found admin group users',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'adminUserCount' => count($adminUsers),
+ ]
+ );
- // Get the organization entity (not object) to update its users list
+ // Get the organization entity (not object) to update its users list.
$organisationMapper = $this->_container->get('OCA\\OpenRegister\\Db\\OrganisationMapper');
- if (!$organisationMapper) {
+ if ($organisationMapper === null) {
$this->_logger->error('SoftwareCatalogueService: OrganisationMapper not available for adding admin users');
return;
}
- // Find the organization entity by UUID
- $this->_logger->info('SoftwareCatalogueService: Searching for organization entity', [
- 'organizationUuid' => $organizationUuid
- ]);
-
+ // Find the organization entity by UUID.
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Searching for organization entity',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
+
try {
$targetOrganisation = $organisationMapper->findByUuid($organizationUuid);
-
- $this->_logger->info('SoftwareCatalogueService: Found target organization entity', [
- 'organizationUuid' => $organizationUuid,
- 'entityId' => $targetOrganisation->getId()
- ]);
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Found target organization entity',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'entityId' => $targetOrganisation->getId(),
+ ]
+ );
} catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
- $this->_logger->warning('SoftwareCatalogueService: Organization entity not found for adding admin users', [
- 'organizationUuid' => $organizationUuid
- ]);
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: Organization entity not found for adding admin users',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
return;
}
- // Get current users list from the entity
+ // Get current users list from the entity.
$currentUsers = $targetOrganisation->getUsers() ?? [];
-
- $this->_logger->info('SoftwareCatalogueService: Current organization entity users', [
- 'organizationUuid' => $organizationUuid,
- 'currentUsers' => $currentUsers,
- 'currentUserCount' => count($currentUsers)
- ]);
-
- // Add admin users to the list
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Current organization entity users',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'currentUsers' => $currentUsers,
+ 'currentUserCount' => count($currentUsers),
+ ]
+ );
+
+ // Add admin users to the list.
$updatedUsers = $currentUsers;
- $addedUsers = [];
+ $addedUsers = [];
foreach ($adminUsers as $adminUser) {
$adminUsername = $adminUser->getUID();
- if (!in_array($adminUsername, $updatedUsers)) {
+ if (in_array($adminUsername, $updatedUsers) === false) {
$updatedUsers[] = $adminUsername;
- $addedUsers[] = $adminUsername;
- $this->_logger->debug('SoftwareCatalogueService: Added admin user to organization entity', [
- 'organizationUuid' => $organizationUuid,
- 'adminUsername' => $adminUsername
- ]);
+ $addedUsers[] = $adminUsername;
+ $this->_logger->debug(
+ 'SoftwareCatalogueService: Added admin user to organization entity',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'adminUsername' => $adminUsername,
+ ]
+ );
}
}
-
- $this->_logger->info('SoftwareCatalogueService: Admin users processing complete', [
- 'organizationUuid' => $organizationUuid,
- 'addedUsers' => $addedUsers,
- 'totalUsersAfterUpdate' => count($updatedUsers)
- ]);
-
- // Update the organization entity with the new users list
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Admin users processing complete',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'addedUsers' => $addedUsers,
+ 'totalUsersAfterUpdate' => count($updatedUsers),
+ ]
+ );
+
+ // Update the organization entity with the new users list.
if (count($updatedUsers) > count($currentUsers)) {
- $this->_logger->info('SoftwareCatalogueService: Updating organization entity with new users', [
- 'organizationUuid' => $organizationUuid,
- 'entityId' => $targetOrganisation->getId(),
- 'usersToAdd' => count($updatedUsers) - count($currentUsers)
- ]);
-
- // Set the updated users list on the entity
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Updating organization entity with new users',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'entityId' => $targetOrganisation->getId(),
+ 'usersToAdd' => count($updatedUsers) - count($currentUsers),
+ ]
+ );
+
+ // Set the updated users list on the entity.
$targetOrganisation->setUsers($updatedUsers);
-
- $this->_logger->info('SoftwareCatalogueService: Saving updated organization entity', [
- 'organizationUuid' => $organizationUuid,
- 'entityId' => $targetOrganisation->getId(),
- 'newUserCount' => count($updatedUsers)
- ]);
-
- // Save the updated organization entity
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Saving updated organization entity',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'entityId' => $targetOrganisation->getId(),
+ 'newUserCount' => count($updatedUsers),
+ ]
+ );
+
+ // Save the updated organization entity.
$savedOrganisation = $organisationMapper->save($targetOrganisation);
-
- $this->_logger->info('SoftwareCatalogueService: Successfully added admin users to organization entity', [
- 'organizationUuid' => $organizationUuid,
- 'addedUsers' => count($updatedUsers) - count($currentUsers),
- 'totalUsers' => count($updatedUsers)
- ]);
- } else {
- $this->_logger->info('SoftwareCatalogueService: All admin users already in organization entity', [
- 'organizationUuid' => $organizationUuid,
- 'totalUsers' => count($updatedUsers)
- ]);
- }
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Successfully added admin users to organization entity',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'addedUsers' => count($updatedUsers) - count($currentUsers),
+ 'totalUsers' => count($updatedUsers),
+ ]
+ );
+ } else {
+ $this->_logger->info(
+ 'SoftwareCatalogueService: All admin users already in organization entity',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'totalUsers' => count($updatedUsers),
+ ]
+ );
+ }//end if
} catch (\Exception $e) {
- $this->_logger->error('SoftwareCatalogueService: Failed to add admin users to organization entity: ' . $e->getMessage(), [
- 'organizationUuid' => $organizationUuid,
- 'exception' => $e
- ]);
- }
- }
+ $this->_logger->error(
+ 'SoftwareCatalogueService: Failed to add admin users to organization entity: '.$e->getMessage(),
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'exception' => $e,
+ ]
+ );
+ }//end try
+ }//end addAdminGroupUsersToOrganization()
/**
* Checks if a contactpersoon username is in the organization's users list
*
* @param object $contactpersoonObject The contactpersoon object
- *
+ *
* @return bool True if the user should be added to the organization
*/
public function shouldAddContactpersoonToOrganization(object $contactpersoonObject): bool
{
try {
- $objectData = $contactpersoonObject->getObject();
- $username = $objectData['username'] ?? '';
+ $objectData = $contactpersoonObject->getObject();
+ $username = $objectData['username'] ?? '';
$organizationUuid = $objectData['organisation'] ?? '';
- if (empty($username) || empty($organizationUuid)) {
+ if (empty($username) === true || empty($organizationUuid) === true) {
return false;
}
- $objectService = $this->_getObjectService();
- if (!$objectService) {
+ $objectService = $this->getObjectService();
+ if ($objectService === null) {
return false;
}
- // Get the organization object
- $settingsService = $this->_container->get(SettingsService::class);
- $registerId = $settingsService->getVoorzieningenRegisterId();
+ // Get the organization object.
+ $settingsService = $this->_container->get(SettingsService::class);
+ $registerId = $settingsService->getVoorzieningenRegisterId();
$organisatieSchemaId = $settingsService->getSchemaIdForObjectType('organisatie');
- if (!$registerId || !$organisatieSchemaId) {
+ if ($registerId === null || $organisatieSchemaId === false) {
return false;
}
try {
$organizationObject = $objectService->find($organizationUuid, [], false, $registerId, $organisatieSchemaId);
- $organizationData = $organizationObject->getObject();
-
- // Check if the username is already in the organization's users
+ $organizationData = $organizationObject->getObject();
+
+ // Check if the username is already in the organization's users.
$organizationUsers = $organizationData['users'] ?? [];
-
- if (is_array($organizationUsers) && !in_array($username, $organizationUsers)) {
- $this->_logger->info('SoftwareCatalogueService: Contactpersoon should be added to organization', [
- 'username' => $username,
- 'organizationUuid' => $organizationUuid,
- 'currentUsers' => $organizationUsers
- ]);
+
+ if (is_array($organizationUsers) === true && in_array($username, $organizationUsers) === false) {
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Contactpersoon should be added to organization',
+ [
+ 'username' => $username,
+ 'organizationUuid' => $organizationUuid,
+ 'currentUsers' => $organizationUsers,
+ ]
+ );
return true;
}
return false;
-
} catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
- // Organization doesn't exist, so we can't add the user
- $this->_logger->warning('SoftwareCatalogueService: Organization not found for contactpersoon', [
- 'username' => $username,
- 'organizationUuid' => $organizationUuid
- ]);
+ // Organization doesn't exist, so we can't add the user.
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: Organization not found for contactpersoon',
+ [
+ 'username' => $username,
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
return false;
- }
-
+ }//end try
} catch (\Exception $e) {
$this->_logger->error(
- 'SoftwareCatalogueService: Failed to check if contactpersoon should be added to organization: ' . $e->getMessage(),
+ 'SoftwareCatalogueService: Failed to check if contactpersoon should be added to organization: '.$e->getMessage(),
[
- 'objectId' => $contactpersoonObject->getId(),
- 'exception' => $e->getMessage()
+ 'objectId' => $contactpersoonObject->getId(),
+ 'exception' => $e->getMessage(),
]
);
return false;
- }
- }
+ }//end try
+ }//end shouldAddContactpersoonToOrganization()
/**
* Adds a contactpersoon username to the organization's users list
*
* @param object $contactpersoonObject The contactpersoon object
- *
+ *
* @return bool True if the user was successfully added
*/
public function addContactpersoonToOrganization(object $contactpersoonObject): bool
{
try {
- $objectData = $contactpersoonObject->getObject();
- $username = $objectData['username'] ?? '';
+ $objectData = $contactpersoonObject->getObject();
+ $username = $objectData['username'] ?? '';
$organizationUuid = $objectData['organisation'] ?? '';
- if (empty($username) || empty($organizationUuid)) {
- $this->_logger->warning('SoftwareCatalogueService: Cannot add contactpersoon to organization - missing username or organization', [
- 'username' => $username,
- 'organizationUuid' => $organizationUuid
- ]);
+ if (empty($username) === true || empty($organizationUuid) === true) {
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: Cannot add contactpersoon to organization - missing username or organization',
+ [
+ 'username' => $username,
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
return false;
}
- $objectService = $this->_getObjectService();
- if (!$objectService) {
+ $objectService = $this->getObjectService();
+ if ($objectService === null) {
$this->_logger->error('SoftwareCatalogueService: OpenRegister ObjectService not available');
return false;
}
- // Get the organization object
- $settingsService = $this->_container->get(SettingsService::class);
- $registerId = $settingsService->getVoorzieningenRegisterId();
+ // Get the organization object.
+ $settingsService = $this->_container->get(SettingsService::class);
+ $registerId = $settingsService->getVoorzieningenRegisterId();
$organisatieSchemaId = $settingsService->getSchemaIdForObjectType('organisatie');
- if (!$registerId || !$organisatieSchemaId) {
+ if ($registerId === null || $organisatieSchemaId === false) {
$this->_logger->error('SoftwareCatalogueService: Register or schema not configured for organisatie');
return false;
}
try {
$organizationObject = $objectService->find($organizationUuid, [], false, $registerId, $organisatieSchemaId);
- $organizationData = $organizationObject->getObject();
-
- // Add the username to the organization's users list
+ $organizationData = $organizationObject->getObject();
+
+ // Add the username to the organization's users list.
$organizationUsers = $organizationData['users'] ?? [];
- if (!is_array($organizationUsers)) {
+ if (is_array($organizationUsers) === false) {
$organizationUsers = [];
}
-
- if (!in_array($username, $organizationUsers)) {
- $organizationUsers[] = $username;
+
+ if (in_array($username, $organizationUsers) === false) {
+ $organizationUsers[] = $username;
$organizationData['users'] = $organizationUsers;
-
- // Update the organization object
+
+ // Update the organization object.
$updatedOrganization = $objectService->saveObject(
$organizationData,
[],
@@ -2487,162 +2804,190 @@ public function addContactpersoonToOrganization(object $contactpersoonObject): b
$organizationUuid
);
- $this->_logger->info('SoftwareCatalogueService: Successfully added contactpersoon to organization', [
- 'username' => $username,
- 'organizationUuid' => $organizationUuid,
- 'updatedUsers' => $organizationUsers
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Successfully added contactpersoon to organization',
+ [
+ 'username' => $username,
+ 'organizationUuid' => $organizationUuid,
+ 'updatedUsers' => $organizationUsers,
+ ]
+ );
return true;
} else {
- $this->_logger->debug('SoftwareCatalogueService: Contactpersoon already in organization', [
- 'username' => $username,
- 'organizationUuid' => $organizationUuid
- ]);
- return true; // Already there, consider it successful
- }
-
+ $this->_logger->debug(
+ 'SoftwareCatalogueService: Contactpersoon already in organization',
+ [
+ 'username' => $username,
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
+ return true;
+ // Already there, consider it successful.
+ }//end if
} catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
- $this->_logger->error('SoftwareCatalogueService: Organization not found for contactpersoon', [
- 'username' => $username,
- 'organizationUuid' => $organizationUuid
- ]);
+ $this->_logger->error(
+ 'SoftwareCatalogueService: Organization not found for contactpersoon',
+ [
+ 'username' => $username,
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
return false;
- }
-
+ }//end try
} catch (\Exception $e) {
$this->_logger->error(
- 'SoftwareCatalogueService: Failed to add contactpersoon to organization: ' . $e->getMessage(),
+ 'SoftwareCatalogueService: Failed to add contactpersoon to organization: '.$e->getMessage(),
[
- 'objectId' => $contactpersoonObject->getId(),
+ 'objectId' => $contactpersoonObject->getId(),
'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
]
);
return false;
- }
- }
+ }//end try
+ }//end addContactpersoonToOrganization()
/**
* Handles ownership assignment for anonymous user registrations
- *
+ *
* @param object $organizationObject The organization object
- *
+ *
* @return void
*/
private function handleOwnershipAssignment(object $organizationObject): void
{
try {
- $this->_logger->info('SoftwareCatalogueService: Handling ownership assignment for organization', [
- 'objectId' => $organizationObject->getId()
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Handling ownership assignment for organization',
+ [
+ 'objectId' => $organizationObject->getId(),
+ ]
+ );
- $objectData = $organizationObject->getObject();
+ $objectData = $organizationObject->getObject();
$organizationUuid = $objectData['id'] ?? $organizationObject->getId();
- $contactpersonen = $objectData['contactpersonen'] ?? [];
+ $contactpersonen = $objectData['contactpersonen'] ?? [];
- if (empty($contactpersonen)) {
- $this->_logger->info('SoftwareCatalogueService: No contact persons found for ownership assignment', [
- 'organizationUuid' => $organizationUuid
- ]);
+ if (empty($contactpersonen) === true) {
+ $this->_logger->info(
+ 'SoftwareCatalogueService: No contact persons found for ownership assignment',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
return;
}
- // Get the first contact person as the primary owner
+ // Get the first contact person as the primary owner.
$primaryContactUuid = $contactpersonen[0];
-
- $objectService = $this->_getObjectService();
- if (!$objectService) {
+
+ $objectService = $this->getObjectService();
+ if ($objectService === null) {
$this->_logger->error('SoftwareCatalogueService: OpenRegister ObjectService not available for ownership assignment');
return;
}
- // Get the primary contact person object
+ // Get the primary contact person object.
$settingsService = $this->_container->get(SettingsService::class);
- $registerId = $settingsService->getVoorzieningenRegisterId();
+ $registerId = $settingsService->getVoorzieningenRegisterId();
$contactpersoonSchemaId = $settingsService->getSchemaIdForObjectType('contactpersoon');
- $organisatieSchemaId = $settingsService->getSchemaIdForObjectType('organisatie');
+ $organisatieSchemaId = $settingsService->getSchemaIdForObjectType('organisatie');
- if (!$registerId || !$contactpersoonSchemaId || !$organisatieSchemaId) {
+ if ($registerId === null || $contactpersoonSchemaId === null || $organisatieSchemaId === false) {
$this->_logger->error('SoftwareCatalogueService: Register or schema not configured for contactpersoon or organisatie');
return;
}
- // Retry mechanism for user creation timing
+ // Retry mechanism for user creation timing.
$maxRetries = 3;
- $retryDelay = 1; // seconds
-
+ $retryDelay = 1;
+ // Seconds.
for ($retry = 0; $retry < $maxRetries; $retry++) {
try {
$primaryContactObject = $objectService->find($primaryContactUuid, [], false, $registerId, $contactpersoonSchemaId);
- $primaryContactData = $primaryContactObject->getObject();
- $primaryUsername = $primaryContactData['username'] ?? '';
+ $primaryContactData = $primaryContactObject->getObject();
+ $primaryUsername = $primaryContactData['username'] ?? '';
- if (empty($primaryUsername)) {
+ if (empty($primaryUsername) === true) {
if ($retry < $maxRetries - 1) {
- $this->_logger->info('SoftwareCatalogueService: Primary contact person has no username, retrying in ' . $retryDelay . ' seconds', [
- 'contactUuid' => $primaryContactUuid,
- 'organizationUuid' => $organizationUuid,
- 'retry' => $retry + 1,
- 'maxRetries' => $maxRetries
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Primary contact person has no username, retrying in '.$retryDelay.' seconds',
+ [
+ 'contactUuid' => $primaryContactUuid,
+ 'organizationUuid' => $organizationUuid,
+ 'retry' => $retry + 1,
+ 'maxRetries' => $maxRetries,
+ ]
+ );
sleep($retryDelay);
continue;
} else {
- $this->_logger->warning('SoftwareCatalogueService: Primary contact person still has no username after retries', [
- 'contactUuid' => $primaryContactUuid,
- 'organizationUuid' => $organizationUuid
- ]);
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: Primary contact person still has no username after retries',
+ [
+ 'contactUuid' => $primaryContactUuid,
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
return;
- }
- }
-
- // Get the organization entity UUID - use the same UUID as the organization object
- $organisationEntityUuid = $organizationUuid; // Organization entity should have same UUID as object
+ }//end if
+ }//end if
- // Add users to the organization entity
+ // Get the organization entity UUID - use the same UUID as the organization object.
+ $organisationEntityUuid = $organizationUuid;
+ // Organization entity should have same UUID as object.
+ // Add users to the organization entity.
$organisationMapper = $this->_container->get('OCA\\OpenRegister\\Db\\OrganisationMapper');
try {
$organisationEntity = $organisationMapper->findByUuid($organisationEntityUuid);
-
- // Add all contact person users to the organization entity
+
+ // Add all contact person users to the organization entity.
foreach ($contactpersonen as $contactUuid) {
try {
- $contactObject = $objectService->find($contactUuid, [], false, $registerId, $contactpersoonSchemaId);
- $contactData = $contactObject->getObject();
+ $contactObject = $objectService->find($contactUuid, [], false, $registerId, $contactpersoonSchemaId);
+ $contactData = $contactObject->getObject();
$contactUsername = $contactData['username'] ?? '';
-
- if (!empty($contactUsername)) {
+
+ if (empty($contactUsername) === false) {
$organisationEntity->addUser($contactUsername);
}
} catch (\Exception $e) {
- $this->_logger->warning('SoftwareCatalogueService: Failed to add contact person to organization entity', [
- 'contactUuid' => $contactUuid,
- 'error' => $e->getMessage()
- ]);
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: Failed to add contact person to organization entity',
+ [
+ 'contactUuid' => $contactUuid,
+ 'error' => $e->getMessage(),
+ ]
+ );
}
}
-
- // Save the updated organization entity
+
+ // Save the updated organization entity.
$organisationMapper->save($organisationEntity);
-
- $this->_logger->info('SoftwareCatalogueService: Successfully added users to organization entity', [
- 'organizationUuid' => $organisationEntityUuid,
- 'userCount' => count($organisationEntity->getUserIds())
- ]);
-
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Successfully added users to organization entity',
+ [
+ 'organizationUuid' => $organisationEntityUuid,
+ 'userCount' => count($organisationEntity->getUserIds()),
+ ]
+ );
} catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
- $this->_logger->error('SoftwareCatalogueService: Organization entity not found for adding users', [
- 'organizationUuid' => $organisationEntityUuid
- ]);
- }
+ $this->_logger->error(
+ 'SoftwareCatalogueService: Organization entity not found for adding users',
+ [
+ 'organizationUuid' => $organisationEntityUuid,
+ ]
+ );
+ }//end try
- // Update organization object ownership and organization reference
- $organizationData['owner'] = $primaryUsername;
+ // Update organization object ownership and organization reference.
+ $organizationData['owner'] = $primaryUsername;
$organizationData['organisation'] = $organisationEntityUuid;
-
+
$updatedOrganization = $objectService->saveObject(
$organizationData,
[],
@@ -2651,10 +2996,10 @@ private function handleOwnershipAssignment(object $organizationObject): void
$organizationUuid
);
- // Update primary contact person object ownership and organization reference
- $primaryContactData['owner'] = $primaryUsername;
+ // Update primary contact person object ownership and organization reference.
+ $primaryContactData['owner'] = $primaryUsername;
$primaryContactData['organisatie'] = $organisationEntityUuid;
-
+
$updatedPrimaryContact = $objectService->saveObject(
$primaryContactData,
[],
@@ -2663,18 +3008,18 @@ private function handleOwnershipAssignment(object $organizationObject): void
$primaryContactUuid
);
- // Update other contact persons with organization reference
+ // Update other contact persons with organization reference.
for ($i = 1; $i < count($contactpersonen); $i++) {
$contactUuid = $contactpersonen[$i];
try {
- $contactObject = $objectService->find($contactUuid, [], false, $registerId, $contactpersoonSchemaId);
- $contactData = $contactObject->getObject();
+ $contactObject = $objectService->find($contactUuid, [], false, $registerId, $contactpersoonSchemaId);
+ $contactData = $contactObject->getObject();
$contactUsername = $contactData['username'] ?? '';
- if (!empty($contactUsername)) {
- $contactData['owner'] = $contactUsername;
+ if (empty($contactUsername) === false) {
+ $contactData['owner'] = $contactUsername;
$contactData['organisatie'] = $organisationEntityUuid;
-
+
$objectService->saveObject(
$contactData,
[],
@@ -2684,383 +3029,469 @@ private function handleOwnershipAssignment(object $organizationObject): void
);
}
} catch (\Exception $e) {
- $this->_logger->warning('SoftwareCatalogueService: Failed to update contact person ownership', [
- 'contactUuid' => $contactUuid,
- 'error' => $e->getMessage()
- ]);
- }
- }
-
- $this->_logger->info('SoftwareCatalogueService: Successfully assigned ownership for organization', [
- 'organizationUuid' => $organizationUuid,
- 'primaryOwner' => $primaryUsername,
- 'organisationEntityUuid' => $organisationEntityUuid,
- 'contactPersonCount' => count($contactpersonen),
- 'retries' => $retry
- ]);
-
- return; // Success, exit retry loop
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: Failed to update contact person ownership',
+ [
+ 'contactUuid' => $contactUuid,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end for
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Successfully assigned ownership for organization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'primaryOwner' => $primaryUsername,
+ 'organisationEntityUuid' => $organisationEntityUuid,
+ 'contactPersonCount' => count($contactpersonen),
+ 'retries' => $retry,
+ ]
+ );
+ return;
+ // Success, exit retry loop.
} catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
if ($retry < $maxRetries - 1) {
- $this->_logger->info('SoftwareCatalogueService: Primary contact person not found, retrying in ' . $retryDelay . ' seconds', [
- 'contactUuid' => $primaryContactUuid,
- 'organizationUuid' => $organizationUuid,
- 'retry' => $retry + 1,
- 'maxRetries' => $maxRetries
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Primary contact person not found, retrying in '.$retryDelay.' seconds',
+ [
+ 'contactUuid' => $primaryContactUuid,
+ 'organizationUuid' => $organizationUuid,
+ 'retry' => $retry + 1,
+ 'maxRetries' => $maxRetries,
+ ]
+ );
sleep($retryDelay);
continue;
} else {
- $this->_logger->error('SoftwareCatalogueService: Primary contact person not found after retries', [
- 'contactUuid' => $primaryContactUuid,
- 'organizationUuid' => $organizationUuid
- ]);
+ $this->_logger->error(
+ 'SoftwareCatalogueService: Primary contact person not found after retries',
+ [
+ 'contactUuid' => $primaryContactUuid,
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
return;
- }
- }
- }
-
+ }//end if
+ }//end try
+ }//end for
} catch (\Exception $e) {
- $this->_logger->error('SoftwareCatalogueService: Error handling ownership assignment', [
- 'objectId' => $organizationObject->getId(),
- 'error' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine()
- ]);
- }
- }
+ $this->_logger->error(
+ 'SoftwareCatalogueService: Error handling ownership assignment',
+ [
+ 'objectId' => $organizationObject->getId(),
+ 'error' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]
+ );
+ }//end try
+ }//end handleOwnershipAssignment()
/**
* Synchronizes contact person usernames with the organization entity's users array
* This method finds all contact persons associated with a given organization UUID
* and ensures their emails are present in the organization entity's users array
- *
+ *
* @param string $organizationUuid The UUID of the organization
- *
+ *
* @return void
*/
public function syncContactPersonUsernamesWithOrganization(string $organizationUuid): void
{
- $this->_logger->info('SoftwareCatalogueService: Starting contact person username synchronization', [
- 'organizationUuid' => $organizationUuid
- ]);
-
- // Get the ObjectService to find contact persons
- $objectService = $this->_getObjectService();
- if (!$objectService) {
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Starting contact person username synchronization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
+
+ // Get the ObjectService to find contact persons.
+ $objectService = $this->getObjectService();
+ if ($objectService === null) {
$this->_logger->error('SoftwareCatalogueService: ObjectService not available for username synchronization');
return;
}
-
- // Get the contact person schema ID from configuration
- $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
+
+ // Get the contact person schema ID from configuration.
+ $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
$voorzieningenConfig = $settingsService->getVoorzieningenConfig();
- $contactSchemaId = $voorzieningenConfig['contactpersoon_schema'] ?? null;
- $registerId = $voorzieningenConfig['register'] ?? null;
-
- if (!$contactSchemaId || !$registerId) {
- $this->_logger->warning('SoftwareCatalogueService: Missing Voorzieningen configuration for contact person sync', [
- 'organizationUuid' => $organizationUuid,
- 'contactSchemaId' => $contactSchemaId,
- 'registerId' => $registerId
- ]);
+ $contactSchemaId = $voorzieningenConfig['contactpersoon_schema'] ?? null;
+ $registerId = $voorzieningenConfig['register'] ?? null;
+
+ if ($contactSchemaId === null || $registerId === false) {
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: Missing Voorzieningen configuration for contact person sync',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'contactSchemaId' => $contactSchemaId,
+ 'registerId' => $registerId,
+ ]
+ );
return;
}
-
+
try {
- // Find all contact persons that have this organization as their organisatie
- $contactPersons = $objectService->findAll([
- 'filters' => [
- 'register' => (string) $registerId,
- 'schema' => $contactSchemaId,
- 'organisatie' => $organizationUuid
- ]
- ]);
-
- $this->_logger->info('SoftwareCatalogueService: Found contact persons for synchronization', [
- 'organizationUuid' => $organizationUuid,
- 'contactPersonCount' => count($contactPersons)
- ]);
-
- // Collect all usernames from contact persons
+ // Find all contact persons that have this organization as their organisatie.
+ $contactPersons = $objectService->findAll(
+ [
+ 'filters' => [
+ 'register' => (string) $registerId,
+ 'schema' => $contactSchemaId,
+ 'organisatie' => $organizationUuid,
+ ],
+ ]
+ );
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Found contact persons for synchronization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'contactPersonCount' => count($contactPersons),
+ ]
+ );
+
+ // Collect all usernames from contact persons.
$contactPersonUsernames = [];
foreach ($contactPersons as $contactPerson) {
$contactData = $contactPerson->getObject();
- $email = $contactData['email'] ?? null;
- if ($email) {
+ $email = $contactData['email'] ?? null;
+ if (empty($email) === false) {
$contactPersonUsernames[] = $email;
- $this->_logger->info('SoftwareCatalogueService: Found contact person username', [
- 'username' => $email,
- 'contactPersonId' => $contactPerson->getId()
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Found contact person username',
+ [
+ 'username' => $email,
+ 'contactPersonId' => $contactPerson->getId(),
+ ]
+ );
}
}
-
- // Get the organization entity
+
+ // Get the organization entity.
$organisationMapper = $this->_container->get('OCA\\OpenRegister\\Db\\OrganisationMapper');
- $organisation = $organisationMapper->findByUuid($organizationUuid);
-
- if (!$organisation) {
- $this->_logger->error('SoftwareCatalogueService: Organization entity not found for synchronization', [
- 'organizationUuid' => $organizationUuid
- ]);
+ $organisation = $organisationMapper->findByUuid($organizationUuid);
+
+ if ($organisation === null) {
+ $this->_logger->error(
+ 'SoftwareCatalogueService: Organization entity not found for synchronization',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
return;
}
-
- // Get current users and add contact person usernames
+
+ // Get current users and add contact person usernames.
$currentUsers = $organisation->getUsers() ?? [];
- $allUsers = array_merge($currentUsers, $contactPersonUsernames);
- $allUsers = array_unique($allUsers);
-
- $this->_logger->info('SoftwareCatalogueService: Updating organization entity users', [
- 'organizationUuid' => $organizationUuid,
- 'currentUsers' => $currentUsers,
- 'contactPersonUsernames' => $contactPersonUsernames,
- 'finalUsers' => $allUsers
- ]);
-
- // Update the organization entity
+ $allUsers = array_merge($currentUsers, $contactPersonUsernames);
+ $allUsers = array_unique($allUsers);
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Updating organization entity users',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'currentUsers' => $currentUsers,
+ 'contactPersonUsernames' => $contactPersonUsernames,
+ 'finalUsers' => $allUsers,
+ ]
+ );
+
+ // Update the organization entity.
$organisation->setUsers($allUsers);
$organisationMapper->save($organisation);
-
- $this->_logger->info('SoftwareCatalogueService: Successfully synchronized contact person usernames', [
- 'organizationUuid' => $organizationUuid,
- 'totalUsers' => count($allUsers)
- ]);
-
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Successfully synchronized contact person usernames',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'totalUsers' => count($allUsers),
+ ]
+ );
} catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
- // Organization entity doesn't exist yet - this can happen due to race conditions
- // Log and return gracefully, the organization sync will handle this later
- $this->_logger->warning('SoftwareCatalogueService: Organization entity not found during username sync (race condition)', [
- 'organizationUuid' => $organizationUuid,
- 'message' => 'This is expected during anonymous registration - organization entity is created after contact persons'
- ]);
+ // Organization entity doesn't exist yet - this can happen due to race conditions.
+ // Log and return gracefully, the organization sync will handle this later.
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: Organization entity not found during username sync (race condition)',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'message' => 'This is expected during anonymous registration - organization entity is created after contact persons',
+ ]
+ );
} catch (\Exception $e) {
- $this->_logger->error('SoftwareCatalogueService: Error synchronizing contact person usernames', [
- 'organizationUuid' => $organizationUuid,
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
- ]);
- }
- }
+ $this->_logger->error(
+ 'SoftwareCatalogueService: Error synchronizing contact person usernames',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]
+ );
+ }//end try
+ }//end syncContactPersonUsernamesWithOrganization()
/**
* Ensures a contact person's username is in their organization's users array
* This method is called when a contact person is created or updated
- *
+ *
* @param object $contactPersonObject The contact person object
- *
+ *
* @return void
*/
public function ensureContactPersonInOrganization(object $contactPersonObject): void
{
$contactData = $contactPersonObject->getObject();
- $email = $contactData['email'] ?? null;
+ $email = $contactData['email'] ?? null;
$organisatie = $contactData['organisatie'] ?? null;
-
- if (!$email || !$organisatie) {
- $this->_logger->info('SoftwareCatalogueService: Contact person missing email or organisation', [
- 'contactPersonId' => $contactPersonObject->getId(),
- 'hasEmail' => !empty($email),
- 'hasOrganisatie' => !empty($organisatie)
- ]);
+
+ if ($email === null || $organisatie === false) {
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Contact person missing email or organisation',
+ [
+ 'contactPersonId' => $contactPersonObject->getId(),
+ 'hasEmail' => empty($email) === false,
+ 'hasOrganisatie' => empty($organisatie) === false,
+ ]
+ );
return;
}
-
- // Skip if the contact person is owned by the default organization
+
+ // Skip if the contact person is owned by the default organization.
$owner = $contactPersonObject->getOwner();
if ($owner === 'system') {
- $this->_logger->info('SoftwareCatalogueService: Skipping contact person owned by system', [
- 'contactPersonId' => $contactPersonObject->getId(),
- 'username' => $email
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Skipping contact person owned by system',
+ [
+ 'contactPersonId' => $contactPersonObject->getId(),
+ 'username' => $email,
+ ]
+ );
return;
}
-
- $this->_logger->info('SoftwareCatalogueService: Ensuring contact person in organization', [
- 'contactPersonId' => $contactPersonObject->getId(),
- 'username' => $email,
- 'organisatie' => $organisatie
- ]);
-
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Ensuring contact person in organization',
+ [
+ 'contactPersonId' => $contactPersonObject->getId(),
+ 'username' => $email,
+ 'organisatie' => $organisatie,
+ ]
+ );
+
try {
- // Get the organization entity
+ // Get the organization entity.
$organisationMapper = $this->_container->get('OCA\\OpenRegister\\Db\\OrganisationMapper');
- $organisation = $organisationMapper->findByUuid($organisatie);
-
- if (!$organisation) {
- $this->_logger->error('SoftwareCatalogueService: Organization entity not found for contact person', [
- 'contactPersonId' => $contactPersonObject->getId(),
- 'organisatie' => $organisatie
- ]);
+ $organisation = $organisationMapper->findByUuid($organisatie);
+
+ if ($organisation === null) {
+ $this->_logger->error(
+ 'SoftwareCatalogueService: Organization entity not found for contact person',
+ [
+ 'contactPersonId' => $contactPersonObject->getId(),
+ 'organisatie' => $organisatie,
+ ]
+ );
return;
}
-
- // Check if the username is already in the organization's users array
+
+ // Check if the username is already in the organization's users array.
$currentUsers = $organisation->getUsers() ?? [];
- if (in_array($email, $currentUsers)) {
- $this->_logger->info('SoftwareCatalogueService: Contact person already in organization', [
- 'contactPersonId' => $contactPersonObject->getId(),
- 'username' => $email,
- 'organisatie' => $organisatie
- ]);
+ if (in_array($email, $currentUsers) === true) {
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Contact person already in organization',
+ [
+ 'contactPersonId' => $contactPersonObject->getId(),
+ 'username' => $email,
+ 'organisatie' => $organisatie,
+ ]
+ );
return;
}
-
- // Add the username to the organization's users array
+
+ // Add the username to the organization's users array.
$currentUsers[] = $email;
$organisation->setUsers($currentUsers);
$organisationMapper->save($organisation);
-
- $this->_logger->info('SoftwareCatalogueService: Successfully added contact person to organization', [
- 'contactPersonId' => $contactPersonObject->getId(),
- 'username' => $email,
- 'organisatie' => $organisatie,
- 'totalUsers' => count($currentUsers)
- ]);
-
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Successfully added contact person to organization',
+ [
+ 'contactPersonId' => $contactPersonObject->getId(),
+ 'username' => $email,
+ 'organisatie' => $organisatie,
+ 'totalUsers' => count($currentUsers),
+ ]
+ );
} catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
- // Organization entity doesn't exist yet - this can happen due to race conditions
- // Log and return gracefully, the organization sync will handle this later
- $this->_logger->warning('SoftwareCatalogueService: Organization entity not found (race condition), will be handled by organization sync', [
- 'contactPersonId' => $contactPersonObject->getId(),
- 'username' => $email,
- 'organisatie' => $organisatie,
- 'message' => 'This is expected during anonymous registration - organization entity is created after contact persons'
- ]);
+ // Organization entity doesn't exist yet - this can happen due to race conditions.
+ // Log and return gracefully, the organization sync will handle this later.
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: Organization entity not found (race condition), will be handled by organization sync',
+ [
+ 'contactPersonId' => $contactPersonObject->getId(),
+ 'username' => $email,
+ 'organisatie' => $organisatie,
+ 'message' => 'This is expected during anonymous registration - organization entity is created after contact persons',
+ ]
+ );
return;
- }
- }
+ }//end try
+ }//end ensureContactPersonInOrganization()
/**
* Updates organization references on objects to point to the newly created organization entity
- *
+ *
* @param object $organizationObject The organization object
- *
+ *
* @return void
*/
private function updateOrganizationReferences(object $organizationObject): void
{
try {
- $this->_logger->info('SoftwareCatalogueService: Updating organization references', [
- 'objectId' => $organizationObject->getId()
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Updating organization references',
+ [
+ 'objectId' => $organizationObject->getId(),
+ ]
+ );
- $objectData = $organizationObject->getObject();
+ $objectData = $organizationObject->getObject();
$organizationUuid = $objectData['id'] ?? $organizationObject->getId();
- // Get the ObjectService to update objects
- $objectService = $this->_getObjectService();
- if (!$objectService) {
+ // Get the ObjectService to update objects.
+ $objectService = $this->getObjectService();
+ if ($objectService === null) {
$this->_logger->error('SoftwareCatalogueService: ObjectService not available for updating references');
return;
}
- // Get the organization entity UUID (should be the same as the organization object UUID)
+ // Get the organization entity UUID (should be the same as the organization object UUID).
$organisationMapper = $this->_container->get('OCA\\OpenRegister\\Db\\OrganisationMapper');
try {
- // Use the original UUID format for OpenRegister lookup
- $organisationEntity = $organisationMapper->findByUuid($organizationUuid);
+ // Use the original UUID format for OpenRegister lookup.
+ $organisationEntity = $organisationMapper->findByUuid($organizationUuid);
$organisationEntityUuid = $organisationEntity->getUuid();
-
- $this->_logger->info('SoftwareCatalogueService: Found organization entity for reference update', [
- 'organizationObjectUuid' => $organizationUuid,
- 'organizationEntityUuid' => $organisationEntityUuid
- ]);
+
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Found organization entity for reference update',
+ [
+ 'organizationObjectUuid' => $organizationUuid,
+ 'organizationEntityUuid' => $organisationEntityUuid,
+ ]
+ );
} catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
- $this->_logger->error('SoftwareCatalogueService: Organization entity not found for reference update', [
- 'organizationUuid' => $organizationUuid
- ]);
+ $this->_logger->error(
+ 'SoftwareCatalogueService: Organization entity not found for reference update',
+ [
+ 'organizationUuid' => $organizationUuid,
+ ]
+ );
return;
- }
+ }//end try
- // Update the organization object's @self.organisation field
- $this->_logger->info('SoftwareCatalogueService: Updating organization object reference', [
- 'objectId' => $organizationObject->getId(),
- 'newOrganisationUuid' => $organisationEntityUuid
- ]);
+ // Update the organization object's @self.organisation field.
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Updating organization object reference',
+ [
+ 'objectId' => $organizationObject->getId(),
+ 'newOrganisationUuid' => $organisationEntityUuid,
+ ]
+ );
- // Get the current object data and update the organisation field
+ // Get the current object data and update the organisation field.
$currentObjectData = $organizationObject->getObject();
$currentObjectData['@self']['organisation'] = $organisationEntityUuid;
-
- // Update the organization object using the ObjectService
+
+ // Update the organization object using the ObjectService.
+ // Don't update version, not a patch, no extend.
$objectService->updateFromArray(
$organizationObject->getId(),
$currentObjectData,
- false, // don't update version
- false, // not a patch
- [], // no extend
+ false,
+ false,
+ [],
$organizationObject->getRegisterId(),
$organizationObject->getSchemaId()
);
- // Update contact person objects' @self.organisatie field
+ // Update contact person objects' @self.organisatie field.
$contactpersonen = $objectData['contactpersonen'] ?? [];
foreach ($contactpersonen as $contactUuid) {
- $this->_logger->info('SoftwareCatalogueService: Updating contact person object reference', [
- 'contactUuid' => $contactUuid,
- 'newOrganisationUuid' => $organisationEntityUuid
- ]);
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Updating contact person object reference',
+ [
+ 'contactUuid' => $contactUuid,
+ 'newOrganisationUuid' => $organisationEntityUuid,
+ ]
+ );
- // Get the contact person schema ID from configuration
- $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
+ // Get the contact person schema ID from configuration.
+ $settingsService = $this->_container->get('OCA\SoftwareCatalog\Service\SettingsService');
$voorzieningenConfig = $settingsService->getVoorzieningenConfig();
- $contactSchemaId = $voorzieningenConfig['contactpersoon_schema'] ?? null;
-
- if (!$contactSchemaId) {
- $this->_logger->warning('SoftwareCatalogueService: Missing contactpersoon schema configuration for object update', [
- 'contactUuid' => $contactUuid
- ]);
+ $contactSchemaId = $voorzieningenConfig['contactpersoon_schema'] ?? null;
+
+ if ($contactSchemaId === null) {
+ $this->_logger->warning(
+ 'SoftwareCatalogueService: Missing contactpersoon schema configuration for object update',
+ [
+ 'contactUuid' => $contactUuid,
+ ]
+ );
continue;
}
- // Find the contact person object
+ // Find the contact person object.
try {
$contactObject = $objectService->find($contactUuid, [], false, $organizationObject->getRegisterId(), $contactSchemaId);
- if ($contactObject) {
- // Get the current object data and update the organisatie field
+ if (empty($contactObject) === false) {
+ // Get the current object data and update the organisatie field.
$contactObjectData = $contactObject->getObject();
$contactObjectData['@self']['organisatie'] = $organisationEntityUuid;
-
- // Update the contact person object using the ObjectService
+
+ // Update the contact person object using the ObjectService.
+ // Don't update version, not a patch, no extend.
$objectService->updateFromArray(
$contactObject->getId(),
$contactObjectData,
- false, // don't update version
- false, // not a patch
- [], // no extend
+ false,
+ false,
+ [],
$organizationObject->getRegisterId(),
$contactSchemaId
);
}
} catch (\Exception $e) {
- $this->_logger->error('SoftwareCatalogueService: Failed to update contact person object', [
- 'contactUuid' => $contactUuid,
- 'error' => $e->getMessage()
- ]);
- }
- }
-
- $this->_logger->info('SoftwareCatalogueService: Successfully updated organization references', [
- 'organizationUuid' => $organizationUuid,
- 'organizationEntityUuid' => $organisationEntityUuid,
- 'contactPersonCount' => count($contactpersonen)
- ]);
+ $this->_logger->error(
+ 'SoftwareCatalogueService: Failed to update contact person object',
+ [
+ 'contactUuid' => $contactUuid,
+ 'error' => $e->getMessage(),
+ ]
+ );
+ }//end try
+ }//end foreach
+ $this->_logger->info(
+ 'SoftwareCatalogueService: Successfully updated organization references',
+ [
+ 'organizationUuid' => $organizationUuid,
+ 'organizationEntityUuid' => $organisationEntityUuid,
+ 'contactPersonCount' => count($contactpersonen),
+ ]
+ );
} catch (\Exception $e) {
$this->_logger->error(
- 'SoftwareCatalogueService: Failed to update organization references: ' . $e->getMessage(),
+ 'SoftwareCatalogueService: Failed to update organization references: '.$e->getMessage(),
[
- 'objectId' => $organizationObject->getId(),
+ 'objectId' => $organizationObject->getId(),
'exception' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- 'trace' => $e->getTraceAsString()
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ 'trace' => $e->getTraceAsString(),
]
);
- }
- }
-
-}
\ No newline at end of file
+ }//end try
+ }//end updateOrganizationReferences()
+}//end class
diff --git a/lib/Service/SymfonyEmailService.php b/lib/Service/SymfonyEmailService.php
index 20012d78..62d9dc07 100644
--- a/lib/Service/SymfonyEmailService.php
+++ b/lib/Service/SymfonyEmailService.php
@@ -1,5 +1,17 @@
+ * @copyright 2024 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ * @version GIT:
+ * @link https://github.com/ConductionNL/SoftwareCatalog
+ */
+
declare(strict_types=1);
namespace OCA\SoftwareCatalog\Service;
@@ -26,7 +38,7 @@
* @author Conduction b.v.
* @license AGPL-3.0-or-later https://www.gnu.org/licenses/agpl-3.0.html
* @link https://github.com/ConductionNL/SoftwareCatalog
- * @version 1.0.0
+ * @version GIT:
*/
class SymfonyEmailService
{
@@ -102,7 +114,8 @@ class SymfonyEmailService
Welkom {{ user.name }}!
Beste {{ user.name }},
- Hartelijk welkom bij de Software Catalogus! Uw gebruikersaccount is succesvol aangemaakt voor {{ organization.name }}.
+ Hartelijk welkom bij de Software Catalogus! Uw gebruikersaccount is
+ succesvol aangemaakt voor {{ organization.name }}.
U kunt nu:
Inloggen op het platform
@@ -142,7 +155,8 @@ class SymfonyEmailService
Tijdelijk wachtwoord: {{ user.password }}
Belangrijk: We raden u aan om dit tijdelijke wachtwoord te wijzigen na uw eerste inlog.
- U kunt inloggen op het platform en direct aan de slag met het beheren van software voor {{ organization.name }}.
+ U kunt inloggen op het platform en direct aan de slag met het beheren
+ van software voor {{ organization.name }}.
Heeft u vragen? Neem dan contact met ons op via info@conduction.nl
Met vriendelijke groet, Het Software Catalogus Team
@@ -172,7 +186,8 @@ class SymfonyEmailService
Samenwerken met andere organisaties
Login gegevens:
- We hebben uw bestaande account gekoppeld aan een nieuwe organisatie. Uw inloggegevens zijn hetzelfde, maar u kunt nu uw organisatie wisselen tussen uw organisaties.
+ We hebben uw bestaande account gekoppeld aan een nieuwe organisatie.
+ Uw inloggegevens zijn hetzelfde, maar u kunt nu uw organisatie wisselen tussen uw organisaties.
Heeft u vragen? Neem dan contact met ons op via info@conduction.nl
Met vriendelijke groet, Het Software Catalogus Team