Appearance
Documents / File Downloads
Flow ID: CF-31 | Module(s): documents, Cms\Document domain | Complexity: Low
Business Overview
Public-facing document repository where administrators upload downloadable files (PDFs, spreadsheets, Word documents) and customers browse and download them from a paginated storefront page. Each document has per-language content: a title, optional subtitle, a downloadable file, and an optional display image.
What customers experience:
- Visit
/documentsto see all active documents, ordered by admin-defined position - Each document shows its title, subtitle, optional banner image, and a download link
- Clicking "Download file" opens the file in a new browser tab via a direct asset URL
- Pagination at 20 documents per page
What administrators experience:
- Admin panel at
documents/documents_admin(under the "Content" menu group, labeled "Files") - Add documents with per-language title, subtitle, file upload, and optional banner image
- Drag-and-drop reordering (position-based sorting via AJAX)
- Toggle active/inactive status per document
- Copy file URL to clipboard for embedding elsewhere
- Delete documents with file cleanup from storage
- Allowed file types:
pdf,doc,docx,xls,xlsx,txt
Key business behaviors:
- Multi-language support: each language gets its own file and image via the
documents_muicompanion table - Only active documents (
active = 1) are shown on the storefront - Files stored in
files/documents/directory via thestorage()abstraction (local, S3, or SFTP) - Storefront results are cached via
pscachewith the front controller's default TTL - Admin edits invalidate the cache (
pscache->delete_all('documents_model/')) - New documents default to active; the active toggle is only available on the edit form
- On document deletion, both the file and image are cleaned from storage
API Reference
REST Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /rest/cms/document | Guest | List documents (paginated, filterable) |
| GET | /rest/cms/document/{id} | Guest | Get single document by ID |
| GET | /rest/cms/document/item | Guest | Get single document by filters |
| POST | /rest/cms/document | Backend (Admin, CMS, Marketing, Media) | Create document |
| POST | /rest/cms/document/{id} | Backend (Admin, CMS, Marketing, Media) | Update document |
| DELETE | /rest/cms/document/{id} | Backend (Admin, CMS, Marketing, Media) | Delete document |
All GET endpoints also support locale-prefixed paths: /{lang}/rest/cms/document.
Query parameters (GET collection):
| Parameter | Type | Description |
|---|---|---|
filter[id] | string | Filter by document IDs (comma-separated) |
filter[active] | boolean | Filter by active status |
filter[title.{locale}] | string | Partial match on title in a specific language |
sort | string | Sort field: id, active, position, title.{locale} |
page | integer | Page number |
limit | integer | Items per page |
with | string | Include relations (e.g., translations) |
REST resource shape (DocumentResource):
json
{
"id": 1,
"date": "2025-01-15T10:30:00+00:00",
"active": 1,
"position": 0,
"translations": [
{
"id": 1,
"documentId": 1,
"title": "Product Catalog 2025",
"subtitle": "Complete product listing",
"file": "/files/documents/catalog_2025.pdf",
"image": "/files/documents/catalog_banner.jpg",
"lang": "en"
}
]
}Note: active and position fields are only included in backend context responses.
Legacy Storefront
| URL | Method | Description |
|---|---|---|
/documents | index() | Paginated document listing (20/page) |
/documents/{offset} | index($offset) | Offset-based pagination |
Legacy Admin
| URL | Method | Description |
|---|---|---|
documents/documents_admin | index() | List all documents |
documents/documents_admin/add | add() | Create form + handler |
documents/documents_admin/edit/{id} | edit($id) | Edit form + handler |
documents/documents_admin/delete/{id} | delete($id) | Delete document |
documents/documents_admin/update_order | update_order() | AJAX position reorder |
Code Flow
Storefront Display
File: ecommercen/documents/controllers/Adv_documents.php
- Load model:
documents/documents_model - Fetch documents:
pscache->model('documents_model', 'getDocumentsFront', ...)-- cached query - Filter: Only
active = 1records, current language, ordered byposition ASC - Paginate: 20 per page, offset-based via URL segment
- Render: Template view
documentsPagewith documents array and pagination
View: application/views/main/layouts/documents/documents.php
- Iterates documents, shows image (via
html_picture()with responsive srcset), title, subtitle - Download link:
assetUrl("files/documents/{$document->file}")opened in a new tab - Empty state message when no documents exist
Admin Create
File: ecommercen/documents/controllers/Adv_documents_admin.php -- add()
- Validate: Per-language
title(required),subtitle,file,imagefields - Upload:
advuploader->uploadFilesUsingConfig()with config fromgetUploadConfig() - Build MUI data: Loop
adminLanguages, assemble title, subtitle, file path, image path per language - Insert:
documents_model->add_record($data, $dataMui)-- inserts master row then MUI rows - Hook:
afterAdd($documentId)-- empty by default, available for client override - Redirect: Back to listing
Admin Edit
File: ecommercen/documents/controllers/Adv_documents_admin.php -- edit($id)
- Delete images: If
del_image_{lang}checkbox posted, delete image viadeleteFile($id, 'image', $lang) - Validate + Upload: Same as create, but file upload is optional
- Update:
documents_model->update_record($data, $dataMui, $id)-- updates master + upserts MUI - Cache clear:
pscache->delete_all('documents_model/') - Hook:
afterUpdate($documentId)-- empty by default
File Upload Configuration
php
// Upload folder: files/documents/
// File fields: allowed types from config('documentsAllowedTypes') = pdf|doc|docx|xls|xlsx|txt
// Image fields: default image allowed types
// Max size: config('max_upload_size')Domain Layer
| Component | Path |
|---|---|
| Entity | src/Domains/Cms/Document/Repository/Entity.php |
| MuiEntity | src/Domains/Cms/Document/Repository/MuiEntity.php |
| Repository | src/Domains/Cms/Document/Repository/Repository.php |
| RepositoryConfigurator | src/Domains/Cms/Document/Repository/RepositoryConfigurator.php |
| MuiRepository | src/Domains/Cms/Document/Repository/MuiRepository.php |
| WriteRepository | src/Domains/Cms/Document/Repository/WriteRepository.php |
| MuiWriteRepository | src/Domains/Cms/Document/Repository/MuiWriteRepository.php |
| Service (read) | src/Domains/Cms/Document/Service.php |
| WriteService | src/Domains/Cms/Document/WriteService.php |
| WriteData | src/Domains/Cms/Document/WriteData.php |
| MuiWriteData | src/Domains/Cms/Document/MuiWriteData.php |
| Validator | src/Domains/Cms/Document/Validator.php |
| ListRequest | src/Domains/Cms/Document/ListRequest.php |
| FilterByTranslation | src/Domains/Cms/Document/Repository/Specification/FilterByTranslation.php |
| SortByTranslation | src/Domains/Cms/Document/Repository/Specification/SortByTranslation.php |
REST Layer
| Component | Path |
|---|---|
| Controller | src/Rest/Cms/Controllers/Document.php |
| Resource | src/Rest/Cms/Resources/Document/Resource.php |
| Collection | src/Rest/Cms/Resources/Document/Collection.php |
| MuiResource | src/Rest/Cms/Resources/Document/MuiResource.php |
| MuiCollection | src/Rest/Cms/Resources/Document/MuiCollection.php |
Legacy Layer
| Component | Path |
|---|---|
| Front controller | ecommercen/documents/controllers/Adv_documents.php |
| Admin controller | ecommercen/documents/controllers/Adv_documents_admin.php |
| Model | ecommercen/documents/models/Adv_documents_model.php |
| Storefront view | application/views/main/layouts/documents/documents.php |
| Admin list view | application/views/admin/documents/list.php |
| Admin create view | application/views/admin/documents/create.php |
| Admin update view | application/views/admin/documents/update.php |
Architecture
Entity Properties
Entity (documents table): id, position, active, date_created
The entity implements FilterTranslation and uses the FiltersTranslation trait for cross-table filtering on MUI fields.
MuiEntity (documents_mui table): id, document_id, title, subtitle, file, image, lang
Repository Relations
documents (Entity)
└─ translations (ONE_TO_MANY) → documents_mui (MuiEntity) via document_idSpecification Pattern
- FilterByTranslation: Subquery-based filter on MUI fields. Supports
Exact(IN clause) andPartial(LIKE with wildcards) match types. Scoped by locale. - SortByTranslation: LEFT JOIN on
documents_muiwith locale constraint, GROUP BYdocuments.id, ORDER BY the joined MUI field.
DI Container Registration
Domain: src/Domains/Cms/container.php -- registers Repository, RepositoryConfigurator, MuiRepository (with NullRelationConfigurator), Service, WriteRepository, MuiWriteRepository, Validator, WriteService.
REST: src/Rest/Cms/container.php -- wires the Document controller with Service, Resource, Collection, ListRequest, and WriteService references.
REST Controller Traits
The Document controller extends HandlesRestfulActions (for GET endpoints) and uses HandlesWriteActions (for POST/DELETE). Write operations accept JSON bodies -- file uploads are handled by the legacy admin, not via the REST API.
Data Model
Tables
| Table | Key Columns | Description |
|---|---|---|
documents | id (PK, auto), position (int), active (int, 0/1), date_created (datetime) | Master document record |
documents_mui | id (PK, auto), document_id (FK), title (varchar), subtitle (varchar, nullable), file (varchar), image (varchar, nullable), lang (varchar) | Per-language content |
File Storage
- Path:
files/documents/ - Storage backend: Via
storage()helper (Flysystem -- local, S3, or SFTP depending on environment) - File naming: Managed by the
advuploaderlibrary (auto-generated unique names) - REST file URLs: Prefixed with
/files/documents/byformatFile()in MuiResource
Configuration
| Config | File | Value |
|---|---|---|
documentsAllowedTypes | application/config/app.php | ['pdf|doc|docx|xls|xlsx|txt'] |
max_upload_size | application/config/app.php | Global max upload size |
imagesAllowedTypes | application/config/app.php | Used for banner image uploads |
Routes
Legacy routes (application/config/routes.php):
php
$route['documents/documents_admin'] = 'documents/documents_admin';
$route['documents/documents_admin/(.+)'] = 'documents/documents_admin/$1';
$route['documents'] = 'documents/documents';
$route['documents/(:num)'] = 'documents/documents/index/$1';REST routes (application/config/rest_routes.php):
php
// Read (Guest)
$route['rest/cms/document']['GET']
$route['rest/cms/document/item']['GET']
$route['rest/cms/document/(:num)']['GET']
// Write (Backend auth)
$route['rest/cms/document']['POST']
$route['rest/cms/document/(:num)']['POST']
$route['rest/cms/document/(:num)']['DELETE']
// + locale-prefixed variants: (\w{2})/rest/cms/document/...REST Policies
php
Document::class => [
'defaults' => ['auth' => 'backend', 'roles' => [AUTH_ROLE_ADMIN, AUTH_ROLE_CMS, AUTH_ROLE_MARKETING, AUTH_ROLE_MEDIA]],
'methods' => [
'index' => ['auth' => 'guest'],
'show' => ['auth' => 'guest'],
'item' => ['auth' => 'guest'],
],
],Admin Menu
Registered in application/config/admin_menu.php under the CONTENT group:
- Route:
documents/documents_admin - Icon:
fa-regular fa-file - Label: "Files" (
admin.menu.documents.label) - Roles: ADVISABLE, ADMIN, CMS, MEDIA
Admin Access Roles
Legacy admin controller allows: AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, AUTH_ROLE_MARKETING, AUTH_ROLE_MEDIA.
Client Extension Points
afterAdd($documentId): Empty hook in admin controller, override inapplication/controllers/to add post-create logicafterUpdate($documentId): Empty hook for post-update logicafterDelete($id): Empty hook for post-delete cleanupdeleteFile($filename): Protected method for custom file deletion logicgetUploadConfig($filesPostKeys): Override to customize upload folder, allowed types, or size limitsvalidation($isUpdate): Override to add custom validation rules per language- Admin views: Override
application/views/admin/documents/templates for custom UI - Storefront view: Override via theme template
documentsPagelayout - DI override: Register a
Custom\Domains\Cms\Document\...service and alias the upstream class incustom/Domains/container.php - REST WriteService: Override
WriteServiceorValidatorto add custom business rules for API-based document management - Allowed file types: Override
documentsAllowedTypesin clientapplication/config/app.php
Business Rules
- Active filtering: Only documents with
active = 1appear on the storefront - Position ordering: Admin-defined order via drag-and-drop, stored as integer position values
- Per-language files: Each language can have a different file and image -- the storefront shows the file for the current language
- File required on create: The create form requires a file per language (
requiredattribute); edit allows keeping the existing file - Cache invalidation: Admin updates call
pscache->delete_all('documents_model/')to clear cached storefront queries - Image deletion: Individual language images can be deleted via checkbox on the edit form without replacing them
- File cleanup on delete:
delete_record()attempts to remove both the file and image from storage per language - No authentication required: The
/documentsstorefront page and REST GET endpoints are publicly accessible (guest auth) - Write operations restricted: REST POST/DELETE require backend JWT with Admin, CMS, Marketing, or Media roles
Related Flows
- CF-02 Product Detail -- product-specific file downloads use the separate
product_downloadstable andAdvDownloadsToProductlibrary - SY-23 MUI Translation Pattern --
documents_muicompanion table stores per-language title, subtitle, file, and image - SY-25 File Upload & Storage --
advuploader->uploadFilesUsingConfig()processes per-language file and image uploads; REST write layer usesHandlesUploadActions - SY-28 Storage Abstraction -- file cleanup on delete and physical file removal routed through the storage abstraction layer
See also the Storage guide for details on Flysystem storage backends (local, S3, SFTP).