A REST API for tracking stock market values built with Slim Framework 4, PHP 8.2, and MySQL.
- Stock Data Retrieval: Real-time stock information from Stooq API
- User Authentication: JWT-based authentication system
- Stock Query History: Track and retrieve user's past stock queries
- Email Notifications: Asynchronous email notifications using RabbitMQ
- API Documentation: OpenAPI/Swagger documentation
- Containerized: Docker and Docker Compose for easy setup
- Database Migrations: Automated database setup and seeding
- Unit Tests: Comprehensive test coverage
- Project Overview
- Getting Started
- Available Endpoints
- Email System with RabbitMQ Integration
- Stock API Features
- Database Migrations and Seeders
- Running Tests
- Project Structure
- Development Guidelines
- Operational Information
This project implements a RESTful API for tracking stock market values, meeting all mandatory and bonus requirements of the PHP Challenge:
- âś… SQL Database: MySQL for users and stock query logs
- âś… Authentication System: Secure user authentication
- âś… User Creation Endpoint: API for creating new users
- âś… Stock Quote Endpoint: Real-time stock data with email notifications
- âś… Query History Endpoint: Track and retrieve user's past stock queries
- âś… Unit Tests: Comprehensive test coverage
- âś… RabbitMQ Integration: Asynchronous email processing
- âś… JWT Authentication: Secure token-based API access
- âś… Docker Containerization: Easy setup and deployment
- PHP 8.2 with strict typing and attribute-based annotations
- Slim Framework 4 for routing and middleware
- MySQL 8.0 with Eloquent ORM for data persistence
- RabbitMQ for message queuing
- Symfony Mailer for email delivery
- OpenAPI/Swagger for API documentation
- Docker and Docker Compose installed on your system
# 1. Clone the repository (or download it)
git clone https://github.com/yourusername/php-challenge.git
cd php-challenge
# 2. Create environment file
cp .env.sample .env
# 3. Configure your .env file with appropriate values
# Important settings to review:
# - JWT_SECRET: Set a secure random string for JWT token generation
# - DB_PASSWORD: Change the default database password if you want
# - MAILER_DSN: Configure your email service (Mailtrap recommended for testing)
# - RMQ_ENABLED: Set to 1 to enable asynchronous email processing
# 4. Start the application
docker-compose up -dThat's it! The application will:
- Start all necessary containers (PHP, MySQL, RabbitMQ)
- Run database migrations automatically
- Create a default user (email: [email protected], password: user123)
- Start the email consumer service
- API Endpoints: http://localhost:8080
- API Documentation: http://localhost:8080/swagger
- RabbitMQ Management: http://localhost:15672 (guest/guest)
The Stock API provides the following features:
-
Stock Data Retrieval: Query real-time stock information using the
/stock?q={symbol}endpoint- Example:
/stock?q=AAPL.USretrieves Apple Inc. stock data - Returns data including symbol, name, open, high, low, and close values
- Data is sourced from the Stooq API, a free financial data provider
- Example:
-
Stock Query History: View your past stock queries using the
/historyendpoint- Returns a list of all stock queries made by the authenticated user
- Results are ordered by most recent first
-
Data Persistence: All stock queries are saved to the database for historical tracking
- Each query is associated with the authenticated user
- Includes timestamp information for when the query was made
- Database schema includes:
id: Unique identifier for the queryuser_id: Foreign key to the users tablesymbol: Stock symbol (e.g., 'AAPL.US')name: Company name (e.g., 'APPLE')open: Opening pricehigh: Highest pricelow: Lowest priceclose: Closing pricecreated_at: When the query was madeupdated_at: When the record was last updated
-
Symbol Format: Stock symbols should be provided in the format used by Stooq
- US stocks: Add
.USsuffix (e.g.,AAPL.US,MSFT.US) - For other markets, check the Stooq website for the correct symbol format
- US stocks: Add
-
Response Format:
- All API responses follow a consistent JSON format
- Success responses include
success: true,message, anddatafields - Error responses include
success: false,message, anderrorfields - Stock data includes: symbol, name, open, high, low, and close values
-
Error Handling:
- Missing symbol parameter: Returns 400 Bad Request
- Invalid symbol or no data available: Returns 404 Not Found
- Successful requests: Returns 200 OK with stock data
-
Security Considerations:
- All stock API endpoints are protected with JWT authentication
- Users can only access their own stock query history
- API requests are validated to prevent injection attacks
- Sensitive operations require a valid JWT token
The application implements an asynchronous email notification system using RabbitMQ, satisfying the bonus feature requirement:
-
Message Queue Architecture:
- When a user requests stock information, the API sends the stock data to a RabbitMQ queue
- A separate consumer process reads from the queue and sends emails asynchronously
- This decouples the API request handling from email delivery, improving performance
-
Implementation Components:
MessageQueueservice: Handles connection to RabbitMQ and message publishingEmailNotificationService: Prepares email content and publishes to the queueEmailConsumer: Background process that consumes messages and sends emailsTemplateRenderer: Renders HTML email templates with stock data
-
Configuration: Configure your email and RabbitMQ settings in the
.envfile:# Email Settings MAILER_DSN=smtp://username:[email protected]:587 [email protected] MAILER_FROM_NAME="Stock API" MAILER_ENABLED=1 # RabbitMQ Settings RMQ_ENABLED=1 # Set to 1 to enable the RabbitMQ consumer RMQ_HOST=rabbitmq RMQ_VHOST=/ RMQ_PORT=5672 RMQ_USERNAME=guest RMQ_PASSWORD=guest -
Automatic Consumer: The email consumer service runs automatically when the Docker container starts if RabbitMQ is enabled, processing any messages in the queue.
-
Fallback Mechanism: If RabbitMQ is disabled or unavailable, the system gracefully degrades without affecting the API functionality.
-
User Creation
POST /users- Creates a new user account- Protected by JWT authentication
- Accepts email, password, and name
-
Stock Quote Retrieval
GET /stock?q={symbol}- Returns real-time stock information- Protected by JWT authentication
- Example:
/stock?q=AAPL.USretrieves Apple Inc. stock data - Sends an email notification with stock data to the requesting user
- Response format:
{ "success": true, "message": "Stock quote retrieved successfully", "data": { "symbol": "AAPL.US", "name": "APPLE", "open": 123.66, "high": 123.66, "low": 122.49, "close": 123 } }
-
Stock Query History
GET /history- Returns the user's stock query history- Protected by JWT authentication
- Returns queries in reverse chronological order (newest first)
- Response format:
{ "success": true, "message": "Stock query history retrieved successfully", "data": [ { "date": "2023-04-01T19:20:30Z", "symbol": "AAPL.US", "name": "APPLE", "open": 123.66, "high": 123.66, "low": 122.49, "close": 123 }, // More history entries... ] }
GET /hello/{name}- Public endpoint, returns a greeting messageGET /bye/{name}- Protected endpoint, returns a goodbye messageGET /users- Protected endpoint, returns all usersGET /users/{id}- Protected endpoint, returns a specific userPUT /users/{id}- Protected endpoint, updates a userDELETE /users/{id}- Protected endpoint, deletes a user
The API uses JWT (JSON Web Token) authentication for protected endpoints. To access protected routes:
-
First, obtain a JWT token by authenticating with the login endpoint:
POST /auth/login { "email": "[email protected]", "password": "user123" } -
The response will contain a JWT token:
{ "success": true, "message": "Login successful", "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "user": { "id": 1, "name": "Test User", "email": "[email protected]" } } } -
Include this token in the Authorization header for protected endpoints:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
A default admin user is automatically created during the first application startup if no users exist in the database:
- Email:
[email protected] - Password:
user123
These credentials are defined in the .env file as DEFAULT_USERNAME and DEFAULT_PASSWORD.
The project includes a robust database migration and seeding system that allows you to create, run, rollback, and refresh database migrations, as well as seed your database with initial data.
The project provides a convenient CLI script for managing migrations:
# Make the migrate script executable (if needed)
chmod +x migrate
# Run all pending migrations
./migrate run
# Rollback all migrations
./migrate rollback
# Refresh all migrations (rollback then run)
./migrate refreshYou can also run these commands inside the Docker container:
docker-compose exec app ./migrate runTo create a new migration:
- Create a new PHP class in the
src/Database/Schema/Migrationsdirectory - Implement the
App\Database\Schema\Migrationinterface - Define the
up()anddown()methods to create and drop your database structures - Implement the
getName()method to return a unique name for your migration
If your migrations have dependencies (e.g., foreign key constraints), you should explicitly register them in the correct order in MigrateCommand.php. For example:
// In MigrateCommand.php
$migrationRunner = new MigrationRunner();
// Register migrations in the correct order
$migrationRunner->register(CreateMigrationsTable::class);
$migrationRunner->register(CreateUsersTable::class); // Parent table
$migrationRunner->register(CreateStockQueriesTable::class); // Child table with foreign key
// Then scan for any other migrations
$migrationRunner->scanMigrations(__DIR__ . '/../../Schema/Migrations');Example migration class:
<?php
declare(strict_types=1);
namespace App\Database\Schema\Migrations;
use App\Database\Schema\Migration;
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Schema\Blueprint;
class CreateExampleTable implements Migration
{
public static function getName(): string
{
return 'create_example_table';
}
public static function up(): void
{
Capsule::schema()->create('examples', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}
public static function down(): void
{
Capsule::schema()->dropIfExists('examples');
}
}The migration system:
- Creates a
migrationstable to track executed migrations - Only runs migrations that haven't been executed yet
- Automatically scans the
Migrationsdirectory for new migration classes - Provides detailed output during the migration process
Important Note: Migrations do not follow a specific order when running automatically. If you have tables with foreign key dependencies, make sure to explicitly register dependent tables in the correct order in
MigrateCommand.phpbefore running migrations.
Migrations run automatically when the Docker container starts. This ensures your database schema is always up-to-date when you deploy or restart the application.
Seeders allow you to populate your database with initial or test data. The project includes a seeding system that works alongside migrations.
The application includes a UserSeeder that automatically creates a default admin user if no users exist in the database. This ensures you always have access to the system, even on a fresh installation.
To create a new seeder:
- Create a new PHP class in the
src/Database/Schema/Seedersdirectory - Implement the
App\Database\Schema\Seederinterface - Define the
run()method to insert your data - Implement the
getName()method to return a unique name for your seeder
Example seeder class:
<?php
declare(strict_types=1);
namespace App\Database\Schema\Seeders;
use App\Database\Schema\Seeder;
use App\Models\ExampleModel;
class ExampleSeeder implements Seeder
{
public static function getName(): string
{
return 'example_seeder';
}
public static function run(): void
{
// Check if data already exists
if (ExampleModel::count() > 0) {
return;
}
// Create example data
ExampleModel::create([
'name' => 'Example Data',
// other fields...
]);
}
}Seeders run automatically after migrations when the Docker container starts. You can also run them manually using the migration CLI:
# Run all seeders
./migrate seedThe project includes comprehensive PHPUnit tests for all endpoints and services, satisfying the bonus feature requirement for unit tests.
The test suite covers:
- Authentication: Login, JWT validation, and protected routes
- Stock API: Stock data retrieval and error handling
- User Management: User creation, retrieval, update, and deletion
- History: Stock query history storage and retrieval
- Email System: Email notification generation and queue integration
All tests can be run within the Docker environment:
# Run all tests
docker-compose exec app vendor/bin/phpunit
# Run a specific test file
docker-compose exec app vendor/bin/phpunit tests/HelloTest.php
# Run a specific test method
docker-compose exec app vendor/bin/phpunit --filter testHelloEndpoint tests/HelloTest.phpThe project is configured for code coverage reporting with Xdebug. To generate a coverage report:
docker-compose exec app vendor/bin/phpunit --coverage-html=coverageThe coverage report will be generated in the coverage directory. You can open coverage/html/index.html in your browser to view a detailed coverage report.
The project follows a clean, modular architecture with clear separation of concerns:
-
Controllers (
src/Controllers/)AuthController.php- Handles user authentication and JWT token generationStockController.php- Manages stock data retrieval and history endpointsUserController.php- Provides user management functionality
-
Services (
src/Services/)StockService.php- Business logic for stock data operationsStooqApiService.php- Integration with the Stooq APIEmailNotificationService.php- Email notification handlingMessageQueue.php- RabbitMQ integration for async processingUserService.php- User management operations
-
Models (
src/Models/)- Database entity models using Eloquent ORM
-
Middleware (
src/Middleware/)JwtMiddleware.php- JWT authentication and validation
-
Application Config (
app/)routes.php- API route definitionsservices.php- Dependency injection container configuration
-
Database (
src/Database/)- Migration and seeding system for database setup
- Automated schema creation and initial data seeding
-
API Documentation (
src/OpenApi/)- OpenAPI/Swagger definitions for API documentation
-
Tests (
tests/)- Comprehensive test suite for all components
-
Docker Containerization: The application is fully containerized with Docker, making it easy to set up and run in any environment.
-
Dependency Injection: Services are registered in
app/services.phpand automatically injected where needed. -
Database Migrations: Automated database schema creation and seeding for quick setup.
-
API Documentation: All endpoints are documented with OpenAPI annotations, accessible via Swagger UI.
-
Asynchronous Processing: Email notifications are handled asynchronously via RabbitMQ.
-
JWT Authentication: Secure API access with JWT tokens and middleware protection.
The project follows a comprehensive testing approach with:
- Unit Tests: Testing individual components in isolation
- Integration Tests: Testing interactions between components
- API Tests: End-to-end testing of API endpoints
- PSR Standards: The codebase follows PSR-1, PSR-4, and PSR-12 coding standards
- Type Hinting: Strict type declarations are used throughout the codebase
- Dependency Management: Composer is used for managing dependencies
# Rebuild containers after Dockerfile changes
docker-compose down
docker-compose up -d --build
# View logs from all containers
docker-compose logs -fThe application follows a modular architecture that makes it easy to extend:
- New Endpoints: Add routes in
app/routes.phpand implement controllers insrc/Controllers/ - New Features: Create services in
src/Services/and register them inapp/services.php - Database Changes: Add migrations in
src/Database/Schema/Migrations/and run withphp console migrate - New Tests: Add test cases in
tests/directory following the existing patterns
- PHP error logs are available in the
logs/php-errors.logfile - RabbitMQ consumer logs are available in the
logs/consumer.logfile - Database connection issues can be resolved by checking the
.envconfiguration - RabbitMQ connection issues can be verified through the management interface at port 15672