diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index f1dba4d..ca756f5 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,21 +1,33 @@ +FROM node:20-alpine AS frontend-build + +WORKDIR /build +COPY frontend/package.json frontend/package-lock.json* ./ +RUN npm install +COPY frontend/ ./ +RUN npm run build + +# --- + FROM php:8.0-fpm # Install system packages RUN apt-get update \ - && apt-get -y install git nano procps unzip default-mysql-client nginx supervisor + && apt-get -y install git nano procps unzip default-mysql-client nginx supervisor curl + +# Install Node.js 20 for frontend builds +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y nodejs # Install 'retry' script RUN curl https://raw.githubusercontent.com/kadwanev/retry/0b65e6b7f54ed36b492910470157e180bbcc3c84/retry -o /usr/local/bin/retry \ && chmod +x /usr/local/bin/retry # Install PHP extensions -RUN docker-php-ext-install mysqli pdo pdo_mysql \ +RUN docker-php-ext-install mysqli pdo pdo_mysql opcache \ && pecl install xdebug-3.1.1 \ && docker-php-ext-enable pdo_mysql xdebug -# Configure Xdebug -RUN echo "xdebug.mode=develop" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ - && echo "xdebug.start_with_request=yes" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini +# Xdebug and OPcache are configured at runtime by docker-start.sh based on APP_ENV # Install composer (pinned to 2.2 LTS for compatibility with symfony/flex v1.x) RUN curl -sSL https://getcomposer.org/download/2.2.24/composer.phar -o /usr/local/bin/composer \ @@ -24,14 +36,17 @@ RUN curl -sSL https://getcomposer.org/download/2.2.24/composer.phar -o /usr/loca # Install Symfony CLI RUN curl -sSL https://get.symfony.com/cli/installer | bash -s -- --install-dir=/usr/local/bin -# Create workspace directory (application code should be attached here using a bind mount) +# Create workspace directory RUN mkdir /srv/app WORKDIR /srv/app -# Install dependencies (this will populate the ~/.composer/cache for faster installs later) +# Install PHP dependencies COPY composer.json composer.lock ./ RUN composer install --no-scripts +# Copy Vue frontend build output +COPY --from=frontend-build /build/dist /srv/app/frontend/dist + # Add project specific nginx config COPY .devcontainer/nginx.conf /etc/nginx/nginx.conf RUN rm -f /etc/nginx/sites-enabled/default diff --git a/.devcontainer/docker-start.sh b/.devcontainer/docker-start.sh index f771061..71f633b 100755 --- a/.devcontainer/docker-start.sh +++ b/.devcontainer/docker-start.sh @@ -1,9 +1,45 @@ #!/bin/bash +APP_ENV="${APP_ENV:-prod}" +echo "==> Starting in $APP_ENV mode" + # Update nginx to match worker_processes to no. of cpu's procs=$(cat /proc/cpuinfo | grep processor | wc -l) sed -i -e "s/worker_processes 1/worker_processes $procs/" /etc/nginx/nginx.conf +# Configure PHP based on environment +if [ "$APP_ENV" = "prod" ]; then + echo "==> Enabling OPcache" + cat > /usr/local/etc/php/conf.d/opcache-env.ini < Disabling Xdebug" + rm -f /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini + + echo "==> Warming Symfony cache" + php /srv/app/bin/console cache:warmup --env=prod --no-debug 2>/dev/null || true +else + echo "==> OPcache disabled for development" + cat > /usr/local/etc/php/conf.d/opcache-env.ini < Xdebug in develop mode" + cat > /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini <; - # fastcgi_param DATABASE_URL "mysql://db_user:db_pass@host:3306/db_name"; - - # When you are using symlinks to link the document root to the - # current version of your application, you should pass the real - # application path instead of the path to the symlink to PHP - # FPM. - # Otherwise, PHP's OPcache may not properly detect changes to - # your PHP files (see https://github.com/zendtech/ZendOptimizerPlus/issues/126 - # for more information). - # Caveat: When PHP-FPM is hosted on a different machine from nginx - # $realpath_root may not resolve as you expect! In this case try using - # $document_root instead. - fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; - fastcgi_param DOCUMENT_ROOT $realpath_root; + # Vue.js static files + root /srv/app/frontend/dist; - # Prevents URIs that include the front controller. This will 404: - # http://domain.tld/index.php/some-path - # Remove the internal directive to allow URIs like this - internal; + location / { + try_files $uri $uri/ /index.html; } - # return 404 for all other php files not matching the front controller - # this prevents access to other php files you don't want to be accessible. location ~ \.php$ { return 404; } } -} \ No newline at end of file +} diff --git a/.devcontainer/php-fpm.conf b/.devcontainer/php-fpm.conf index af3e1f5..2e99cf9 100644 --- a/.devcontainer/php-fpm.conf +++ b/.devcontainer/php-fpm.conf @@ -37,6 +37,4 @@ pm.start_servers = 3 pm.min_spare_servers = 1 pm.max_spare_servers = 10 -; Enable Xdebug -php_value[xdebug.mode] = debug -php_value[xdebug.log] = /var/log/php-fpm/xdebug.log +; Xdebug is configured at runtime by docker-start.sh based on APP_ENV diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 7b20ff4..f4a8123 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -5,9 +5,18 @@ set -e # These commands will be run by VSCode inside the remote container on first startup. # -# Install dependencies +# Install PHP dependencies composer install +# Install and build Vue frontend +if command -v node &> /dev/null; then + cd /srv/app/frontend && npm install && npm run build + cd /srv/app +elif command -v npx &> /dev/null; then + cd /srv/app/frontend && npm install && npm run build + cd /srv/app +fi + # Wait for database to be available MYSQL_PWD=symfony retry -t 10 -m 1 -- mysqladmin -h database -P 3306 -u symfony status diff --git a/.dockerignore b/.dockerignore index b42be70..0f4c2d8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,4 @@ var/ vendor/ +frontend/node_modules/ +frontend/dist/ diff --git a/.env b/.env index e9f2b29..e18cd36 100644 --- a/.env +++ b/.env @@ -14,7 +14,7 @@ # https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration ###> symfony/framework-bundle ### -APP_ENV=dev +APP_ENV=prod APP_SECRET=59db887a5bf81a90b747f4e5002bd30f ###< symfony/framework-bundle ### diff --git a/README.md b/README.md index 41d6d0a..454d351 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,119 @@ -## Run locally using Docker +## Architecture -The application can be run on your local machine using [Docker](https://www.docker.com/products/docker-desktop). You only need Docker Desktop installed — PHP and Composer run inside the container. +This is a balance transfer application with a **Symfony 5 JSON API** backend and a **Vue.js 3** frontend. + +- **Backend:** Symfony 5.3 / PHP 8.0 — serves API endpoints at `/api/*` +- **Frontend:** Vue 3 + Vite + Tailwind CSS — single-page application served as static files +- **Database:** MySQL 8.0 +- **Web server:** Nginx serves the Vue app at `/` and proxies `/api` requests to PHP-FPM + +All services run in Docker. Port 8000 is the only port exposed. + +--- + +## Quick Start + +You only need [Docker Desktop](https://www.docker.com/products/docker-desktop) installed. 1. Clone the git repository to your local machine. +2. Start the application: + + **Windows:** + ``` + start.bat + ``` + + **Linux / Mac:** + ```sh + chmod +x start.sh + ./start.sh + ``` + + This builds and starts in **prod mode** by default (OPcache enabled, Xdebug off, Symfony cache warmed). + +3. Open the application at `http://localhost:8000` + +#### Development Mode + +To run with dev-friendly settings (OPcache off, Xdebug in develop mode, file changes reflect immediately): + +**Windows:** +``` +start.bat dev +``` + +**Linux / Mac:** +```sh +./start.sh dev +``` + +--- + +## Manual Setup (alternative) + +If you prefer not to use the start scripts: + +1. Build the Vue frontend: + ```sh + cd frontend + npm install + npm run build + cd .. + ``` 2. Start the containers: ```sh - docker compose up -d + docker compose up -d --build ``` -3. Run database migrations from the php container: +3. Run database migrations and load fixtures: ```sh docker exec -it balance-transfer-symfony-1 /bin/bash ``` then ```sh - php bin/console doctrine:migrations:migrate - ``` -4. Load database fixtures: - ```sh - php bin/console doctrine:fixtures:load + php bin/console doctrine:migrations:migrate --no-interaction + php bin/console doctrine:fixtures:load --no-interaction ``` -> **Note:** If using VS Code with the [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension, you can open this project with "Reopen in Container". The `post-create.sh` script will automatically install dependencies, run migrations, and load fixtures. +> **Note:** If using VS Code with the [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension, you can open this project with "Reopen in Container". The `post-create.sh` script will automatically install dependencies, build the frontend, run migrations, and load fixtures. -#### Application URL -When running locally, the development URL for your application will be: -``` -http://localhost:8000 +--- + +## Environment Modes + +| Setting | prod (default) | dev | +|-----------------|----------------------------------|----------------------------------| +| OPcache | Enabled, no file revalidation | Disabled | +| Xdebug | Completely removed | Develop mode (nice error pages) | +| Symfony cache | Pre-warmed at startup | Built on first request | +| Performance | Fast (sub-second responses) | Slower (convenience over speed) | + +The mode is controlled by the `APP_ENV` environment variable, which the start scripts set for you. + +--- + +## API Endpoints + +| Method | Endpoint | Description | +|--------|----------------------|--------------------------| +| GET | `/api/customers` | List all customers | +| GET | `/api/customers/{id}`| Get a single customer | +| GET | `/api/transfers` | List all transfers | +| POST | `/api/transfers` | Create a new transfer | + +**POST `/api/transfers`** expects JSON: +```json +{ + "senderId": 1, + "recipientId": 2, + "amount": 50.00 +} ``` -#### Database -You can access the application database using a MySQL client with the following parameters: +--- + +## Database + +You can access the application database using a MySQL client: | Parameter | Value | |---------------|---------------------| @@ -39,12 +123,37 @@ You can access the application database using a MySQL client with the following | Password | `symfony` | | Database Name | `app` | ----- +--- + +## Frontend Development + +The Vue.js frontend lives in `frontend/`. After making changes: + +**Option A — Rebuild inside the container:** +```sh +docker exec -it balance-transfer-symfony-1 bash -c "cd frontend && npm run build" +``` + +**Option B — Rebuild locally:** +```sh +cd frontend +npm run build +``` + +**Option C — Use Vite dev server for hot-reload (local Node.js required):** +```sh +cd frontend +npm run dev +``` +This starts a dev server at `http://localhost:5173` with API requests proxied to `http://localhost:8000`. + +--- ## Testing + [PHPUnit](https://phpunit.readthedocs.io/en/9.5/) unit tests, functional tests and application tests are located in the `tests/` directory. -To run the test suite, exec into the php container first: +To run the test suite, exec into the container first: ```sh docker exec -it balance-transfer-symfony-1 /bin/bash ``` diff --git a/config/services.yaml b/config/services.yaml index ef07b76..8cc9c83 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -23,3 +23,8 @@ services: # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones + + App\EventListener\CorsListener: + tags: + - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 256 } + - { name: kernel.event_listener, event: kernel.response, method: onKernelResponse } diff --git a/docker-compose.yaml b/docker-compose.yaml index 0fedb5c..4aab883 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -7,6 +7,8 @@ services: - '8000:8000' depends_on: - database + environment: + - APP_ENV=${APP_ENV:-prod} volumes: - .:/srv/app:cached diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..f06235c --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..9812e2d --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Balance Transfer + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..dabd3d2 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,23 @@ +{ + "name": "balance-transfer-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.6.0", + "vue": "^3.4.0", + "vue-router": "^4.2.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "autoprefixer": "^10.4.0", + "postcss": "^8.4.0", + "tailwindcss": "^3.4.0", + "vite": "^5.0.0" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..cfb12e0 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,39 @@ + + + diff --git a/frontend/src/api.js b/frontend/src/api.js new file mode 100644 index 0000000..d00e0f9 --- /dev/null +++ b/frontend/src/api.js @@ -0,0 +1,26 @@ +import axios from 'axios' + +const api = axios.create({ + baseURL: '/api', + headers: { + 'Content-Type': 'application/json', + }, +}) + +export default { + getCustomers() { + return api.get('/customers') + }, + + getCustomer(id) { + return api.get(`/customers/${id}`) + }, + + getTransfers() { + return api.get('/transfers') + }, + + createTransfer(senderId, recipientId, amount) { + return api.post('/transfers', { senderId, recipientId, amount }) + }, +} diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..54ce204 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,6 @@ +import { createApp } from 'vue' +import App from './App.vue' +import router from './router' +import './style.css' + +createApp(App).use(router).mount('#app') diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..4122ccf --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,15 @@ +import { createRouter, createWebHistory } from 'vue-router' +import Home from '../views/Home.vue' +import Transfer from '../views/Transfer.vue' + +const routes = [ + { path: '/', name: 'home', component: Home }, + { path: '/transfer/:id', name: 'transfer', component: Transfer }, +] + +const router = createRouter({ + history: createWebHistory(), + routes, +}) + +export default router diff --git a/frontend/src/store.js b/frontend/src/store.js new file mode 100644 index 0000000..07fdd72 --- /dev/null +++ b/frontend/src/store.js @@ -0,0 +1,40 @@ +import { reactive } from 'vue' +import api from './api' + +const store = reactive({ + customers: [], + transfers: [], + loaded: false, + loading: false, +}) + +export async function loadData(forceRefresh = false) { + if (store.loaded && !forceRefresh) return store + if (store.loading) return store + + store.loading = true + try { + const [customersRes, transfersRes] = await Promise.all([ + api.getCustomers(), + api.getTransfers(), + ]) + store.customers = customersRes.data + store.transfers = transfersRes.data + store.loaded = true + } catch (error) { + console.error('Failed to load data:', error) + } finally { + store.loading = false + } + return store +} + +export function getCustomer(id) { + return store.customers.find(c => c.id === id) +} + +export function refreshData() { + return loadData(true) +} + +export default store diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue new file mode 100644 index 0000000..b1695b4 --- /dev/null +++ b/frontend/src/views/Home.vue @@ -0,0 +1,77 @@ + + + diff --git a/frontend/src/views/Transfer.vue b/frontend/src/views/Transfer.vue new file mode 100644 index 0000000..7b5330e --- /dev/null +++ b/frontend/src/views/Transfer.vue @@ -0,0 +1,136 @@ + + + diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..4980bc3 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + './index.html', + './src/**/*.{vue,js,ts}', + ], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..e0de0ba --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + server: { + host: '0.0.0.0', + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + }, + }, + }, + build: { + outDir: 'dist', + }, +}) diff --git a/src/Controller/Api/CustomerController.php b/src/Controller/Api/CustomerController.php new file mode 100644 index 0000000..f9591ef --- /dev/null +++ b/src/Controller/Api/CustomerController.php @@ -0,0 +1,52 @@ +customerRepository = $customerRepository; + } + + #[Route('/customers', name: 'api_customers', methods: ['GET'])] + public function index(): JsonResponse + { + $customers = $this->customerRepository->findAll(); + + $data = array_map(function (Customer $customer) { + return [ + 'id' => $customer->getId(), + 'name' => $customer->getName(), + 'balance' => $customer->getBalance(), + ]; + }, $customers); + + return $this->json($data); + } + + #[Route('/customers/{id}', name: 'api_customer_show', methods: ['GET'])] + public function show(int $id): JsonResponse + { + $customer = $this->customerRepository->find($id); + + if (!$customer) { + return $this->json(['error' => 'Customer not found'], 404); + } + + return $this->json([ + 'id' => $customer->getId(), + 'name' => $customer->getName(), + 'balance' => $customer->getBalance(), + ]); + } +} diff --git a/src/Controller/Api/TransferController.php b/src/Controller/Api/TransferController.php new file mode 100644 index 0000000..81df3c0 --- /dev/null +++ b/src/Controller/Api/TransferController.php @@ -0,0 +1,168 @@ +em = $entityManager; + $this->customerRepository = $customerRepository; + $this->transferRepository = $transferRepository; + } + + #[Route('/transfers', name: 'api_transfers', methods: ['GET'])] + public function index(): JsonResponse + { + $transfers = $this->transferRepository->findAll(); + + $data = array_map(function (Transfer $transfer) { + return [ + 'id' => $transfer->getId(), + 'amount' => $transfer->getAmount(), + 'date' => $transfer->getDate()->format('Y-m-d H:i:s'), + 'customerFrom' => $transfer->getCustomerFrom(), + 'customerTo' => $transfer->getCustomerTo(), + ]; + }, $transfers); + + return $this->json($data); + } + + #[Route('/transfers', name: 'api_transfer_create', methods: ['POST'])] + public function create(Request $request): JsonResponse + { + $data = json_decode($request->getContent(), true); + + $senderId = $data['senderId'] ?? null; + $recipientId = $data['recipientId'] ?? null; + $amount = $data['amount'] ?? null; + + // Validate required fields + if (!$senderId || !$recipientId || $amount === null) { + return $this->json([ + 'type' => 'error', + 'message' => 'Missing required fields: senderId, recipientId, amount', + ], 400); + } + + // Validate amount is numeric + if (!is_numeric($amount)) { + return $this->json([ + 'type' => 'error', + 'message' => 'Only numeric values are allowed!', + ], 422); + } + + $amount = (float) $amount; + + // Validate amount is not empty/zero + if ($amount == 0) { + return $this->json([ + 'type' => 'error', + 'message' => 'The transfer was submitted without an amount entered!', + ], 422); + } + + // Validate amount is not negative + if ($amount < 0) { + return $this->json([ + 'type' => 'error', + 'message' => 'Transfers may not be negative values', + ], 422); + } + + // Validate amount does not exceed $500 + if ($amount > 500) { + return $this->json([ + 'type' => 'error', + 'message' => 'Transfers may not exceed $500', + ], 422); + } + + /** @var Customer|null $sender */ + $sender = $this->customerRepository->find($senderId); + if (!$sender) { + return $this->json([ + 'type' => 'error', + 'message' => 'Sender not found', + ], 404); + } + + /** @var Customer|null $recipient */ + $recipient = $this->customerRepository->find($recipientId); + if (!$recipient) { + return $this->json([ + 'type' => 'error', + 'message' => 'There was an error attempting to retrieve the recipient of the transfer. Please try again.', + ], 404); + } + + // Validate sender has sufficient balance + if ((float) $sender->getBalance() <= 0) { + return $this->json([ + 'type' => 'error', + 'message' => 'Transfers may not be made if balance is zero or in overdraft', + ], 422); + } + + if ($amount > (float) $sender->getBalance()) { + return $this->json([ + 'type' => 'error', + 'message' => 'Transfer amount must be less or equal to the balance of $' . + $sender->getBalance() . ' and may not exceed $500', + ], 422); + } + + // Prevent self-transfer + if ($senderId === $recipientId) { + return $this->json([ + 'type' => 'error', + 'message' => 'Cannot transfer to yourself', + ], 422); + } + + // Process the transfer + $senderNewBalance = (float) $sender->getBalance() - $amount; + $sender->setBalance((string) $senderNewBalance); + $this->em->persist($sender); + + $recipientNewBalance = (float) $recipient->getBalance() + $amount; + $recipient->setBalance((string) $recipientNewBalance); + $this->em->persist($recipient); + + $transfer = new Transfer(); + $transfer->setDate(new \DateTime()); + $transfer->setAmount($amount); + $transfer->setCustomerFrom($sender->getName()); + $transfer->setCustomerTo($recipient->getName()); + $this->em->persist($transfer); + + $this->em->flush(); + + return $this->json([ + 'type' => 'success', + 'message' => 'Transferred $' . number_format($amount, 2) . + ' from ' . $sender->getName() . + ' to ' . $recipient->getName(), + ], 201); + } +} diff --git a/src/EventListener/CorsListener.php b/src/EventListener/CorsListener.php new file mode 100644 index 0000000..a6fef22 --- /dev/null +++ b/src/EventListener/CorsListener.php @@ -0,0 +1,40 @@ +isMainRequest()) { + return; + } + + $request = $event->getRequest(); + + if ($request->getMethod() === 'OPTIONS') { + $response = new Response('', 204); + $response->headers->set('Access-Control-Allow-Origin', '*'); + $response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + $response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + $response->headers->set('Access-Control-Max-Age', '3600'); + $event->setResponse($response); + } + } + + public function onKernelResponse(ResponseEvent $event): void + { + if (!$event->isMainRequest()) { + return; + } + + $response = $event->getResponse(); + $response->headers->set('Access-Control-Allow-Origin', '*'); + $response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + $response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); + } +} diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..1a2ecac --- /dev/null +++ b/start.bat @@ -0,0 +1,23 @@ +@echo off +setlocal + +set APP_ENV=prod +if /i "%~1"=="dev" set APP_ENV=dev + +echo Starting in %APP_ENV% mode... + +docker compose build symfony +docker compose up -d --force-recreate symfony + +echo. +echo Application running at http://localhost:8000 (%APP_ENV% mode) +if /i "%APP_ENV%"=="dev" ( + echo - OPcache disabled, file changes reflect immediately + echo - Xdebug in develop mode +) +if /i "%APP_ENV%"=="prod" ( + echo - OPcache enabled for fast responses + echo - Xdebug disabled +) + +endlocal diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..82aa95e --- /dev/null +++ b/start.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +MODE="${1:-prod}" + +echo "Starting in $MODE mode..." + +APP_ENV="$MODE" docker compose build symfony +APP_ENV="$MODE" docker compose up -d --force-recreate symfony + +echo "" +echo "Application running at http://localhost:8000 ($MODE mode)" +if [ "$MODE" = "dev" ]; then + echo " - OPcache disabled, file changes reflect immediately" + echo " - Xdebug in develop mode" +else + echo " - OPcache enabled for fast responses" + echo " - Xdebug disabled" +fi