Step 1: Setting up the Symfony Backend with API Platform
We’ll start by setting up a new Symfony project with API Platform to create our backend API.
- Install Symfony and API Platform
- Use Composer to create a new Symfony project with API Platform:
composer create-project symfony/skeleton:"^6.2" my-api cd my-api composer require api
- Use Composer to create a new Symfony project with API Platform:
- Configure the Database
- Update your .env file with your database credentials:
DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=8.0"
- Update your .env file with your database credentials:
- Create an Entity
- Generate a new entity class, for example, a Book entity:
<?php namespace App\Entity; use ApiPlatform\Metadata\ApiResource; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] #[ApiResource] class Book { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\Column(length: 255)] private ?string $title = null; #[ORM\Column(type: 'text')] private ?string $description = null; // Getters and setters... }
- Generate a new entity class, for example, a Book entity:
Step 2: Creating API Endpoints
API Platform automatically creates CRUD endpoints for our entities. Let’s customize an endpoint.
- Customize API Resource
- Modify the Book entity to customize the API resource:
<?php namespace App\Entity; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Put; use ApiPlatform\Metadata\Delete; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] #[ApiResource( operations: [ new Get(), new GetCollection(), new Post(), new Put(), new Delete(), ], paginationEnabled: false )] class Book { // ... }
- Modify the Book entity to customize the API resource:
Step 3: Setting up the React Frontend
Now, let’s create a React application to consume our API.
- Create React App
- Use Create React App to set up a new React project:
npx create-react-app my-app cd my-app
- Use Create React App to set up a new React project:
- Install Axios for API Calls
- Install Axios to make HTTP requests to our Symfony API:
npm install axios
- Install Axios to make HTTP requests to our Symfony API:
Step 4: Implementing API Calls from React
Let’s create a component to fetch and display books from our API.
- Create a BookList Component
- Create a new file src/components/BookList.js:
import React, { useState, useEffect } from 'react'; import axios from 'axios'; const BookList = () => { const [books, setBooks] = useState([]); useEffect(() => { const fetchBooks = async () => { try { const response = await axios.get('http://localhost:8000/api/books'); setBooks(response.data['hydra:member']); } catch (error) { console.error('Error fetching books:', error); } }; fetchBooks(); }, []); return ( <div> <h1>Book List</h1> <ul> {books.map(book => ( <li key={book.id}>{book.title}</li> ))} </ul> </div> ); }; export default BookList;
- Create a new file src/components/BookList.js:
Step 5: Handling CORS for Frontend-Backend Communication
To allow our React frontend to communicate with the Symfony backend, we need to configure CORS (Cross-Origin Resource Sharing).
- Install Nelmio CORS Bundle
- In your Symfony project, install the Nelmio CORS Bundle:
composer require nelmio/cors-bundle
- In your Symfony project, install the Nelmio CORS Bundle:
- Configure CORS
- Update your config/packages/nelmio_cors.yaml file:
nelmio_cors: defaults: origin_regex: true allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] allow_headers: ['Content-Type', 'Authorization'] expose_headers: ['Link'] max_age: 3600 paths: '^/api/': allow_origin: ['*'] allow_headers: ['*'] allow_methods: ['POST', 'PUT', 'GET', 'DELETE']
- Update your config/packages/nelmio_cors.yaml file:
Step 6: Adding Authentication to the API
Let’s add JWT authentication to secure our API.
- Install Lexik JWT Authentication Bundle
- In your Symfony project, install the JWT Bundle:
composer require lexik/jwt-authentication-bundle
- In your Symfony project, install the JWT Bundle:
- Generate the JWT Keys
- Generate public and private keys:
php bin/console lexik:jwt:generate-keypair
- Generate public and private keys:
- Configure Security
- Update your config/packages/security.yaml:
security: enable_authenticator_manager: true password_hashers: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' providers: app_user_provider: entity: class: App\Entity\User property: email firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false login: pattern: ^/api/login stateless: true json_login: check_path: /api/login_check success_handler: lexik_jwt_authentication.handler.authentication_success failure_handler: lexik_jwt_authentication.handler.authentication_failure api: pattern: ^/api stateless: true jwt: ~ access_control: - { path: ^/api/login, roles: PUBLIC_ACCESS } - { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
- Update your config/packages/security.yaml:
Step 7: Implementing Authentication in React
Now, let’s add authentication to our React frontend.
- Create Login Component
- Create a new file src/components/Login.js:
import React, { useState } from 'react'; import axios from 'axios'; const Login = ({ onLogin }) => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const handleSubmit = async (e) => { e.preventDefault(); try { const response = await axios.post('http://localhost:8000/api/login_check', { email, password }); localStorage.setItem('token', response.data.token); onLogin(true); } catch (error) { console.error('Login failed:', error); } }; return ( <form onSubmit={handleSubmit}> <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" required /> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" required /> <button type="submit">Login</button> </form> ); }; export default Login;
- Create a new file src/components/Login.js:
- Update API Calls
- Modify your axios calls to include the JWT token:
const token = localStorage.getItem('token'); const config = { headers: { Authorization: `Bearer ${token}` } }; axios.get('http://localhost:8000/api/books', config) .then(response => { // Handle response });
- Modify your axios calls to include the JWT token:
Step 8: Deploying the Application
Finally, let’s prepare our application for deployment.
- Prepare Symfony for Production
- In your Symfony project, clear and warm up the cache:
APP_ENV=prod APP_DEBUG=0 php bin/console cache:clear APP_ENV=prod APP_DEBUG=0 php bin/console cache:warmup
- In your Symfony project, clear and warm up the cache:
- Build React for Production
- In your React project, build the production version:
npm run build
- In your React project, build the production version:
- Deploy to a Web Server
- Upload your Symfony files to your web server.
- Upload the contents of your React build folder to a static file hosting service or to a directory on your web server.
- Configure your web server to serve the Symfony application and the React static files.
This completes our tutorial on building a full-stack web application with Symfony API Platform and React. We’ve covered setting up the backend, creating API endpoints, building a React frontend, implementing authentication, and preparing for deployment. Remember to adjust security settings, add error handling, and implement proper user management before deploying to a production environment.
Step 9: Adding Data Validation
Let’s implement data validation for our API endpoints using Symfony’s built-in validation component.
- Update the Book Entity
- Add validation constraints to the Book entity:
<?php namespace App\Entity; use ApiPlatform\Metadata\ApiResource; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity] #[ApiResource] class Book { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\Column(length: 255)] #[Assert\NotBlank] #[Assert\Length(min: 2, max: 255)] private ?string $title = null; #[ORM\Column(type: 'text')] #[Assert\NotBlank] private ?string $description = null; // Getters and setters... }
- Add validation constraints to the Book entity:
Step 10: Implementing Custom Operations
Let’s add a custom operation to our API for searching books.
- Create a Custom Controller
- Create a new file src/Controller/BookSearchController.php:
<?php namespace App\Controller; use App\Repository\BookRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\JsonResponse; class BookSearchController extends AbstractController { public function __invoke(Request $request, BookRepository $bookRepository): JsonResponse { $query = $request->query->get('q'); $books = $bookRepository->searchByTitle($query); return $this->json($books); } }
- Create a new file src/Controller/BookSearchController.php:
- Update the Book Entity
- Add the custom operation to the Book entity:
<?php use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Put; use ApiPlatform\Metadata\Delete; #[ApiResource( operations: [ new Get(), new GetCollection(), new Post(), new Put(), new Delete(), new GetCollection( uriTemplate: '/books/search', controller: BookSearchController::class, openapiContext: [ 'summary' => 'Search for books', 'parameters' => [ [ 'name' => 'q', 'in' => 'query', 'description' => 'Search query', 'required' => true, 'schema' => ['type' => 'string'] ] ] ] ) ] )] class Book { // ... }
- Add the custom operation to the Book entity:
Step 11: Implementing Pagination in React
Let’s add pagination to our BookList component in React.
- Update BookList Component
- Modify src/components/BookList.js to include pagination:
import React, { useState, useEffect } from 'react'; import axios from 'axios'; const BookList = () => { const [books, setBooks] = useState([]); const [page, setPage] = useState(1); const [totalPages, setTotalPages] = useState(0); useEffect(() => { const fetchBooks = async () => { try { const response = await axios.get(`http://localhost:8000/api/books?page=${page}`); setBooks(response.data['hydra:member']); setTotalPages(Math.ceil(response.data['hydra:totalItems'] / response.data['hydra:itemsPerPage'])); } catch (error) { console.error('Error fetching books:', error); } }; fetchBooks(); }, [page]); return ( <div> <h1>Book List</h1> <ul> {books.map(book => ( <li key={book.id}>{book.title}</li> ))} </ul> <div> <button onClick={() => setPage(page - 1)} disabled={page === 1}>Previous</button> <span>Page {page} of {totalPages}</span> <button onClick={() => setPage(page + 1)} disabled={page === totalPages}>Next</button> </div> </div> ); }; export default BookList;
- Modify src/components/BookList.js to include pagination:
Step 12: Adding Error Handling and Loading States
Let’s improve our React components by adding error handling and loading states.
- Update BookList Component
- Modify src/components/BookList.js to include error handling and loading state:
import React, { useState, useEffect } from 'react'; import axios from 'axios'; const BookList = () => { const [books, setBooks] = useState([]); const [page, setPage] = useState(1); const [totalPages, setTotalPages] = useState(0); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { const fetchBooks = async () => { setIsLoading(true); setError(null); try { const response = await axios.get(`http://localhost:8000/api/books?page=${page}`); setBooks(response.data['hydra:member']); setTotalPages(Math.ceil(response.data['hydra:totalItems'] / response.data['hydra:itemsPerPage'])); } catch (error) { setError('An error occurred while fetching books. Please try again later.'); console.error('Error fetching books:', error); } finally { setIsLoading(false); } }; fetchBooks(); }, [page]); if (isLoading) return <div>Loading...</div>; if (error) return <div>{error}</div>; return ( <div> <h1>Book List</h1> <ul> {books.map(book => ( <li key={book.id}>{book.title}</li> ))} </ul> <div> <button onClick={() => setPage(page - 1)} disabled={page === 1}>Previous</button> <span>Page {page} of {totalPages}</span> <button onClick={() => setPage(page + 1)} disabled={page === totalPages}>Next</button> </div> </div> ); }; export default BookList;
- Modify src/components/BookList.js to include error handling and loading state:
This concludes our extended tutorial on building a full-stack web application with Symfony API Platform and React. We’ve covered advanced topics such as custom API operations, data validation, pagination, and error handling. These additions will make your application more robust and user-friendly.
Remember to always follow best practices for security, performance, and code organization as you continue to develop your application. Consider adding features like user registration, more complex data relationships, and comprehensive testing to further enhance your project.
Certainly. Let’s continue by adding some more advanced features and best practices to our Symfony API Platform and React application.
Step 13: Implementing Caching
Let’s add caching to our API to improve performance.
- Configure API Platform Caching
- Update your config/packages/api_platform.yaml file:
api_platform: # ... defaults: cache_headers: max_age: 3600 shared_max_age: 3600 vary: ['Content-Type', 'Authorization', 'Origin'] collection: pagination: items_per_page: 30 # The default number of items per page
- Update your config/packages/api_platform.yaml file:
- Add Cache Tags to Entity
- Update your Book entity to include cache tags:
<?php namespace App\Entity; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Cache; #[ApiResource( cacheHeaders: new Cache(maxAge: 3600, public: true) )] class Book { // ... }
- Update your Book entity to include cache tags:
Step 14: Adding Real-time Updates with Mercure
Let’s implement real-time updates using Mercure protocol.
- Install Mercure Bundle
- In your Symfony project, install the Mercure bundle:
composer require symfony/mercure-bundle
- In your Symfony project, install the Mercure bundle:
- Configure Mercure
- Update your .env file:
MERCURE_URL=http://localhost:3000/.well-known/mercure MERCURE_PUBLIC_URL=http://localhost:3000/.well-known/mercure MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
- Update your .env file:
- Update API Resource
- Modify your Book entity to enable Mercure:
<?php use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Put; #[ApiResource( mercure: true, operations: [ new GetCollection(), new Post( publish: true ), new Put( publish: true ) ] )] class Book { // ... }
- Modify your Book entity to enable Mercure:
Step 15: Implementing Real-time Updates in React
Now let’s update our React component to listen for real-time updates.
- Update BookList Component
- Modify src/components/BookList.js to include Mercure:
import React, { useState, useEffect } from 'react'; import axios from 'axios'; const BookList = () => { const [books, setBooks] = useState([]); useEffect(() => { const fetchBooks = async () => { try { const response = await axios.get('http://localhost:8000/api/books'); setBooks(response.data['hydra:member']); } catch (error) { console.error('Error fetching books:', error); } }; fetchBooks(); const url = new URL('http://localhost:3000/.well-known/mercure'); url.searchParams.append('topic', 'http://example.com/books/{id}'); const eventSource = new EventSource(url); eventSource.onmessage = event => { const book = JSON.parse(event.data); setBooks(prevBooks => { const index = prevBooks.findIndex(b => b.id === book.id); if (index !== -1) { return [...prevBooks.slice(0, index), book, ...prevBooks.slice(index + 1)]; } else { return [...prevBooks, book]; } }); }; return () => { eventSource.close(); }; }, []); return ( <div> <h1>Book List</h1> <ul> {books.map(book => ( <li key={book.id}>{book.title}</li> ))} </ul> </div> ); }; export default BookList;
- Modify src/components/BookList.js to include Mercure:
Step 16: Adding Unit Tests
Let’s add some unit tests to our Symfony application.
- Install PHPUnit Bridge
- In your Symfony project, install PHPUnit Bridge:
composer require --dev symfony/phpunit-bridge
- In your Symfony project, install PHPUnit Bridge:
- Create a Test for Book Entity
- Create a new file tests/Entity/BookTest.php:
<?php namespace App\Tests\Entity; use App\Entity\Book; use PHPUnit\Framework\TestCase; class BookTest extends TestCase { public function testSetTitle(): void { $book = new Book(); $title = 'Test Book Title'; $book->setTitle($title); $this->assertEquals($title, $book->getTitle()); } public function testSetDescription(): void { $book = new Book(); $description = 'Test book description'; $book->setDescription($description); $this->assertEquals($description, $book->getDescription()); } }
- Create a new file tests/Entity/BookTest.php:
Step 17: Adding Integration Tests
Let’s add an integration test for our API.
- Create an API Test
- Create a new file tests/Api/BooksTest.php:
<?php namespace App\Tests\Api; use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCase; use App\Entity\Book; class BooksTest extends ApiTestCase { public function testGetCollection(): void { $response = static::createClient()->request('GET', '/api/books'); $this->assertResponseIsSuccessful(); $this->assertJsonContains(['@context' => '/api/contexts/Book']); } public function testCreateBook(): void { $response = static::createClient()->request('POST', '/api/books', ['json' => [ 'title' => 'The Great Gatsby', 'description' => 'A novel by F. Scott Fitzgerald', ]]); $this->assertResponseStatusCodeSame(201); $this->assertJsonContains([ '@context' => '/api/contexts/Book', '@type' => 'Book', 'title' => 'The Great Gatsby', 'description' => 'A novel by F. Scott Fitzgerald', ]); } }
- Create a new file tests/Api/BooksTest.php:
This concludes our extended tutorial on building a full-stack web application with Symfony API Platform and React. We’ve covered advanced topics such as caching, real-time updates with Mercure, unit testing, and integration testing. These additions will make your application more robust, performant, and maintainable.
Remember to always follow best practices for security, performance, and code organization as you continue to develop your application. Consider adding features like comprehensive error handling, logging, monitoring, and continuous integration/continuous deployment (CI/CD) pipelines to further enhance your project.