diff --git a/.env.example b/.env.example index ea952db..150d08f 100644 --- a/.env.example +++ b/.env.example @@ -13,4 +13,11 @@ ORDER_DB_PORT=5434 NOTIFICATION_DB_PORT=5435 # Application Secrets -APP_SECRET=your_microservices_secret_here \ No newline at end of file +APP_SECRET=your_microservices_secret_here + + +# RabbitMQ +RABBITMQ_USER=guest +RABBITMQ_PASSWORD=guest +RABBITMQ_HOST=rabbitmq +RABBITMQ_PORT=5672 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index f4d1fdd..1ffa17d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,21 @@ services: - order-service - notification-service + rabbitmq: + image: rabbitmq:3-management + container_name: rabbitmq + environment: + RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-guest} + RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:-guest} + ports: + - "15672:15672" # Web UI + - "5672:5672" # AMQP port + healthcheck: + test: [ "CMD", "rabbitmqctl", "status" ] + interval: 30s + timeout: 10s + retries: 3 + user-service: build: ./services/user-service container_name: user-service @@ -20,10 +35,13 @@ services: - ./services/user-service/.env volumes: - ./services/user-service:/var/www/html + - ./shared:/var/www/html/shared environment: - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@user-db:5432/${USER_DB_NAME}?serverVersion=15&charset=utf8 + - MESSENGER_TRANSPORT_DSN=amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672/%2f/messages depends_on: - user-db + - rabbitmq expose: - "9000" @@ -32,9 +50,10 @@ services: container_name: order-service env_file: - .env - - ./services/user-service/.env + - ./services/order-service/.env volumes: - ./services/order-service:/var/www/html + - ./shared:/var/www/html/shared environment: - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@order-db:5432/${ORDER_DB_NAME}?serverVersion=15&charset=utf8 depends_on: @@ -47,13 +66,16 @@ services: container_name: notification-service env_file: - .env - - ./services/user-service/.env + - ./services/notification-service/.env volumes: - ./services/notification-service:/var/www/html + - ./shared:/var/www/html/shared environment: - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@notification-db:5432/${NOTIFICATION_DB_NAME}?serverVersion=15&charset=utf8 + - MESSENGER_TRANSPORT_DSN=amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@rabbitmq:5672/%2f/messages depends_on: - notification-db + - rabbitmq expose: - "9000" diff --git a/makefile b/makefile index 9c6ff0d..fdabd61 100644 --- a/makefile +++ b/makefile @@ -69,6 +69,29 @@ lint: fi; \ done +## Install dependencies for a specific service inside container +composer-install: + @if [ -z "$(service)" ]; then \ + echo "❌ Usage: make composer-install service="; \ + echo "Example: make composer-install service=user-service"; \ + exit 1; \ + fi + @echo "📦 Installing dependencies for $(service)..." + @docker compose exec $(service) composer install --no-interaction --prefer-dist --optimize-autoloader + @echo "✅ Dependencies installed for $(service)." + +## Update dependencies for a specific service inside container +composer-update: + @if [ -z "$(service)" ]; then \ + echo "❌ Usage: make composer-update service="; \ + echo "Example: make composer-update service=order-service"; \ + exit 1; \ + fi + @echo "📦 Updating dependencies for $(service)..." + @docker compose exec $(service) composer update --no-interaction --prefer-dist --optimize-autoloader + @echo "✅ Dependencies updated for $(service)." + + ## Show this help help: @echo "Available commands:" @@ -81,4 +104,4 @@ help: @echo " make update - Update dependencies in all services" @echo " make test - Run test for services" @echo " make lint - Check services with PhpStan" - @echo " make help - Show this help" \ No newline at end of file + @echo " make help - Show this help" diff --git a/services/notification-service/Dockerfile b/services/notification-service/Dockerfile index a85f11e..bffbd58 100644 --- a/services/notification-service/Dockerfile +++ b/services/notification-service/Dockerfile @@ -1,10 +1,15 @@ - FROM php:8.3-fpm -# Install dependencies -RUN apt-get update && apt-get install -y zip unzip git libpq-dev \ +# Install system dependencies +RUN apt-get update && apt-get install -y \ + zip unzip git libpq-dev \ && docker-php-ext-install pdo pdo_pgsql +# Install RabbitMQ dependencies and AMQP extension +RUN apt-get update && apt-get install -y librabbitmq-dev \ + && pecl install amqp \ + && docker-php-ext-enable amqp + # Install composer COPY --from=composer:2 /usr/bin/composer /usr/bin/composer @@ -12,6 +17,7 @@ WORKDIR /var/www/html # Copy app COPY . . + # Install PHP deps RUN composer install --no-interaction --prefer-dist --no-scripts || true @@ -20,4 +26,3 @@ RUN chown -R www-data:www-data /var/www/html EXPOSE 9000 CMD ["php-fpm", "-F", "-R"] - diff --git a/services/notification-service/composer.json b/services/notification-service/composer.json index de3902b..e23c6a9 100644 --- a/services/notification-service/composer.json +++ b/services/notification-service/composer.json @@ -9,10 +9,13 @@ "php": ">=8.2", "ext-ctype": "*", "ext-iconv": "*", + "microservices/shared-contracts": "1.*", + "symfony/amqp-messenger": "7.3.*", "symfony/console": "7.3.*", "symfony/dotenv": "7.3.*", "symfony/flex": "^2.8.2", "symfony/framework-bundle": "7.3.*", + "symfony/messenger": "7.3.*", "symfony/runtime": "7.3.*", "symfony/yaml": "7.3.*" }, @@ -64,7 +67,7 @@ "repositories": [ { "type": "path", - "url": "../../shared", + "url": "./shared", "options": { "symlink": false } diff --git a/services/notification-service/config/packages/messenger.yaml b/services/notification-service/config/packages/messenger.yaml new file mode 100644 index 0000000..1438694 --- /dev/null +++ b/services/notification-service/config/packages/messenger.yaml @@ -0,0 +1,31 @@ +framework: + messenger: + # Uncomment this (and the failed transport below) to send failed messages to this transport for later handling. + # failure_transport: failed + + transports: + # https://symfony.com/doc/current/messenger.html#transport-configuration + # async: '%env(MESSENGER_TRANSPORT_DSN)%' + # failed: 'doctrine://default?queue_name=failed' + # sync: 'sync://' + async: + dsn: '%env(MESSENGER_TRANSPORT_DSN)%' + options: + exchange: + name: user_events + type: direct + queues: + user_registered: ~ + + routing: + # Route your messages to the transports + # 'App\Message\YourMessage': async + 'Microservices\SharedEvents\UserRegisteredEvent': async + +# when@test: +# framework: +# messenger: +# transports: +# # replace with your transport name here (e.g., my_transport: 'in-memory://') +# # For more Messenger testing tools, see https://github.com/zenstruck/messenger-test +# async: 'in-memory://' diff --git a/services/notification-service/src/MessageHandler/UserRegisteredEventHandler.php b/services/notification-service/src/MessageHandler/UserRegisteredEventHandler.php new file mode 100644 index 0000000..4a58a79 --- /dev/null +++ b/services/notification-service/src/MessageHandler/UserRegisteredEventHandler.php @@ -0,0 +1,65 @@ +logger->info('🎉 User registered event received', [ + 'eventId' => $event->eventId, + 'userId' => $event->userId, + 'email' => $event->email, + 'firstName' => $event->firstName, + 'lastName' => $event->lastName, + ]); + + try { + // Имитация отправки приветственного email + $this->sendWelcomeEmail($event->email, $event->firstName, $event->lastName); + + // Имитация отправки push-уведомления + $this->sendPushNotification($event->userId, $event->firstName); + + $this->logger->info('✅ Notifications sent successfully for user', [ + 'userId' => $event->userId, + 'email' => $event->email, + ]); + + } catch (\Exception $e) { + $this->logger->error('❌ Failed to send notifications', [ + 'userId' => $event->userId, + 'error' => $e->getMessage(), + ]); + } + } + + private function sendWelcomeEmail(string $email, string $firstName, string $lastName): void + { + // Имитация отправки email + $this->logger->info("📧 Sending welcome email to: {$email}"); + $this->logger->info("👋 Dear {$firstName} {$lastName}, welcome to our platform!"); + + // Заглушка для реальной отправки email + // sleep(1); + } + + private function sendPushNotification(string $userId, string $firstName): void + { + // Имитация отправки push-уведомления + $this->logger->info("📱 Sending push notification to user: {$userId}"); + $this->logger->info("🔔 Welcome {$firstName}! Your account has been created successfully."); + + // Заглушка для реальной отправки push + // sleep(1); + } +} diff --git a/services/notification-service/symfony.lock b/services/notification-service/symfony.lock index e115c14..640e94c 100644 --- a/services/notification-service/symfony.lock +++ b/services/notification-service/symfony.lock @@ -71,6 +71,18 @@ ".editorconfig" ] }, + "symfony/messenger": { + "version": "7.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.0", + "ref": "ba1ac4e919baba5644d31b57a3284d6ba12d52ee" + }, + "files": [ + "config/packages/messenger.yaml" + ] + }, "symfony/routing": { "version": "7.3", "recipe": { diff --git a/services/order-service/composer.json b/services/order-service/composer.json index e66da38..ada1f24 100644 --- a/services/order-service/composer.json +++ b/services/order-service/composer.json @@ -69,7 +69,7 @@ "repositories": [ { "type": "path", - "url": "../../shared", + "url": "./shared", "options": { "symlink": false } diff --git a/services/user-service/Dockerfile b/services/user-service/Dockerfile index dd1d937..bffbd58 100644 --- a/services/user-service/Dockerfile +++ b/services/user-service/Dockerfile @@ -1,10 +1,15 @@ - FROM php:8.3-fpm -# Install dependencies -RUN apt-get update && apt-get install -y zip unzip git libpq-dev \ +# Install system dependencies +RUN apt-get update && apt-get install -y \ + zip unzip git libpq-dev \ && docker-php-ext-install pdo pdo_pgsql +# Install RabbitMQ dependencies and AMQP extension +RUN apt-get update && apt-get install -y librabbitmq-dev \ + && pecl install amqp \ + && docker-php-ext-enable amqp + # Install composer COPY --from=composer:2 /usr/bin/composer /usr/bin/composer @@ -21,4 +26,3 @@ RUN chown -R www-data:www-data /var/www/html EXPOSE 9000 CMD ["php-fpm", "-F", "-R"] - diff --git a/services/user-service/composer.json b/services/user-service/composer.json index fb3fc32..86a891a 100644 --- a/services/user-service/composer.json +++ b/services/user-service/composer.json @@ -14,10 +14,12 @@ "doctrine/doctrine-migrations-bundle": "^3.5", "doctrine/orm": "^3.5", "microservices/shared-contracts": "1.*", + "symfony/amqp-messenger": "7.3.*", "symfony/console": "7.3.*", "symfony/dotenv": "7.3.*", "symfony/flex": "^2.8.2", "symfony/framework-bundle": "7.3.*", + "symfony/messenger": "7.3.*", "symfony/runtime": "7.3.*", "symfony/security-bundle": "7.3.*", "symfony/yaml": "7.3.*" @@ -73,7 +75,7 @@ "repositories": [ { "type": "path", - "url": "../../shared", + "url": "./shared", "options": { "symlink": false } diff --git a/services/user-service/config/packages/messenger.yaml b/services/user-service/config/packages/messenger.yaml new file mode 100644 index 0000000..1438694 --- /dev/null +++ b/services/user-service/config/packages/messenger.yaml @@ -0,0 +1,31 @@ +framework: + messenger: + # Uncomment this (and the failed transport below) to send failed messages to this transport for later handling. + # failure_transport: failed + + transports: + # https://symfony.com/doc/current/messenger.html#transport-configuration + # async: '%env(MESSENGER_TRANSPORT_DSN)%' + # failed: 'doctrine://default?queue_name=failed' + # sync: 'sync://' + async: + dsn: '%env(MESSENGER_TRANSPORT_DSN)%' + options: + exchange: + name: user_events + type: direct + queues: + user_registered: ~ + + routing: + # Route your messages to the transports + # 'App\Message\YourMessage': async + 'Microservices\SharedEvents\UserRegisteredEvent': async + +# when@test: +# framework: +# messenger: +# transports: +# # replace with your transport name here (e.g., my_transport: 'in-memory://') +# # For more Messenger testing tools, see https://github.com/zenstruck/messenger-test +# async: 'in-memory://' diff --git a/services/user-service/migrations/Version20251013220532.php b/services/user-service/migrations/Version20251013220532.php new file mode 100644 index 0000000..ad2c600 --- /dev/null +++ b/services/user-service/migrations/Version20251013220532.php @@ -0,0 +1,35 @@ +addSql('CREATE TABLE users (id SERIAL NOT NULL, email VARCHAR(180) NOT NULL, password VARCHAR(255) NOT NULL, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, status VARCHAR(20) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9E7927C74 ON users (email)'); + $this->addSql('COMMENT ON COLUMN users.created_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN users.updated_at IS \'(DC2Type:datetime_immutable)\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP TABLE users'); + } +} diff --git a/services/user-service/migrations/Version20251013230043.php b/services/user-service/migrations/Version20251013230043.php new file mode 100644 index 0000000..1d520ff --- /dev/null +++ b/services/user-service/migrations/Version20251013230043.php @@ -0,0 +1,35 @@ +addSql('CREATE TABLE users (id SERIAL NOT NULL, email VARCHAR(180) NOT NULL, password VARCHAR(255) NOT NULL, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, status VARCHAR(20) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9E7927C74 ON users (email)'); + $this->addSql('COMMENT ON COLUMN users.created_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN users.updated_at IS \'(DC2Type:datetime_immutable)\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP TABLE users'); + } +} diff --git a/services/user-service/src/Controller/UserController.php b/services/user-service/src/Controller/UserController.php index 31d25af..3722ed6 100644 --- a/services/user-service/src/Controller/UserController.php +++ b/services/user-service/src/Controller/UserController.php @@ -1,6 +1,6 @@ getContent(), true); + // Валидация обязательных полей + if (!isset($data['email'], $data['firstName'], $data['lastName'], $data['password'])) { + return $this->json([ + 'error' => 'Missing required fields: email, firstName, lastName, password' + ], 400); + } + // Проверяем существует ли пользователь $existingUser = $this->userRepository->findByEmail($data['email']); if ($existingUser) { @@ -50,25 +58,28 @@ public function create(Request $request): JsonResponse // Сохраняем в БД $this->userRepository->save($user, true); - // Создаем ответ по контракту - $userResponse = new UserResponse( - id: (string)$user->getId(), // Преобразуем int в string для контракта + // Создаем и отправляем событие + $event = new UserRegisteredEvent( + eventId: uniqid('user_reg_', true), + userId: (string)$user->getId(), email: $user->getEmail(), firstName: $user->getFirstName(), lastName: $user->getLastName(), - status: $user->getStatus(), - createdAt: $user->getCreatedAt(), - updatedAt: $user->getUpdatedAt() + registeredAt: $user->getCreatedAt() ); - // Создаем событие - $event = new UserRegisteredEvent( - eventId: uniqid('event_'), - userId: (string)$user->getId(), + // Отправляем событие в RabbitMQ + $this->messageBus->dispatch($event); + + // Создаем ответ по контракту + $userResponse = new UserResponse( + id: (string)$user->getId(), email: $user->getEmail(), firstName: $user->getFirstName(), lastName: $user->getLastName(), - registeredAt: $user->getCreatedAt() + status: $user->getStatus(), + createdAt: $user->getCreatedAt(), + updatedAt: $user->getUpdatedAt() ); return $this->json($userResponse, 201); diff --git a/services/user-service/symfony.lock b/services/user-service/symfony.lock index 734cefb..5bb58de 100644 --- a/services/user-service/symfony.lock +++ b/services/user-service/symfony.lock @@ -103,6 +103,18 @@ ".editorconfig" ] }, + "symfony/messenger": { + "version": "7.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.0", + "ref": "ba1ac4e919baba5644d31b57a3284d6ba12d52ee" + }, + "files": [ + "config/packages/messenger.yaml" + ] + }, "symfony/property-info": { "version": "7.3", "recipe": {