Backend Assignment for Creatim
➜ creotify git:(main) ✗ composer test
PASS Tests\Smoke\HealthEndpointSmokeTest
✓ it responds with 200 OK for /api/health 0.07s
PASS Tests\Smoke\KernelSmokeTest
✓ it boots the Symfony kernel successfully 0.01s
PASS Tests\Smoke\RouteSmokeTest
✓ it all public routes respond without error 0.04s
PASS Tests\Smoke\UnimplementedClassesTest
✓ it sends SMS with ProviderA 0.01s
✓ it sends SMS with ProviderB 0.01s
PASS Tests\Unit\Entity\CustomerTest
✓ it adds and removes orders 0.01s
PASS Tests\Unit\Entity\OrderTest
✓ it generates an order number automatically
✓ it adds and removes order items
✓ it updates total price when items are added or removed
PASS Tests\Unit\Service\OrderServiceTest
✓ it creates a valid order successfully 0.01s
✓ it throws on duplicate article 0.01s
✓ it throws on duplicate subscription 0.01s
PASS Tests\Unit\Service\SMS\RateLimitedSmsManagerTest
✓ it uses primary provider until rate limit is reached 0.01s
✓ it switches to fallback provider after 5 messages
✓ it resets counter after a minute and uses primary again
DEPR Tests\Feature\ApiOrderTest
! it creates an order successfully → Passing $sequence as a Sequence object to
Doctrine\DBAL\Platforms\AbstractPlatform::getDropSequenceSQL is deprecated. Pass it as a quoted
name instead. (AbstractPlatform.php:2434 called by… 0.14s
✓ it prevents duplicate subscription purchase 0.11s
✓ it lists all orders for a customer 0.14s
✓ it retrieves a specific order by orderNumber 0.11s
✓ it cancels (deletes) an order successfully 0.13s
PASS Tests\Integration\DataFixtures\AppFixturesTest
✓ it executes AppFixtures load() successfully 0.11s
PASS Tests\Integration\Repository\OrderRepositoryTest
✓ it returns bool for customerHasSubscription 0.08s
✓ it returns bool for customerHasArticle 0.07s
──────────────────────────────────────────────────────────────────────────────────────────────────
DEPRECATED Tests\Feature\ApiOrderTest > it creates an order successfully
Passing $sequence as a Sequence object to
Doctrine\DBAL\Platforms\AbstractPlatform::getDropSequenceSQL is deprecated. Pass it as a quoted
name instead. (AbstractPlatform.php:2434 called by PostgreSQLPlatform.php:808,
https://github.com/doctrine/dbal/issues/4798, package doctrine/dbal)
at vendor/doctrine/deprecations/src/Deprecation.php:208
204▕ $link,
205▕ $package
206▕ );
207▕
➜ 208▕ @trigger_error($message, E_USER_DEPRECATED);
209▕ }
210▕
211▕ /**
212▕ * A non-local-aware version of PHPs basename function.
+8 vendor frames
9 tests/Traits/RefreshDatabase.php:33
10 tests/Feature/ApiOrderTest.php:14
Tests: 1 deprecated, 22 passed (57 assertions)
Duration: 1.17s
Controller/Api/HealthController ........................................................... 100.0%
Controller/Api/OrderController ............................... 33, 58..59, 68..69, 99, 119 / 88.9%
Entity/Article ....................................................... 46, 58, 82, 94..101 / 64.7%
Entity/Customer ................................................................... 39..63 / 63.2%
Entity/Order ................................................ 51, 61..63, 85..87, 109..111 / 80.6%
Entity/OrderItem .............................................. 42, 64..66, 76..78, 88..90 / 58.8%
Entity/SubscriptionPackage ........................................... 46, 58, 82, 94..101 / 64.7%
Kernel .................................................................................... 100.0%
Repository/ArticleRepository ...................................................... 31..35 / 20.0%
Repository/Contract/ArticleRepositoryInterface ............................................ 100.0%
Repository/Contract/CustomerRepositoryInterface ........................................... 100.0%
Repository/Contract/OrderItemRepositoryInterface .......................................... 100.0%
Repository/Contract/OrderRepositoryInterface .............................................. 100.0%
Repository/Contract/RepositoryInterface ................................................... 100.0%
Repository/Contract/SubscriptionPackageRepositoryInterface ................................ 100.0%
Repository/CustomerRepository ............................................................. 100.0%
Repository/OrderItemRepository .................................................... 31..38 / 12.5%
Repository/OrderRepository ................................................................ 100.0%
Repository/SubscriptionPackageRepository .......................................... 31..35 / 20.0%
Service/OrderService ...................................................................... 100.0%
Service/SMS/Contract/SmsManagerInterface .................................................. 100.0%
Service/SMS/Contract/SmsProviderInterface ................................................. 100.0%
Service/SMS/RateLimitedSmsManager ......................................................... 100.0%
Service/SMS/SmsProviderA .................................................................. 100.0%
Service/SMS/SmsProviderB .................................................................. 100.0%
──────────────────────────────────────────────────────────────────────────────────────────────────
Total: 78.5 %
FAIL Code coverage below expected 100.0 %, currently 78.5 %.
Script XDEBUG_MODE=coverage php vendor/bin/pest --coverage --min=100 handling the test event
returned with error code 1Note: Deprecation warnings are expected due to Doctrine DBAL 4 compatibility; they do not indicate test failures.
- Symfony 7.3.4
- PHP 8.4
- Pest v4
- Ray
- PostgreSQL (local via Docker)
- Create new Symfony project via
symfony new creotify --webapp - Configure project with Laravel Herd & HTTPS (https://creotify.test)
- Add Docker setup (PHP-FPM + Nginx + Postgres + Mailpit)
- Extend Dockerfile with Xdebug + Ray pre-configuration
- Update
.envwith Postgres credentials - Require optional dev dependencies:
composer require pestphp/pest --dev --with-all-dependencies composer require --dev doctrine/doctrine-fixtures-bundle fakerphp/faker friendsofphp/php-cs-fixer composer require spatie/ray
- Configure CS Fixer
- Initialize Pest:
./vendor/bin/pest --init
- Add PHPStan
- Add composer scripts:
See composer.json
"scripts": { "lint": "php vendor/bin/php-cs-fixer fix --dry-run --diff", "lint:fix": "php vendor/bin/php-cs-fixer fix --verbose", "test": "XDEBUG_MODE=coverage php vendor/bin/pest --coverage --min=100", "test:smoke": "php vendor/bin/pest --group=smoke", "test:unit": "php vendor/bin/pest --group=unit", "test:feature": "php vendor/bin/pest --group=feature", "test:integration": "php vendor/bin/pest --group=integration", }
"scripts"for more aliases. - Ensure all tests pass with
composer testand 100% coverage - Add authentication
- Create entities:
-
Article -
SubscriptionPackage -
Customer -
Order -
OrderItem
-
- Generate and run Doctrine migrations
- Add Fixtures for sample data using Faker
- Use enums for item types and order status
- Implement validation for unique item/subscription per customer
- Implement order total calculation
- Implement error handling (custom exceptions)
- Implement SMS service management (two services, 5 SMS/min limit + fallback)
- Log SMS events (Spatie Ray / Monolog)
- Implement soft-deletion for all entities
- Order endpoints:
- Create order (purchase flow)
- List orders
- Cancel order (handle deletion logic)
- Implement Symfony Forms for validation and clean DTO mapping
- Write smoke tests for:
- Kernel
- Health endpoint
- All public routes
- Write unit tests for:
- OrderService (purchase logic)
- SMSService (fallback logic)
- Write functional tests for:
- Order creation endpoint
- Duplicate purchase restriction
- Enforce 100% coverage via composer script
- Configure Ray to show test output (
ray()->showQueries()->showEvents();)
- Add
Dockerfile+docker-compose.yml - Include PHP 8.4, Composer, Postgres 18, Nginx
- Add setup instructions to README
- Add Postman/cURL examples for all API endpoints
- Document SMS Service usage (interface + example)
- Add note on test execution (
composer test)
- Ensure code comments in complex logic
- Apply DRY and MVC clean-up
- Format code via PHP-CS-Fixer
- Commit history clean and semantic
We support two local setups - pick one (don't run both on the same ports):
Herd (macOS, Windows) - uses Docker only for Postgres Docker (Linux, macOS, Windows) - Nginx + PHP-FPM + Postgres in containers
Prerequisites:
- Laravel Herd (acts as local PHP server with HTTPS)
- Parent folder of project root is added to Herd Paths (in Herd settings)
- Docker Desktop 4.x (only for Postgres)
Quick start:
# 1) Clone
git clone https://github.com/mathmul/creotify.git && cd creotify
# 2) Create .env.local and copy DATABASE_URL for Herd from .env
touch .env.local
# 3) Install dependencies
herd composer install
# 4) Start postgres docker
docker compose --profile dbonly up -d
# To stop: docker compose --profile dbonly down
# 5) Create the database
php bin/console doctrine:database:create
# 6) Run migrations
composer migrate
# 7) Load fixtures
composer dev:db:seed
# 8) Serve via Herd (as <folder-name>.test)
herd init
# 9) Enable HTTPS
herd secure
# To disable HTTPS: herd unsecure
# 10) (Optional) Health + tests
curl https://creotify.test/api/health && echo
./vendor/bin/pest
# or with coverage
XDEBUG_MODE=coverage ./vendor/bin/pest --coverage --min=100Prerequisites
- Docker Desktop 4.x
- Ports 8080 (web) and 5432 (Postgres) free
Quick start:
# 1) Clone
git clone git@github.com:mathmul/creotify.git && cd creotify
# 2) (Optional) Create .env.local for overrides
touch .env.local
# 3) Start full docker stack
docker compose --profile full up -d
# To stop: docker compose --profile full down
# 4) (Optional) Check logs
docker compose logs -f app
# 5) Run migrations
docker compose exec app composer migrate
# 6) Load fixtures
docker compose exec app composer dev:db:seed
# 7) (Optional) Health + tests with coverage
curl http://localhost:8080/api/health && echo
docker compose exec app composer testNote: All
composer <command>commands mentioned below can be run in the container withdocker compose exec app composer <command>.
When an order is successfully placed, an SMS confirmation is sent to the customer.
The system uses the RateLimitedSmsManager, which delegates to SmsProviderA until
five messages per minute are reached, then automatically switches to SmsProviderB.
Example usage:
use App\Service\SMS\RateLimitedSmsManager;
use App\Service\SMS\SmsProviderA;
use App\Service\SMS\SmsProviderB;
$manager = new RateLimitedSmsManager(new SmsProviderA(), new SmsProviderB());
$manager->sendSMS('+38612345678', 'Your order was successfully placed!');In development, messages are logged via Spatie Ray and visible in the Ray desktop app.
Run all tests (smoke, unit, integration, and feature) with coverage:
composer testTo run a specific group:
composer test:smoke
composer test:unit
composer test:integration
composer test:featureCode coverage target: 100% (adjust in composer.json ifwhen necessary)
We follow a Test-Driven Development (TDD) approach:
- Write a feature test
- Run the test and verify it fails (red)
- Implement the minimal code to pass
- Run the test and verify it passes (green)
- Refactor if needed
- Repeat
We use Pest instead of PHPUnit for cleaner, expressive tests. All test files reside in tests/ directory, except ./phpunit.dist.xml on project root.
Symfony’s native test classes (WebTestCase, KernelTestCase) work perfectly with Pest.
Symfony provides two main testing layers:
| Type | Base class | Purpose |
|---|---|---|
| Unit tests | KernelTestCase |
Tests that use the service container or Doctrine |
| Functional tests | WebTestCase |
Simulate HTTP requests using Symfony’s HttpKernelBrowser |
We configured Pest to use these automatically in tests/Pest.php:
<?php
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
pest()->extends(KernelTestCase::class)->in('Smoke')->group('smoke');
pest()->extends(KernelTestCase::class)->in('Unit')->group('unit');
pest()->extends(WebTestCase::class)->in('Feature')->group('feature');
pest()->extends(KernelTestCase::class)->in('Integration')->group('integration');We initialized Pest:
./vendor/bin/pest --initPreferably we create new Pest tests manually under tests/{Smoke,Unit,Feature,Integration} directories, but we can create new PHPUnit tests using the built-in Symfony Maker command and later convert them to Pest:
php bin/console make:test WebTestCase Feature\\ExampleApiTest # Functional test
php bin/console make:test KernelTestCase Unit\\ExampleUnitTest --unit # Unit testExample test:
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
it('returns 200 OK for /api/health', function () {
/** @var KernelBrowser $client */
$client = static::createClient();
$client->request('GET', '/api/health');
expect($client->getResponse()->getStatusCode())->toBe(200);
expect($client->getResponse()->getContent())->toBeJson();
});Run tests:
# All tests + coverage
composer test
# or one of test groups w/o coverage
composer test:smoke
composer test:unit
composer test:feature
composer test:integrationWe use PHP-CS-Fixer for automatic code formatting following the official Symfony and PSR-12 standards. It was already installed as a dev dependency with:
composer require --dev friendsofphp/php-cs-fixerBut if dependencies conflict, you can use the shim instead:
composer require --dev php-cs-fixer/shimConfiguration file: .php-cs-fixer.dist.php
You can lint and auto-fix the code manually:
# Dry run
composer lint
# Fix
composer lint:fixMake sure docker container is running - see Getting started.
After creating/updating models, create new migration files:
composer make:migrationsThen sync migrations to database:
composer migrate