Skip to content

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 /documents to 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_mui companion table
  • Only active documents (active = 1) are shown on the storefront
  • Files stored in files/documents/ directory via the storage() abstraction (local, S3, or SFTP)
  • Storefront results are cached via pscache with 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

MethodPathAuthDescription
GET/rest/cms/documentGuestList documents (paginated, filterable)
GET/rest/cms/document/{id}GuestGet single document by ID
GET/rest/cms/document/itemGuestGet single document by filters
POST/rest/cms/documentBackend (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):

ParameterTypeDescription
filter[id]stringFilter by document IDs (comma-separated)
filter[active]booleanFilter by active status
filter[title.{locale}]stringPartial match on title in a specific language
sortstringSort field: id, active, position, title.{locale}
pageintegerPage number
limitintegerItems per page
withstringInclude 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

URLMethodDescription
/documentsindex()Paginated document listing (20/page)
/documents/{offset}index($offset)Offset-based pagination

Legacy Admin

URLMethodDescription
documents/documents_adminindex()List all documents
documents/documents_admin/addadd()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_orderupdate_order()AJAX position reorder

Code Flow

Storefront Display

File: ecommercen/documents/controllers/Adv_documents.php

  1. Load model: documents/documents_model
  2. Fetch documents: pscache->model('documents_model', 'getDocumentsFront', ...) -- cached query
  3. Filter: Only active = 1 records, current language, ordered by position ASC
  4. Paginate: 20 per page, offset-based via URL segment
  5. Render: Template view documentsPage with 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()

  1. Validate: Per-language title (required), subtitle, file, image fields
  2. Upload: advuploader->uploadFilesUsingConfig() with config from getUploadConfig()
  3. Build MUI data: Loop adminLanguages, assemble title, subtitle, file path, image path per language
  4. Insert: documents_model->add_record($data, $dataMui) -- inserts master row then MUI rows
  5. Hook: afterAdd($documentId) -- empty by default, available for client override
  6. Redirect: Back to listing

Admin Edit

File: ecommercen/documents/controllers/Adv_documents_admin.php -- edit($id)

  1. Delete images: If del_image_{lang} checkbox posted, delete image via deleteFile($id, 'image', $lang)
  2. Validate + Upload: Same as create, but file upload is optional
  3. Update: documents_model->update_record($data, $dataMui, $id) -- updates master + upserts MUI
  4. Cache clear: pscache->delete_all('documents_model/')
  5. 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

ComponentPath
Entitysrc/Domains/Cms/Document/Repository/Entity.php
MuiEntitysrc/Domains/Cms/Document/Repository/MuiEntity.php
Repositorysrc/Domains/Cms/Document/Repository/Repository.php
RepositoryConfiguratorsrc/Domains/Cms/Document/Repository/RepositoryConfigurator.php
MuiRepositorysrc/Domains/Cms/Document/Repository/MuiRepository.php
WriteRepositorysrc/Domains/Cms/Document/Repository/WriteRepository.php
MuiWriteRepositorysrc/Domains/Cms/Document/Repository/MuiWriteRepository.php
Service (read)src/Domains/Cms/Document/Service.php
WriteServicesrc/Domains/Cms/Document/WriteService.php
WriteDatasrc/Domains/Cms/Document/WriteData.php
MuiWriteDatasrc/Domains/Cms/Document/MuiWriteData.php
Validatorsrc/Domains/Cms/Document/Validator.php
ListRequestsrc/Domains/Cms/Document/ListRequest.php
FilterByTranslationsrc/Domains/Cms/Document/Repository/Specification/FilterByTranslation.php
SortByTranslationsrc/Domains/Cms/Document/Repository/Specification/SortByTranslation.php

REST Layer

ComponentPath
Controllersrc/Rest/Cms/Controllers/Document.php
Resourcesrc/Rest/Cms/Resources/Document/Resource.php
Collectionsrc/Rest/Cms/Resources/Document/Collection.php
MuiResourcesrc/Rest/Cms/Resources/Document/MuiResource.php
MuiCollectionsrc/Rest/Cms/Resources/Document/MuiCollection.php

Legacy Layer

ComponentPath
Front controllerecommercen/documents/controllers/Adv_documents.php
Admin controllerecommercen/documents/controllers/Adv_documents_admin.php
Modelecommercen/documents/models/Adv_documents_model.php
Storefront viewapplication/views/main/layouts/documents/documents.php
Admin list viewapplication/views/admin/documents/list.php
Admin create viewapplication/views/admin/documents/create.php
Admin update viewapplication/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_id

Specification Pattern

  • FilterByTranslation: Subquery-based filter on MUI fields. Supports Exact (IN clause) and Partial (LIKE with wildcards) match types. Scoped by locale.
  • SortByTranslation: LEFT JOIN on documents_mui with locale constraint, GROUP BY documents.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

TableKey ColumnsDescription
documentsid (PK, auto), position (int), active (int, 0/1), date_created (datetime)Master document record
documents_muiid (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 advuploader library (auto-generated unique names)
  • REST file URLs: Prefixed with /files/documents/ by formatFile() in MuiResource

Configuration

ConfigFileValue
documentsAllowedTypesapplication/config/app.php['pdf|doc|docx|xls|xlsx|txt']
max_upload_sizeapplication/config/app.phpGlobal max upload size
imagesAllowedTypesapplication/config/app.phpUsed 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 in application/controllers/ to add post-create logic
  • afterUpdate($documentId): Empty hook for post-update logic
  • afterDelete($id): Empty hook for post-delete cleanup
  • deleteFile($filename): Protected method for custom file deletion logic
  • getUploadConfig($filesPostKeys): Override to customize upload folder, allowed types, or size limits
  • validation($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 documentsPage layout
  • DI override: Register a Custom\Domains\Cms\Document\... service and alias the upstream class in custom/Domains/container.php
  • REST WriteService: Override WriteService or Validator to add custom business rules for API-based document management
  • Allowed file types: Override documentsAllowedTypes in client application/config/app.php

Business Rules

  1. Active filtering: Only documents with active = 1 appear on the storefront
  2. Position ordering: Admin-defined order via drag-and-drop, stored as integer position values
  3. Per-language files: Each language can have a different file and image -- the storefront shows the file for the current language
  4. File required on create: The create form requires a file per language (required attribute); edit allows keeping the existing file
  5. Cache invalidation: Admin updates call pscache->delete_all('documents_model/') to clear cached storefront queries
  6. Image deletion: Individual language images can be deleted via checkbox on the edit form without replacing them
  7. File cleanup on delete: delete_record() attempts to remove both the file and image from storage per language
  8. No authentication required: The /documents storefront page and REST GET endpoints are publicly accessible (guest auth)
  9. Write operations restricted: REST POST/DELETE require backend JWT with Admin, CMS, Marketing, or Media roles

  • CF-02 Product Detail -- product-specific file downloads use the separate product_downloads table and AdvDownloadsToProduct library
  • SY-23 MUI Translation Pattern -- documents_mui companion 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 uses HandlesUploadActions
  • 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).