From b421ff10029c8a4a963e5c399273967083b38b38 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Fri, 21 Nov 2025 17:12:52 +0100 Subject: [PATCH 1/2] Created a new Make an RESTful API guide --- user_guide_src/source/guides/api/code/001.php | 16 ++ user_guide_src/source/guides/api/code/002.php | 18 ++ user_guide_src/source/guides/api/code/003.php | 21 +++ user_guide_src/source/guides/api/code/004.php | 41 +++++ user_guide_src/source/guides/api/code/005.php | 50 ++++++ user_guide_src/source/guides/api/code/006.php | 48 ++++++ user_guide_src/source/guides/api/code/007.php | 13 ++ user_guide_src/source/guides/api/code/008.php | 13 ++ user_guide_src/source/guides/api/code/009.php | 54 ++++++ user_guide_src/source/guides/api/code/010.php | 16 ++ user_guide_src/source/guides/api/code/011.php | 29 ++++ user_guide_src/source/guides/api/code/012.php | 120 ++++++++++++++ user_guide_src/source/guides/api/code/013.php | 21 +++ user_guide_src/source/guides/api/code/014.php | 14 ++ .../source/guides/api/conclusion.rst | 6 + .../source/guides/api/controller.rst | 154 ++++++++++++++++++ .../source/guides/api/database-setup.rst | 97 +++++++++++ .../source/guides/api/first-endpoint.rst | 118 ++++++++++++++ user_guide_src/source/guides/api/index.rst | 104 ++++++++++++ .../first-app}/conclusion.rst | 0 .../first-app}/create_news_items.rst | 0 .../first-app}/create_news_items/001.php | 0 .../first-app}/create_news_items/002.php | 0 .../first-app}/create_news_items/003.php | 0 .../first-app}/create_news_items/004.php | 0 .../first-app}/create_news_items/005.php | 0 .../first-app}/create_news_items/006.php | 0 .../{tutorial => guides/first-app}/index.rst | 0 .../first-app}/news_section.rst | 0 .../first-app}/news_section/001.php | 0 .../first-app}/news_section/002.php | 0 .../first-app}/news_section/003.php | 0 .../first-app}/news_section/004.php | 0 .../first-app}/news_section/005.php | 0 .../first-app}/news_section/006.php | 0 .../first-app}/news_section/007.php | 0 .../first-app}/news_section/008.php | 0 .../first-app}/static_pages.rst | 0 .../first-app}/static_pages/001.php | 0 .../first-app}/static_pages/002.php | 0 .../first-app}/static_pages/003.php | 0 .../first-app}/static_pages/004.php | 0 user_guide_src/source/guides/index.rst | 23 +++ user_guide_src/source/index.rst | 8 +- 44 files changed, 980 insertions(+), 4 deletions(-) create mode 100644 user_guide_src/source/guides/api/code/001.php create mode 100644 user_guide_src/source/guides/api/code/002.php create mode 100644 user_guide_src/source/guides/api/code/003.php create mode 100644 user_guide_src/source/guides/api/code/004.php create mode 100644 user_guide_src/source/guides/api/code/005.php create mode 100644 user_guide_src/source/guides/api/code/006.php create mode 100644 user_guide_src/source/guides/api/code/007.php create mode 100644 user_guide_src/source/guides/api/code/008.php create mode 100644 user_guide_src/source/guides/api/code/009.php create mode 100644 user_guide_src/source/guides/api/code/010.php create mode 100644 user_guide_src/source/guides/api/code/011.php create mode 100644 user_guide_src/source/guides/api/code/012.php create mode 100644 user_guide_src/source/guides/api/code/013.php create mode 100644 user_guide_src/source/guides/api/code/014.php create mode 100644 user_guide_src/source/guides/api/conclusion.rst create mode 100644 user_guide_src/source/guides/api/controller.rst create mode 100644 user_guide_src/source/guides/api/database-setup.rst create mode 100644 user_guide_src/source/guides/api/first-endpoint.rst create mode 100644 user_guide_src/source/guides/api/index.rst rename user_guide_src/source/{tutorial => guides/first-app}/conclusion.rst (100%) rename user_guide_src/source/{tutorial => guides/first-app}/create_news_items.rst (100%) rename user_guide_src/source/{tutorial => guides/first-app}/create_news_items/001.php (100%) rename user_guide_src/source/{tutorial => guides/first-app}/create_news_items/002.php (100%) rename user_guide_src/source/{tutorial => guides/first-app}/create_news_items/003.php (100%) rename user_guide_src/source/{tutorial => guides/first-app}/create_news_items/004.php (100%) rename user_guide_src/source/{tutorial => guides/first-app}/create_news_items/005.php (100%) rename user_guide_src/source/{tutorial => guides/first-app}/create_news_items/006.php (100%) rename user_guide_src/source/{tutorial => guides/first-app}/index.rst (100%) rename user_guide_src/source/{tutorial => guides/first-app}/news_section.rst (100%) rename user_guide_src/source/{tutorial => guides/first-app}/news_section/001.php (100%) rename user_guide_src/source/{tutorial => guides/first-app}/news_section/002.php (100%) rename user_guide_src/source/{tutorial => guides/first-app}/news_section/003.php (100%) rename user_guide_src/source/{tutorial => guides/first-app}/news_section/004.php (100%) rename user_guide_src/source/{tutorial => guides/first-app}/news_section/005.php (100%) rename user_guide_src/source/{tutorial => guides/first-app}/news_section/006.php (100%) rename user_guide_src/source/{tutorial => guides/first-app}/news_section/007.php (100%) rename user_guide_src/source/{tutorial => guides/first-app}/news_section/008.php (100%) rename user_guide_src/source/{tutorial => guides/first-app}/static_pages.rst (100%) rename user_guide_src/source/{tutorial => guides/first-app}/static_pages/001.php (100%) rename user_guide_src/source/{tutorial => guides/first-app}/static_pages/002.php (100%) rename user_guide_src/source/{tutorial => guides/first-app}/static_pages/003.php (100%) rename user_guide_src/source/{tutorial => guides/first-app}/static_pages/004.php (100%) create mode 100644 user_guide_src/source/guides/index.rst diff --git a/user_guide_src/source/guides/api/code/001.php b/user_guide_src/source/guides/api/code/001.php new file mode 100644 index 000000000000..fdf7ad0e3a15 --- /dev/null +++ b/user_guide_src/source/guides/api/code/001.php @@ -0,0 +1,16 @@ +respond(['status' => 'ok'], 200); + } +} diff --git a/user_guide_src/source/guides/api/code/002.php b/user_guide_src/source/guides/api/code/002.php new file mode 100644 index 000000000000..9a812e60d652 --- /dev/null +++ b/user_guide_src/source/guides/api/code/002.php @@ -0,0 +1,18 @@ +respond(['status' => 'ok'], 200); + } +} diff --git a/user_guide_src/source/guides/api/code/003.php b/user_guide_src/source/guides/api/code/003.php new file mode 100644 index 000000000000..39395ebd0731 --- /dev/null +++ b/user_guide_src/source/guides/api/code/003.php @@ -0,0 +1,21 @@ +respond([ + 'status' => 'ok', + 'time' => date('c'), + 'version'=> CodeIgniter::CI_VERSION, + ]); + } +} diff --git a/user_guide_src/source/guides/api/code/004.php b/user_guide_src/source/guides/api/code/004.php new file mode 100644 index 000000000000..06fcfe2cd4e7 --- /dev/null +++ b/user_guide_src/source/guides/api/code/004.php @@ -0,0 +1,41 @@ +forge->addField([ + 'id' => [ + 'type' => 'INTEGER', + 'unsigned' => true, + 'auto_increment' => true, + ], + 'name' => [ + 'type' => 'VARCHAR', + 'constraint' => '255', + 'null' => false, + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'updated_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + + $this->forge->addPrimaryKey('id'); + $this->forge->addUniqueKey('name'); + $this->forge->createTable('authors'); + } + + public function down() + { + $this->forge->dropTable('authors'); + } +} diff --git a/user_guide_src/source/guides/api/code/005.php b/user_guide_src/source/guides/api/code/005.php new file mode 100644 index 000000000000..41c431c12a0a --- /dev/null +++ b/user_guide_src/source/guides/api/code/005.php @@ -0,0 +1,50 @@ +forge->addField([ + 'id' => [ + 'type' => 'INTEGER', + 'unsigned' => true, + 'auto_increment' => true, + ], + 'title' => [ + 'type' => 'VARCHAR', + 'constraint' => '255', + 'null' => false, + ], + 'author_id' => [ + 'type' => 'INTEGER', + 'unsigned' => true, + 'null' => false, + ], + 'year' => [ + 'type' => 'INTEGER', + 'null' => true, + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'updated_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + + $this->forge->addPrimaryKey('id'); + $this->forge->addForeignKey('author_id', 'authors', 'id'); + $this->forge->createTable('books'); + } + + public function down() + { + $this->forge->dropTable('books'); + } +} diff --git a/user_guide_src/source/guides/api/code/006.php b/user_guide_src/source/guides/api/code/006.php new file mode 100644 index 000000000000..d7ff7f016eb3 --- /dev/null +++ b/user_guide_src/source/guides/api/code/006.php @@ -0,0 +1,48 @@ + 'Frank Herbert'], + ['name' => 'William Gibson'], + ['name' => 'Ursula K. Le Guin'], + ]; + + $this->db->table('authors')->insertBatch($authorData); + + // Get all inserted authors, keyed by name for easy lookup + $authors = $this->db->table('authors') + ->get() + ->getResultArray(); + + $authorsByName = array_column($authors, 'id', 'name'); + + // Define books with author references + $books = [ + [ + 'title' => 'Dune', + 'author_id' => $authorsByName['Frank Herbert'], + 'year' => 1965, + ], + [ + 'title' => 'Neuromancer', + 'author_id' => $authorsByName['William Gibson'], + 'year' => 1984, + ], + [ + 'title' => 'The Left Hand of Darkness', + 'author_id' => $authorsByName['Ursula K. Le Guin'], + 'year' => 1969, + ], + ]; + + $this->db->table('books')->insertBatch($books); + } +} diff --git a/user_guide_src/source/guides/api/code/007.php b/user_guide_src/source/guides/api/code/007.php new file mode 100644 index 000000000000..82450035eb40 --- /dev/null +++ b/user_guide_src/source/guides/api/code/007.php @@ -0,0 +1,13 @@ + $resource['id'], + 'name' => $resource['name'], + ]; + } +} diff --git a/user_guide_src/source/guides/api/code/011.php b/user_guide_src/source/guides/api/code/011.php new file mode 100644 index 000000000000..6a020b172a8c --- /dev/null +++ b/user_guide_src/source/guides/api/code/011.php @@ -0,0 +1,29 @@ + $resource['id'], + 'title' => $resource['title'], + 'year' => $resource['year'], + ]; + } + + protected function includeAuthor(array $book): ?array + { + if (empty($book['author_id']) || empty($book['author_name'])) { + return null; + } + + return [ + 'id' => $book['author_id'], + 'name' => $book['author_name'], + ]; + } +} diff --git a/user_guide_src/source/guides/api/code/012.php b/user_guide_src/source/guides/api/code/012.php new file mode 100644 index 000000000000..8f4478fff6b5 --- /dev/null +++ b/user_guide_src/source/guides/api/code/012.php @@ -0,0 +1,120 @@ +withAuthorInfo()->find($id); + + if (!$book) { + return $this->failNotFound('Book not found'); + } + + return $this->respond($transformer->transform($book)); + } + + // Otherwise, fetch all records + $books = $model->withAuthorInfo(); + + return $this->paginate($books, 20, transformWith: BookTransformer::class); + } + + /** + * Update a book + * + * PUT /api/books/{id} + */ + public function putIndex(int $id) + { + $data = $this->request->getRawInput(); + $model = model('BookModel'); + + $rules = [ + 'title' => 'required|string|max_length[255]', + 'author_id' => 'required|integer|is_not_unique[authors.id]', + 'year' => 'required|integer|greater_than_equal_to[2000]|less_than_equal_to[' . date('Y') . ']', + ]; + + if (!$this->validate($rules)) { + return $this->failValidationErrors($this->validator->getErrors()); + } + + $model = model('BookModel'); + + if (!$model->find($id)) { + return $this->failNotFound('Book not found'); + } + + $model->update($id, $data); + + $updatedBook = $model->withAuthorInfo()->find($id); + + return $this->respond((new BookTransformer())->transform($updatedBook)); + } + + /** + * Create a new book + * + * POST /api/books + */ + public function postIndex() + { + $data = $this->request->getPost(); + + $rules = [ + 'title' => 'required|string|max_length[255]', + 'author_id' => 'required|integer|is_not_unique[authors.id]', + 'year' => 'required|integer|greater_than_equal_to[2000]|less_than_equal_to[' . date('Y') . ']', + ]; + + if (!$this->validate($rules)) { + return $this->failValidationErrors($this->validator->getErrors()); + } + + $model = model('BookModel'); + $model->insert($data); + + $newBook = $model->withAuthorInfo()->find($model->insertID()); + + return $this->respondCreated((new BookTransformer())->transform($newBook)); + } + + /** + * Delete a book + * + * DELETE /api/books/{id} + */ + public function deleteIndex(int $id) + { + $model = model('BookModel'); + + if (!$model->find($id)) { + return $this->failNotFound('Book not found'); + } + + $model->delete($id); + + return $this->respondDeleted(['id' => $id]); + } +} diff --git a/user_guide_src/source/guides/api/code/013.php b/user_guide_src/source/guides/api/code/013.php new file mode 100644 index 000000000000..165b37ec71e1 --- /dev/null +++ b/user_guide_src/source/guides/api/code/013.php @@ -0,0 +1,21 @@ +select('books.*, authors.id as author_id, authors.name as author_name') + ->join('authors', 'books.author_id = authors.id'); + } +} diff --git a/user_guide_src/source/guides/api/code/014.php b/user_guide_src/source/guides/api/code/014.php new file mode 100644 index 000000000000..51011e7280f7 --- /dev/null +++ b/user_guide_src/source/guides/api/code/014.php @@ -0,0 +1,14 @@ +select('books.*, authors.name as author_name') + ->join('authors', 'books.author_id = authors.id'); + } +} diff --git a/user_guide_src/source/guides/api/conclusion.rst b/user_guide_src/source/guides/api/conclusion.rst new file mode 100644 index 000000000000..87df398f16f5 --- /dev/null +++ b/user_guide_src/source/guides/api/conclusion.rst @@ -0,0 +1,6 @@ +Conclusion +========== + +This guide provided an overview of using CodeIgniter 4 to build a simple RESTful API for managing books and authors. You learned how to set up a CodeIgniter project, configure a database, create models, and implement API endpoints using controllers and auto-routing, along with the ResponseTrait for consistent API responses. + +From here, you can expand your API by adding more resources, implementing authentication and authorization using `CodeIgniter Shield `__, and exploring advanced features like :doc:`../../libraries/throttler`, :doc:`../../incoming/controller_attributes`, and :doc:`../../general/caching`. CodeIgniter's flexibility and powerful features make it an excellent choice for building robust APIs. diff --git a/user_guide_src/source/guides/api/controller.rst b/user_guide_src/source/guides/api/controller.rst new file mode 100644 index 000000000000..6a663bad43d4 --- /dev/null +++ b/user_guide_src/source/guides/api/controller.rst @@ -0,0 +1,154 @@ +.. _ci47-rest-part4: + +Building a RESTful Controller +############################# + +In this chapter we will build out the API endpoints to expose your ``books`` table through a proper RESTful API. +We'll use :php:class:`CodeIgniter\\RESTful\\ResourceController` to handle CRUD actions with almost no boilerplate. + +What is RESTful? +================ + +RESTful APIs use standard HTTP verbs (GET, POST, PUT, DELETE) to perform actions on resources identified by URIs. This approach makes APIs predictable and easy to use. There can be much debate ont some of the finer points of what makes a REST API, but following these basics can get it close enough for many uses. By using auto-routing and the :php:class:`Api\ResponseTrait`, CodeIgniter makes it simple to create RESTful endpoints. + +Generate the controller +======================= + +Run the Spark command: + +.. code-block:: console + + php spark make:controller Api/Books + +This creates ``app/Controllers/Api/Books.php``. + +Open it and replace its contents with the following stubbed out class: + +.. literalinclude:: code/009.php + +Since we're using auto-routing, we need to use the ``index`` method names so it doesn't interfere with mapping to the URI segments. But we can use the HTTP verb prefixes (get, post, put, delete) to indicate which method handles which verb in a clear manner. The only one that is slightly odd is ``getIndex()`` which must be used to map to both listing all resources and listing a single resource by ID. + +.. tip:: + + If you prefer a different naming scheme, you would need to define routes explicitly in ``app/Config/Routes.php`` and turn auto-routing off. + +API Transformers +================= + +It is considered a best practice to separate your data models from the way they are presented in your API responses. This is often done using transformers or resource classes that format the data consistently. CodeIgniter provides API transformers to help with this. + +Create the transformers with the generator command: + +.. code-block:: console + + php spark make:transformer BookTransformer + +The transormer requires a single method, ``toArray`` to be present and accept a mixed data type called ``$resource``. This method is responsible for transforming the resource into an array format suitable for API responses. The returned array is what is then encoded as JSON or XML for the API response. + +Edit the Book transformer at ``app/Transformers/BookTransformer.php``. This one is a little more complex since it includes related author data: + +.. literalinclude:: code/011.php + +One feature of transformers is the ability to include related resources conditionally. In this case, we check if the ``author`` relationship is loaded on the book resource before including it in the response. This allows for flexibility in how much data is returned based on the context of the request. The client calling the API would have to request the related data explicitly, through query parameters, like: ``/api/book?include=author``. The method name must start with ``include`` followed by the related resource name with the first letter capitalized. + +You have probably noticed that we did not use an AuthorTransformer. This is because the author data is simple enough that we can return it directly without additional transformation. However, for more complex related resources, you might want to create separate transformers for them as well. Additionally, we will collect the author information at query time so that we don't hit any N+1 query issues later. + +Listing Books +============= + +We made the ``$id`` parameter optional so that the same method can handle both listing all books and retrieving a single book by ID. Let's implement that now. + +.. literalinclude:: code/012.php + :language: php + :lines: 15-41 + +In this method, we check if an ``$id`` is provided. If it is, we attempt to find the specific book. If we could not find the book by that ID, we return a 404 Not Found response using the :php:meth:`failNotFound()` helper from :php:trait:`ResponseTrait`. If we do find the book, we use our BookTransformer and return the formatted response. + +If no ``$id`` is provided, we use the model to retrieve all books, stopping short of actually retrieving the records. This allows us to use the ``ResponseTrait``'s :php:meth:`paginate` method to handle pagination automatically. We pass the name of the transformer to the ``paginate`` method so that it can format each book in the paginated result set. + +In both of these cases, we use a new method on the model called ``withAuthorInfo()``. This is a custom method we will add to the model to join the related author data when retrieving books. + +Add the Model helper method +--------------------------- + +In your BookModel, we add a new method called ``withAuthorInfo()``. This method uses the Query Builder to join the ``authors`` table and select the relevant author fields. This way, when we retrieve books, we also get the associated author information without needing to make separate queries for each book. + +.. literalinclude:: code/014.php + + +Test the list all endpoint +-------------------------- + +Start the local server: + +.. code-block:: console + + php spark serve + +Now visit: + +- **Browser:** ``http://localhost:8080/api/book`` +- **cURL:** ``curl http://localhost:8080/api/book`` + +You should see a paginated list of books in JSON format: + +.. code-block:: json + + { + "data": [ + { + "id": 1, + "title": "Dune", + "author": "Frank Herbert", + "year": 1965, + "created_at": "2025-11-08 00:00:00", + "updated_at": "2025-11-08 00:00:00" + }, + { + "id": 2, + "title": "Neuromancer", + "author": "William Gibson", + "year": 1984, + "created_at": "2025-11-08 00:00:00", + "updated_at": "2025-11-08 00:00:00" + } + ], + "meta": { + "page": 1, + "perPage": 20, + "total": 2, + "totalPages": 1 + }, + "links": { + "self": "http://localhost:8080/api/book?page=1", + "first": "http://localhost:8080/api/book?page=1", + "last": "http://localhost:8080/api/book?page=1" + "prev": null, + "next": null, + } + } + +If you see JSON data from your seeder, congratulations—your API is live! + +Implement the remaining methods +=============================== + +Edit ``app/Controllers/Api/Book.php`` to include the remaining methods: + +.. literalinclude:: code/012.php + +Each method uses helpers from :php:trait:`ResponseTrait` to send proper HTTP status codes and JSON payloads. + +And that's it! You now have a fully functional RESTful API for managing books, complete with proper HTTP methods, status codes, and data transformation. You can further enhance this API by adding authentication, validation, and other features as needed. + +A More Semantic Name scheme +============================ + +In the previous examples, we used method names like ``getIndex``, ``putIndex``, etc because we wanted to solely rely on the HTTP verb to determine the action. With auto-routing enabled, we have to use the ``index`` method name to avoid conflicts with URI segments. However, if you prefer more semantic method names, you could change the method names so that they reflect the action being performed, such as ``getList``, ``postCreate``, ``putUpdate``, and ``deleteDelete``. This would then make each method's purpose clearer at a glance. And would just add one new segment to the URI. + +``` +GET /api/book/list -> getList() +POST /api/book/create -> postCreate() +PUT /api/book/update/(:id) -> putUpdate($id) +DELETE /api/book/delete/(:id) -> deleteDelete($id) +``` diff --git a/user_guide_src/source/guides/api/database-setup.rst b/user_guide_src/source/guides/api/database-setup.rst new file mode 100644 index 000000000000..fc8c0a21c720 --- /dev/null +++ b/user_guide_src/source/guides/api/database-setup.rst @@ -0,0 +1,97 @@ +.. _ci47-rest-part3: + +Creating the Database and Model +################################ + +.. contents:: + :local: + :depth: 2 + +In this section we will get the data layer setup by creating a SQLite database table for our ``books`` resource, seed it with some sample data, and define a model to access it. By the end, you’ll have a working ``books`` table populated with example rows. + +Create the Migrations +===================== + +The migrations let you version-control your database schema by telling what changes to make to the database, and what changes to undo if we need to roll it back. Let’s make one for simple ``authors`` and ``books`` tables. + +Run the Spark command: + +.. code-block:: console + + php spark make:migration CreateAuthorsTable + php spark make:migration CreateBooksTable + +This creates a new file under ``app/Database/Migrations/``. + +Edit the CreateAuthorsTable file to look like this: + +.. literalinclude:: code/004.php + +Each author simply has a name for our purposes. We have made the name a uncommented unique key to prevent duplicates. + +Now edit the CreateBooksTable file to look like this: + +.. literalinclude:: code/005.php + +You'll see this contains a foreign key reference to the ``authors`` table. This allows us to safely associate each book with an author and only maintain author names in one place. + +Now run the migration: + +.. code-block:: console + + php spark migrate + +Now the database has the necessary structure to hold our books and authors. + +Create a seeder +================ + +Seeders let you load sample data for development so you have something to work with right away. In this case, we’ll create a seeder to add some example books and their authors. + +Run: + +.. code-block:: console + + php spark make:seeder BookSeeder + +Edit the file at ``app/Database/Seeds/BookSeeder.php``: + +.. literalinclude:: code/006.php + +This seeder first inserts authors into the ``authors`` table, capturing their IDs, then uses those IDs to insert books into the ``books`` table. + +Then run the seeder: + +.. code-block:: console + + php spark db:seed BookSeeder + +You should see confirmation messages indicating the rows were inserted. + +Create the Book model +===================== + +Models make database access simple and reusable by providing an object-oriented interface to your tables and a fluent method for querying. Let's create a model for the ``authors`` and ``books`` tables. + +Generate one: + +.. code-block:: console + + php spark make:model AuthorModel + php spark make:model BookModel + +Both of the models will be simple extensions of CodeIgniter's base Model class. + +Edit ``app/Models/AuthorModel.php``: + +.. literalinclude:: code/007.php + +Edit ``app/Models/BookModel.php``: + +.. literalinclude:: code/008.php + +This tells CodeIgniter which table to use and which fields can be mass-assigned. + + +In the next section, we’ll use your new model to power a RESTful API controller. +You’ll build the ``/api/books`` endpoint and see how CodeIgniter’s ``Api\ResponseTrait`` makes CRUD operations easy. diff --git a/user_guide_src/source/guides/api/first-endpoint.rst b/user_guide_src/source/guides/api/first-endpoint.rst new file mode 100644 index 000000000000..0a4fa107a601 --- /dev/null +++ b/user_guide_src/source/guides/api/first-endpoint.rst @@ -0,0 +1,118 @@ +.. _ci47-rest-part2: + +Auto Routing & Your First Endpoint +################################## + +.. contents:: + :local: + :depth: 2 + +In this section, we will enable CodeIgniter's *Improved Auto Routing* feature and create a simple JSON endpoint to confirm everything is wired up correctly. + +Why Auto-Routing? +================== + +The previous tutorial introduced you to defined routes manually in ``app/Config/Routes.php``. While this is a powerful and flexible way to define your application's routing, it can be tedious for building RESTful APIs where you may have many endpoints that follow a common pattern. Auto-Routing simplifies this by automatically mapping URL patterns to controller classes and methods based on conventions, and the focus on HTTP verbs works quite well for RESTful APIs. + +Enable Improved Auto Routing +============================ + +By default, Auto-Routing is turned off. The first step is to enable it so your controllers automatically handle REST-style methods. + +Open ``app/Config/Feature.php`` and ensure this flag is **true** (this should be the default): + +.. code-block:: php + + public bool $autoRoutesImproved = true; + +The "Improved" auto router is more secure and reliable than the legacy version, so it's recommended for all new projects. + +Then, in ``app/Config/Routing.php`` confirm auto routing is **on**: + +.. code-block:: php + + public bool $autoRoute = true; + +That's all you need for CodeIgniter to automatically map your controller classes and to URIs like ``GET /api/ping`` or ``POST /api/ping``. + +Create a Ping controller +========================= + +To understand how a basic API endpoint works, let's generate a controller to serve as our first API endpoint. This will provide a simple "ping" response to confirm our setup is correct. + +.. code-block:: console + + php spark make:controller Api/Ping + +This creates ``app/Controllers/Api/Ping.php``. + +Edit the file so it looks like this: + +.. literalinclude:: code/001.php + +Here we: + +- Use the :php:class:`ResponseTrait`, which already includes useful REST helpers such as :php:meth:`respond()` and proper status codes. +- Define a ``getIndex()`` method. The **get** prefix means it responds to ``GET`` requests, and the ``Index`` name means that it will match the base URI (``/api/ping``). + +Test the route +============== + +Start the development server if it isn't running: + +.. code-block:: console + + php spark serve + +Now visit: + +- **Browser:** ``http://localhost:8080/api/ping`` +- **cURL:** ``curl http://localhost:8080/api/ping`` + +Expected response: + +.. code-block:: json + + { + "status": "ok" + } + +Congratulations — that's your first working JSON endpoint! + +Understand how it works +======================= + +When you request ``/api/ping``: + +1. The **Improved Auto Router** finds the ``App\Controllers\Api\Ping`` class. +2. It detects the HTTP verb (``GET``). +3. It calls the corresponding method name: ``getIndex()``. +4. :php:trait:`ResponseTrait` provides helper methods to produce the consistent output. + +Here's how other verbs would map if you added them later: + ++----------------------+--------------------------------+ +| HTTP Verb | Method Name | ++======================+================================+ +| ``GET /api/ping`` | ``getIndex()`` | +| ``POST /api/ping`` | ``postIndex()`` | +| ``DELETE /api/ping`` | ``deleteIndex()`` | ++----------------------+--------------------------------+ + +Content Negotiation with the Format Class +========================================== + +By default, CodeIgniter uses the :php:class:`CodeIgniter\\Format\\Format` class to automatically negotiate the response format. It can return responses in either JSON or XML depending on what the client requests. + +The :php:trait:`ResponseTrait` sets the format to JSON by default. You can change this to XML if desired, but this tutorial will focus on JSON responses. + +.. literalinclude:: code/002.php + +Optional: return more data +========================== + +The respond method allows + +.. literalinclude:: code/003.php + +You now have your first working endpoint and you've managed to test it through both the browser and cURL. In the next section, we'll create our first real database resource. You'll define a **migration**, **seeder**, and **model** for a simple ``books`` table that powers the API's CRUD endpoints. diff --git a/user_guide_src/source/guides/api/index.rst b/user_guide_src/source/guides/api/index.rst new file mode 100644 index 000000000000..f454d5a5748b --- /dev/null +++ b/user_guide_src/source/guides/api/index.rst @@ -0,0 +1,104 @@ +.. _ci47-rest-part1: + +Getting Started with REST APIs +############################## + +.. content:: + :local: + :depth: 2 + +This tutorial will guide you through building a simple RESTful API to manage books using Codeigniter 4. Along the way, you'll leanr the basics of setting up a CodeIgniter project, configuring a database, and creating API endpoints, as well as understand what makes a RESTful API. + +This tutorial will primarily focus on: + +- Auto-routing (Improved) +- Creating JSON API endpoints +- Using the API ResponseTrait for consistent responses +- Basic database operations with Models + +.. tip:: + + While we cover the basics of CodeIgniter, it is assumed that you have at least progressed through the :doc:`First App tutorial <../first-app/index>`. + +.. toctree:: + :hidden: + :titlesonly: + + first-endpoint + database-setup + controller + conclusion + +Getting Up and Running +********************** + +Installing CodeIgniter +====================== + +.. code-block:: console + + composer create-project codeigniter4/appstarter books-api + cd books-api + php spark serve + +Open your browser to ``http://localhost:8080`` and you should see the CodeIgniter welcome page. + +.. note:: + + Keep the server running in one terminal. If you prefer, you can stop it anytime with :kbd:`Ctrl+C` and start again with ``php spark serve``. + +Setting Development Mode +======================== + +Copy the environment file and enable development settings: + +.. code-block:: console + + cp env .env + +Open ``.env`` and make sure this line is **uncommented**: + +.. code-block:: ini + + CI_ENVIRONMENT = development + +You can also use the spark ``env`` command to set the environment for you: + +.. code-block:: console + + php spark env development + +Configure SQLite +================ + +We'll use a single-file SQLite database under ``writable/`` so there's no external setup. + +Open ``.env`` and **uncomment** the database section, then set: + +.. code-block:: ini + + database.default.DBDriver = SQLite3 + database.default.database = database.db + database.default.DBPrefix = + database.default.username = + database.default.password = + database.default.hostname = + database.default.port = + +CodeIgniter will automatically create the SQLite database file if it doesn't exist, but you need to ensure that the ``writable/`` directory is writable by the web server. + +.. warning:: + + On some systems you might need to adjust group/owner or use ``chmod 666`` temporarily during development. Never ship world-writable permissions to production. + + +At this point, you should have a working CodeIgniter 4 project with SQLite configured. + +- The app starts with ``php spark serve`` +- ``CI_ENVIRONMENT`` is set to ``development`` in ``.env`` +- ``writable/database.db`` exists and is writable + +What's next +=========== + +In the next section, we'll enable :doc:`../../general/auto-routing` and create a simple JSON endpoint (``/api/ping``) to see how HTTP verbs map to controller methods in CodeIgniter. diff --git a/user_guide_src/source/tutorial/conclusion.rst b/user_guide_src/source/guides/first-app/conclusion.rst similarity index 100% rename from user_guide_src/source/tutorial/conclusion.rst rename to user_guide_src/source/guides/first-app/conclusion.rst diff --git a/user_guide_src/source/tutorial/create_news_items.rst b/user_guide_src/source/guides/first-app/create_news_items.rst similarity index 100% rename from user_guide_src/source/tutorial/create_news_items.rst rename to user_guide_src/source/guides/first-app/create_news_items.rst diff --git a/user_guide_src/source/tutorial/create_news_items/001.php b/user_guide_src/source/guides/first-app/create_news_items/001.php similarity index 100% rename from user_guide_src/source/tutorial/create_news_items/001.php rename to user_guide_src/source/guides/first-app/create_news_items/001.php diff --git a/user_guide_src/source/tutorial/create_news_items/002.php b/user_guide_src/source/guides/first-app/create_news_items/002.php similarity index 100% rename from user_guide_src/source/tutorial/create_news_items/002.php rename to user_guide_src/source/guides/first-app/create_news_items/002.php diff --git a/user_guide_src/source/tutorial/create_news_items/003.php b/user_guide_src/source/guides/first-app/create_news_items/003.php similarity index 100% rename from user_guide_src/source/tutorial/create_news_items/003.php rename to user_guide_src/source/guides/first-app/create_news_items/003.php diff --git a/user_guide_src/source/tutorial/create_news_items/004.php b/user_guide_src/source/guides/first-app/create_news_items/004.php similarity index 100% rename from user_guide_src/source/tutorial/create_news_items/004.php rename to user_guide_src/source/guides/first-app/create_news_items/004.php diff --git a/user_guide_src/source/tutorial/create_news_items/005.php b/user_guide_src/source/guides/first-app/create_news_items/005.php similarity index 100% rename from user_guide_src/source/tutorial/create_news_items/005.php rename to user_guide_src/source/guides/first-app/create_news_items/005.php diff --git a/user_guide_src/source/tutorial/create_news_items/006.php b/user_guide_src/source/guides/first-app/create_news_items/006.php similarity index 100% rename from user_guide_src/source/tutorial/create_news_items/006.php rename to user_guide_src/source/guides/first-app/create_news_items/006.php diff --git a/user_guide_src/source/tutorial/index.rst b/user_guide_src/source/guides/first-app/index.rst similarity index 100% rename from user_guide_src/source/tutorial/index.rst rename to user_guide_src/source/guides/first-app/index.rst diff --git a/user_guide_src/source/tutorial/news_section.rst b/user_guide_src/source/guides/first-app/news_section.rst similarity index 100% rename from user_guide_src/source/tutorial/news_section.rst rename to user_guide_src/source/guides/first-app/news_section.rst diff --git a/user_guide_src/source/tutorial/news_section/001.php b/user_guide_src/source/guides/first-app/news_section/001.php similarity index 100% rename from user_guide_src/source/tutorial/news_section/001.php rename to user_guide_src/source/guides/first-app/news_section/001.php diff --git a/user_guide_src/source/tutorial/news_section/002.php b/user_guide_src/source/guides/first-app/news_section/002.php similarity index 100% rename from user_guide_src/source/tutorial/news_section/002.php rename to user_guide_src/source/guides/first-app/news_section/002.php diff --git a/user_guide_src/source/tutorial/news_section/003.php b/user_guide_src/source/guides/first-app/news_section/003.php similarity index 100% rename from user_guide_src/source/tutorial/news_section/003.php rename to user_guide_src/source/guides/first-app/news_section/003.php diff --git a/user_guide_src/source/tutorial/news_section/004.php b/user_guide_src/source/guides/first-app/news_section/004.php similarity index 100% rename from user_guide_src/source/tutorial/news_section/004.php rename to user_guide_src/source/guides/first-app/news_section/004.php diff --git a/user_guide_src/source/tutorial/news_section/005.php b/user_guide_src/source/guides/first-app/news_section/005.php similarity index 100% rename from user_guide_src/source/tutorial/news_section/005.php rename to user_guide_src/source/guides/first-app/news_section/005.php diff --git a/user_guide_src/source/tutorial/news_section/006.php b/user_guide_src/source/guides/first-app/news_section/006.php similarity index 100% rename from user_guide_src/source/tutorial/news_section/006.php rename to user_guide_src/source/guides/first-app/news_section/006.php diff --git a/user_guide_src/source/tutorial/news_section/007.php b/user_guide_src/source/guides/first-app/news_section/007.php similarity index 100% rename from user_guide_src/source/tutorial/news_section/007.php rename to user_guide_src/source/guides/first-app/news_section/007.php diff --git a/user_guide_src/source/tutorial/news_section/008.php b/user_guide_src/source/guides/first-app/news_section/008.php similarity index 100% rename from user_guide_src/source/tutorial/news_section/008.php rename to user_guide_src/source/guides/first-app/news_section/008.php diff --git a/user_guide_src/source/tutorial/static_pages.rst b/user_guide_src/source/guides/first-app/static_pages.rst similarity index 100% rename from user_guide_src/source/tutorial/static_pages.rst rename to user_guide_src/source/guides/first-app/static_pages.rst diff --git a/user_guide_src/source/tutorial/static_pages/001.php b/user_guide_src/source/guides/first-app/static_pages/001.php similarity index 100% rename from user_guide_src/source/tutorial/static_pages/001.php rename to user_guide_src/source/guides/first-app/static_pages/001.php diff --git a/user_guide_src/source/tutorial/static_pages/002.php b/user_guide_src/source/guides/first-app/static_pages/002.php similarity index 100% rename from user_guide_src/source/tutorial/static_pages/002.php rename to user_guide_src/source/guides/first-app/static_pages/002.php diff --git a/user_guide_src/source/tutorial/static_pages/003.php b/user_guide_src/source/guides/first-app/static_pages/003.php similarity index 100% rename from user_guide_src/source/tutorial/static_pages/003.php rename to user_guide_src/source/guides/first-app/static_pages/003.php diff --git a/user_guide_src/source/tutorial/static_pages/004.php b/user_guide_src/source/guides/first-app/static_pages/004.php similarity index 100% rename from user_guide_src/source/tutorial/static_pages/004.php rename to user_guide_src/source/guides/first-app/static_pages/004.php diff --git a/user_guide_src/source/guides/index.rst b/user_guide_src/source/guides/index.rst new file mode 100644 index 000000000000..4f7f4b7ef729 --- /dev/null +++ b/user_guide_src/source/guides/index.rst @@ -0,0 +1,23 @@ +###### +Guides +###### + +This section contains various guides to help you get started with CodeIgniter 4 in a project-based manner. + +.. toctree:: + :titlesonly: + + first-app/index + api/index + +**************************** +Build Your First Application +**************************** + +This project walks you through building a basic news application. You will begin by writing the code that can load static pages. Next, you will create a news section that reads news items from a database. Finally, you’ll add a form to create news items in the database. This will give you a good overview of how CodeIgniter works and how to use its main components. + +******************* +Build a RESTful API +******************* + +This guide will help you build a simple RESTful API using CodeIgniter 4. You will learn how to use auto-routing, setup controllers, and handle requests and responses in a RESTful manner. By the end of this guide, you will have a functional API that can perform CRUD operations on a resource. This will introduce you to the basic concepts of a RESTful API and the tools that CodeIgniter provides to facilitate its development. diff --git a/user_guide_src/source/index.rst b/user_guide_src/source/index.rst index 1b850ee44139..cd62d40f2a9e 100644 --- a/user_guide_src/source/index.rst +++ b/user_guide_src/source/index.rst @@ -18,15 +18,15 @@ Getting Started installation/index -**************************** -Build Your First Application -**************************** +******* +Guides +******* .. toctree:: :includehidden: :titlesonly: - tutorial/index + guides/index ************************* Overview & General Topics From 55b2458416aa2ae1c00adb7ec7200e8587cf0a0e Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Fri, 21 Nov 2025 17:18:43 +0100 Subject: [PATCH 2/2] cs-fix --- user_guide_src/source/guides/api/code/003.php | 6 +++--- user_guide_src/source/guides/api/code/004.php | 4 ++-- user_guide_src/source/guides/api/code/005.php | 10 +++++----- user_guide_src/source/guides/api/code/009.php | 6 +----- user_guide_src/source/guides/api/code/010.php | 4 ++-- user_guide_src/source/guides/api/code/011.php | 6 +++--- user_guide_src/source/guides/api/code/012.php | 16 ++++++++-------- user_guide_src/source/guides/api/code/013.php | 4 ++-- user_guide_src/source/guides/api/code/014.php | 2 +- 9 files changed, 27 insertions(+), 31 deletions(-) diff --git a/user_guide_src/source/guides/api/code/003.php b/user_guide_src/source/guides/api/code/003.php index 39395ebd0731..043a781cb7ee 100644 --- a/user_guide_src/source/guides/api/code/003.php +++ b/user_guide_src/source/guides/api/code/003.php @@ -13,9 +13,9 @@ class Ping extends BaseController public function getIndex() { return $this->respond([ - 'status' => 'ok', - 'time' => date('c'), - 'version'=> CodeIgniter::CI_VERSION, + 'status' => 'ok', + 'time' => date('c'), + 'version' => CodeIgniter::CI_VERSION, ]); } } diff --git a/user_guide_src/source/guides/api/code/004.php b/user_guide_src/source/guides/api/code/004.php index 06fcfe2cd4e7..389639db478d 100644 --- a/user_guide_src/source/guides/api/code/004.php +++ b/user_guide_src/source/guides/api/code/004.php @@ -15,9 +15,9 @@ public function up() 'auto_increment' => true, ], 'name' => [ - 'type' => 'VARCHAR', + 'type' => 'VARCHAR', 'constraint' => '255', - 'null' => false, + 'null' => false, ], 'created_at' => [ 'type' => 'DATETIME', diff --git a/user_guide_src/source/guides/api/code/005.php b/user_guide_src/source/guides/api/code/005.php index 41c431c12a0a..54042520c8c0 100644 --- a/user_guide_src/source/guides/api/code/005.php +++ b/user_guide_src/source/guides/api/code/005.php @@ -15,14 +15,14 @@ public function up() 'auto_increment' => true, ], 'title' => [ - 'type' => 'VARCHAR', + 'type' => 'VARCHAR', 'constraint' => '255', - 'null' => false, + 'null' => false, ], 'author_id' => [ - 'type' => 'INTEGER', - 'unsigned' => true, - 'null' => false, + 'type' => 'INTEGER', + 'unsigned' => true, + 'null' => false, ], 'year' => [ 'type' => 'INTEGER', diff --git a/user_guide_src/source/guides/api/code/009.php b/user_guide_src/source/guides/api/code/009.php index 65f5c49fa3d7..dd0a6ab679a4 100644 --- a/user_guide_src/source/guides/api/code/009.php +++ b/user_guide_src/source/guides/api/code/009.php @@ -2,8 +2,8 @@ namespace App\Controllers\Api; -use CodeIgniter\Api\ResponseTrait; use App\Controllers\BaseController; +use CodeIgniter\Api\ResponseTrait; class Books extends BaseController { @@ -19,7 +19,6 @@ class Books extends BaseController */ public function getIndex(?int $id = null) { - // } /** @@ -29,7 +28,6 @@ public function getIndex(?int $id = null) */ public function putIndex(int $id) { - // } /** @@ -39,7 +37,6 @@ public function putIndex(int $id) */ public function postIndex() { - // } /** @@ -49,6 +46,5 @@ public function postIndex() */ public function deleteIndex(int $id) { - // } } diff --git a/user_guide_src/source/guides/api/code/010.php b/user_guide_src/source/guides/api/code/010.php index 31ed3179c5b2..2bc67a89e09a 100644 --- a/user_guide_src/source/guides/api/code/010.php +++ b/user_guide_src/source/guides/api/code/010.php @@ -9,8 +9,8 @@ class AuthorTransformer extends BaseTransformer public function toArray(mixed $resource): array { return [ - 'id' => $resource['id'], - 'name' => $resource['name'], + 'id' => $resource['id'], + 'name' => $resource['name'], ]; } } diff --git a/user_guide_src/source/guides/api/code/011.php b/user_guide_src/source/guides/api/code/011.php index 6a020b172a8c..8a455de8207a 100644 --- a/user_guide_src/source/guides/api/code/011.php +++ b/user_guide_src/source/guides/api/code/011.php @@ -9,9 +9,9 @@ class BookTransformer extends BaseTransformer public function toArray(mixed $resource): array { return [ - 'id' => $resource['id'], - 'title' => $resource['title'], - 'year' => $resource['year'], + 'id' => $resource['id'], + 'title' => $resource['title'], + 'year' => $resource['year'], ]; } diff --git a/user_guide_src/source/guides/api/code/012.php b/user_guide_src/source/guides/api/code/012.php index 8f4478fff6b5..0c39d9b6539b 100644 --- a/user_guide_src/source/guides/api/code/012.php +++ b/user_guide_src/source/guides/api/code/012.php @@ -2,9 +2,9 @@ namespace App\Controllers\Api; -use CodeIgniter\Api\ResponseTrait; use App\Controllers\BaseController; use App\Transformers\BookTransformer; +use CodeIgniter\Api\ResponseTrait; class Books extends BaseController { @@ -20,14 +20,14 @@ class Books extends BaseController */ public function getIndex(?int $id = null) { - $model = model('BookModel'); + $model = model('BookModel'); $transformer = new BookTransformer(); // If an ID is provided, fetch a single record if ($id !== null) { $book = $model->withAuthorInfo()->find($id); - if (!$book) { + if (! $book) { return $this->failNotFound('Book not found'); } @@ -47,7 +47,7 @@ public function getIndex(?int $id = null) */ public function putIndex(int $id) { - $data = $this->request->getRawInput(); + $data = $this->request->getRawInput(); $model = model('BookModel'); $rules = [ @@ -56,13 +56,13 @@ public function putIndex(int $id) 'year' => 'required|integer|greater_than_equal_to[2000]|less_than_equal_to[' . date('Y') . ']', ]; - if (!$this->validate($rules)) { + if (! $this->validate($rules)) { return $this->failValidationErrors($this->validator->getErrors()); } $model = model('BookModel'); - if (!$model->find($id)) { + if (! $model->find($id)) { return $this->failNotFound('Book not found'); } @@ -88,7 +88,7 @@ public function postIndex() 'year' => 'required|integer|greater_than_equal_to[2000]|less_than_equal_to[' . date('Y') . ']', ]; - if (!$this->validate($rules)) { + if (! $this->validate($rules)) { return $this->failValidationErrors($this->validator->getErrors()); } @@ -109,7 +109,7 @@ public function deleteIndex(int $id) { $model = model('BookModel'); - if (!$model->find($id)) { + if (! $model->find($id)) { return $this->failNotFound('Book not found'); } diff --git a/user_guide_src/source/guides/api/code/013.php b/user_guide_src/source/guides/api/code/013.php index 165b37ec71e1..156a192a45b3 100644 --- a/user_guide_src/source/guides/api/code/013.php +++ b/user_guide_src/source/guides/api/code/013.php @@ -6,7 +6,7 @@ class BookModel extends Model { - protected string $table = 'books'; + protected string $table = 'books'; protected array $allowedFields = ['title', 'author_id', 'year']; /** @@ -16,6 +16,6 @@ class BookModel extends Model public function withAuthorInfo() { return $this->select('books.*, authors.id as author_id, authors.name as author_name') - ->join('authors', 'books.author_id = authors.id'); + ->join('authors', 'books.author_id = authors.id'); } } diff --git a/user_guide_src/source/guides/api/code/014.php b/user_guide_src/source/guides/api/code/014.php index 51011e7280f7..4b0bc409f728 100644 --- a/user_guide_src/source/guides/api/code/014.php +++ b/user_guide_src/source/guides/api/code/014.php @@ -9,6 +9,6 @@ class BookModel extends Model public function withAuthorInfo() { return $this->select('books.*, authors.name as author_name') - ->join('authors', 'books.author_id = authors.id'); + ->join('authors', 'books.author_id = authors.id'); } }