Comprehensive guide for working with MyShift flat-file CMS - MVC architecture, repository patterns, authentication, and API design
Guide Claude Code when working with MyShift - a flat-file CMS for managing staff, shifts, time tracking, and leave requests using JSON files as the database. Built with PHP 8.3+ and a lightweight MVC architecture.
MyShift uses namespace `MyShift\` mapped to `app/` directory with modern repository pattern and legacy model layer coexisting during transition.
**Controllers** (`app/Controllers/`):
**Repositories** (`app/Repositories/`) - Modern data access layer with three strategies:
1. **SimpleJsonRepository**: Single-file storage (e.g., `timeoffs.json`, `roles.json`)
- Best for small collections (<1000 items), infrequent writes
- Entire file rewritten on each update
2. **IndexedJsonRepository**: Dual-file pattern (e.g., `staff/index.json` + `staff/{id}.json`)
- Index file contains lightweight summaries for fast listings
- Detail files contain full records loaded only when needed
- Best for large collections with frequent individual updates
3. **NestedJsonRepository**: Complex nested structures (e.g., shifts by date)
- Best for hierarchical data with time-based access patterns
**Services** (`app/Services/`):
**Value Objects / DTOs** (`app/ValueObjects/`):
**Models** (`app/Models/`) - Legacy layer being replaced by Repository pattern:
**JsonIO** (`app/Core/JsonIO.php`) provides atomic read/write operations:
**Data Structure** (`database/{storeId}/`):
**Gate Facade** - Preferred approach for permission checks:
```php
use MyShift\Services\Authorization\Gate;
// Check permission
if (Gate::allows('planning.read', $storeId)) { /* ... */ }
if (Gate::denies('staff.delete', $storeId, $staff)) { /* ... */ }
// Throw 403 if denied
Gate::authorize('planning.create', $storeId);
```
**AuthorizationService** - Core permission logic:
- Examples: `"planning.update.own"`, `"staff.delete.all"`
**Legacy Permission Function** - Being phased out:
**Permission Storage**:
1. **Update model**: `app/Models/Staff.php` - add field to `create()` and `update()` methods
2. **Update validation**: Check `app/Helpers/Validator.php` if validation needed
3. **Update controller**: `app/Controllers/StaffController.php` - ensure field is processed
4. **Update view**: `public/view/staff_detail.php` - add input/display element
5. **Update frontend**: `public/assets/js/app.js` - handle field in form submission
1. **Update role structure**: Add action to `database/{storeId}/roles.json`
```json
{
"domain_name": {
"action_name": {"all": true, "own": false}
}
}
```
2. **Add permission check in controller** (modern approach):
```php
use MyShift\Services\Authorization\Gate;
Gate::authorize('domain.action', $storeId); // Throws 403 if denied
```
3. **For scope-based permissions** (all vs own):
```php
// Check if user can update any staff OR their own
Gate::authorize('staff.update.all', $storeId, $staffRecord);
// Fallback to own if not allowed for all
if (Gate::denies('staff.update.all', $storeId)) {
Gate::authorize('staff.update.own', $storeId, $staffRecord);
}
```
**Choose the right repository type**:
```php
// SimpleJsonRepository - Single file for entire collection
// Use for: Roles, Timeoffs, Locks
$repo = new SimpleJsonRepository($io, 'database/1/timeoffs.json');
// IndexedJsonRepository - Index file + individual detail files
// Use for: Staff (large collections with frequent individual access)
$repo = new IndexedJsonRepository($io, 'database/1/staff/index.json', 'database/1/staff');
// NestedJsonRepository - Hierarchical nested structures
// Use for: Shifts (organized by date)
```
**Basic CRUD operations**:
```php
// Create
$id = $repository->create(['name' => 'John', 'email' => '[email protected]']);
// Read
$item = $repository->findById($id);
$allItems = $repository->findAll();
// Update
$repository->update($id, ['name' => 'Jane']);
// Delete
$repository->delete($id);
```
**Using DTOs**:
```php
// DTOs are immutable value objects
$staffDTO = new StaffDTO(
id: 1,
staffCode: 'EMP001',
displayName: 'John Doe',
// ... other required fields
);
// Access properties (read-only)
$name = $staffDTO->displayName;
```
Use `ActionLogger` for audit trail:
```php
$actionLogger->log(
$storeId,
current_user() ?? [],
'action_name',
'resource_type',
['context' => 'data']
);
```
`LocksController` + `Locks` model implements optimistic locking:
```
GET /api/stores/{id}/staff - List all
GET /api/stores/{id}/staff/{staffId} - Get one
POST /api/stores/{id}/staff - Create
PUT /api/stores/{id}/staff/{staffId} - Update
DELETE /api/stores/{id}/staff/{staffId} - Delete
```
Same pattern for: `shifts`, `timeoffs`
```
POST /api/login - Authenticate
POST /api/logout - End session
GET /api/me/permissions?store_id={id} - Get current user permissions
GET /api/stores/{id}/stats?start_date=X&end_date=Y - Analytics
POST /api/stores/{id}/locks - Acquire lock
DELETE /api/stores/{id}/locks - Release lock
POST /api/stores/{id}/staff/{id}/profile-image - Upload PNG (resized to 500x500)
POST /api/stores/{id}/store/upload_shifts - Import shifts from Excel
```
**`store.json` fields requiring special handling**:
1. **salary_types**: Completely replaced via `PUT /api/stores/{id}/store`
- Structure: `{id, name, code, start_time, end_time, fixed_amount, is_active}`
- Times must be `HH:MM` format
- Staff records map codes to amounts: `employment.salary = {"MORNING": 100.0}`
2. **opening_hours.default_schedule.shifts**: Ordered array of shift templates
- Structure: `{type, start, end}` (times in `HH:MM`)
- Frontend depends on this structure
3. **staffing.manager_id**: Must reference existing `staff/{id}.json`
- Server validates file existence on update
**Frontend selectors tied to backend**: Dynamic fields in `public/view/store.php` use `.salary-type-row` and `.opening-shift-row` classes parsed by `public/assets/js/app.js`. Do not change these selectors without updating JavaScript.
**Language files**: `app/lang/{code}.php` return associative arrays
To add a new language, create `app/lang/{code}.php` with translation keys.
```bash
composer install
php -S localhost:8000 -t public
```
1. **File Locking**: Always use `JsonIO` for reads/writes - never use `file_get_contents()`/`file_put_contents()` directly
2. **Validation**: All user inputs must pass through validation before persistence
3. **Backup Directory**: Exposed `database/` is a security risk; ensure `.htaccess` blocks direct access
4. **Time Format**: All times stored as `HH:MM` strings, dates as `Y-m-d`
5. **Manager Validation**: `store.json`'s `staffing.manager_id` must reference existing staff file
6. **Salary Types**: Always replace the entire `salary_types` array; partial updates not supported
7. **Profile Images**: Must be PNG format, auto-resized to 500x500, stored as `public/assets/images/profiles/{storeId}/{staff_code}.png`
Basic test framework in `app/Helpers/GlobalTester.php` - expose via route:
```php
Route::add('/test', function () {
require_once __DIR__ . '/../app/Helpers/GlobalTester.php';
$tester = new MyShift\Helpers\GlobalTester();
echo '<pre>' . json_encode($tester->run(), JSON_PRETTY_PRINT) . '</pre>';
});
```
Leave a review
No reviews yet. Be the first to review this skill!
# Download SKILL.md from killerskills.ai/api/skills/myshift-php-architecture-guide/raw