From 46c1374006d8f1d6664106e88b0f40bcda205c6e Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 11 May 2016 15:10:39 +0200 Subject: [PATCH] Document PSR-4 autoloading for apps --- developer_manual/app/classloader.rst | 37 +++++++++++++++++++++++++--- developer_manual/app/container.rst | 14 +++++------ developer_manual/app/controllers.rst | 2 +- developer_manual/app/startapp.rst | 3 ++- developer_manual/app/testing.rst | 2 +- developer_manual/app/tutorial.rst | 36 +++++++++++++-------------- 6 files changed, 63 insertions(+), 31 deletions(-) diff --git a/developer_manual/app/classloader.rst b/developer_manual/app/classloader.rst index 450a8145e..07d941481 100644 --- a/developer_manual/app/classloader.rst +++ b/developer_manual/app/classloader.rst @@ -4,9 +4,40 @@ Classloader .. sectionauthor:: Bernhard Posselt -The classloader is provided by ownCloud and loads all your classes automatically. The only thing left to include by yourself are 3rdparty libraries. Those should be loaded in :file:`appinfo/application.php`. +The classloader is provided by ownCloud and loads all your classes automatically. The only thing left to include by yourself are 3rdparty libraries. Those should be loaded in :file:`lib/AppInfo/Application.php`. -The classloader works like this: +.. versionadded:: 9.1 + +PSR-4 Autoloading +----------------- + +Since ownCloud 9.1 there is a PSR-4 autoloader in place. The namespace **\\OCA\\MyApp** +is mapped to :file:`/apps/myapp/lib/`. Afterwards normal PSR-4 rules apply, so +a folder is a namespace section in the same casing and the class name matches +the file name. + +If your appid can not be turned into the namespace by uppercasing the first +character, you can specify it in your **appinfo/info.xml** by providing a field +called **namespace**. The required namespace is the one which comes after the +top level namespace **OCA\\**, e.g.: for **OCA\\MyBeautifulApp\\Some\\OtherClass** +the needed namespace would be **MyBeautifulApp** and would be added to the +info.xml in the following way: + + .. code-block:: xml + + + + MyBeautifulApp + + + +A second PSR-4 root is available when running tests. **\\OCA\\MyApp\\Tests** is +thereby mapped to :file:`/apps/myapp/tests/`. + +Legacy Autoloading +------------------ + +The legacy classloader, deprecated since 9.1, is still in place and works like this: * Take the full qualifier of a class:: @@ -33,4 +64,4 @@ The classloader works like this: require_once '/apps/myapp/controller/pagecontroller.php'; -**In other words**: In order for the PageController class to be autoloaded, the class **\\OCA\\MyApp\\Controller\\PageController** needs to be stored in the :file:`/apps/myapp/controller/pagecontroller.php` \ No newline at end of file +**In other words**: In order for the PageController class to be autoloaded, the class **\\OCA\\MyApp\\Controller\\PageController** needs to be stored in the :file:`/apps/myapp/controller/pagecontroller.php` diff --git a/developer_manual/app/container.rst b/developer_manual/app/container.rst index 1908af7e4..c4afeda08 100644 --- a/developer_manual/app/container.rst +++ b/developer_manual/app/container.rst @@ -52,10 +52,10 @@ Using a container ================= Passing dependencies into the constructor rather than instantiating them in the constructor has the following drawback: Every line in the source code where **new AuthorMapper** is being used has to be changed, once a new constructor argument is being added to it. -The solution for this particular problem is to limit the **new AuthorMapper** to one file, the container. The container contains all the factories for creating these objects and is configured in :file:`appinfo/application.php`. +The solution for this particular problem is to limit the **new AuthorMapper** to one file, the container. The container contains all the factories for creating these objects and is configured in :file:`lib/AppInfo/Application.php`. -To add the app's classes simply open the :file:`appinfo/application.php` and use the **registerService** method on the container object: +To add the app's classes simply open the :file:`lib/AppInfo/Application.php` and use the **registerService** method on the container object: .. code-block:: php @@ -155,7 +155,7 @@ Use automatic dependency assembly (recommended) =============================================== .. versionadded:: 8 -Since ownCloud 8 it is possible to omit the **appinfo/application.php** and use automatic dependency assembly instead. +Since ownCloud 8 it is possible to omit the **lib/AppInfo/Application.php** and use automatic dependency assembly instead. How does automatic assembly work -------------------------------- @@ -202,8 +202,8 @@ How does it affect the request lifecycle * A request comes in * All apps' **routes.php** files are loaded - * If a **routes.php** file returns an array, and an **appname/appinfo/application.php** exists, include it, create a new instance of **\\OCA\\AppName\\AppInfo\\Application.php** and register the routes on it. That way a container can be used while still benefitting from the new routes behavior - * If a **routes.php** file returns an array, but there is no **appname/appinfo/application.php**, create a new \\OCP\\AppFramework\\App instance with the app id and register the routes on it + * If a **routes.php** file returns an array, and an **appname/lib/AppInfo/Application.php** exists, include it, create a new instance of **\\OCA\\AppName\\AppInfo\\Application.php** and register the routes on it. That way a container can be used while still benefitting from the new routes behavior + * If a **routes.php** file returns an array, but there is no **appname/lib/AppInfo/Application.php**, create a new \\OCP\\AppFramework\\App instance with the app id and register the routes on it * A request is matched for the route, e.g. with the name **page#index** * The appropriate container is being queried for the entry PageController (to keep backwards compability) @@ -222,7 +222,7 @@ The only thing that needs to be done to add a route and a controller method is n ['name' => 'page#index', 'url' => '/', 'verb' => 'GET'], ]]; -**myapp/appinfo/controller/pagecontroller.php** +**myapp/appinfo/lib/Controller/PageController.php** .. code-block:: php @@ -239,7 +239,7 @@ The only thing that needs to be done to add a route and a controller method is n } } -There is no need to wire up anything in **appinfo/application.php**. Everything will be done automatically. +There is no need to wire up anything in **lib/AppInfo/Application.php**. Everything will be done automatically. How to deal with interface and primitive type parameters diff --git a/developer_manual/app/controllers.rst b/developer_manual/app/controllers.rst index 5dbc73a83..afb371267 100644 --- a/developer_manual/app/controllers.rst +++ b/developer_manual/app/controllers.rst @@ -4,7 +4,7 @@ Controllers .. sectionauthor:: Bernhard Posselt -Controllers are used to connect :doc:`routes ` with app logic. Think of it as callbacks that are executed once a request has come in. Controllers are defined inside the **controller/** directory. +Controllers are used to connect :doc:`routes ` with app logic. Think of it as callbacks that are executed once a request has come in. Controllers are defined inside the **lib/Controller/** directory. To create a controller, simply extend the Controller class and create a method that should be executed on a request: diff --git a/developer_manual/app/startapp.rst b/developer_manual/app/startapp.rst index 12e73d8cd..35b56ed0c 100644 --- a/developer_manual/app/startapp.rst +++ b/developer_manual/app/startapp.rst @@ -25,9 +25,10 @@ App architecture The following directories have now been created: * **appinfo/**: Contains app metadata and configuration -* **controller/**: Contains the controllers * **css/**: Contains the CSS * **js/**: Contains the JavaScript files +* **lib/Controller/**: Contains the controllers +* **lib/**: Contains the other class files of your app * **templates/**: Contains the templates * **tests/**: Contains the tests diff --git a/developer_manual/app/testing.rst b/developer_manual/app/testing.rst index 78da21064..a3f77dbbc 100644 --- a/developer_manual/app/testing.rst +++ b/developer_manual/app/testing.rst @@ -57,7 +57,7 @@ would look like this: .. code-block:: php 'page#index', 'url' => '/', 'verb' => 'GET'] ]]; -This route calls the controller **OCA\\OwnNotes\\PageController->index()** method which is defined in **ownnotes/controller/pagecontroller.php**. The controller returns a :doc:`template `, in this case **ownnotes/templates/main.php**: +This route calls the controller **OCA\\OwnNotes\\PageController->index()** method which is defined in **ownnotes/lib/Controller/PageController.php**. The controller returns a :doc:`template `, in this case **ownnotes/templates/main.php**: .. note:: @NoAdminRequired and @NoCSRFRequired in the comments above the method turn off security checks, see :doc:`controllers` @@ -110,7 +110,7 @@ This route calls the controller **OCA\\OwnNotes\\PageController->index()** metho } -Since the route which returns the intial HTML has been taken care of, the controller which handles the AJAX requests for the notes needs to be set up. Create the following file: **ownnotes/controller/notecontroller.php** with the following content: +Since the route which returns the intial HTML has been taken care of, the controller which handles the AJAX requests for the notes needs to be set up. Create the following file: **ownnotes/lib/Controller/NoteController.php** with the following content: .. code-block:: php @@ -274,7 +274,7 @@ To create the tables in the database, the :doc:`version tag ` in **ownnote Reload the page to trigger the database migration. -Now that the tables are created we want to map the database result to a PHP object to be able to control data. First create an :doc:`entity ` in **ownnotes/db/note.php**: +Now that the tables are created we want to map the database result to a PHP object to be able to control data. First create an :doc:`entity ` in **ownnotes/lib/Db/Note.php**: .. code-block:: php @@ -305,7 +305,7 @@ Now that the tables are created we want to map the database result to a PHP obje We also define a **jsonSerializable** method and implement the interface to be able to transform the entity to JSON easily. -Entities are returned from so called :doc:`Mappers `. Let's create one in **ownnotes/db/notemapper.php** and add a **find** and **findAll** method: +Entities are returned from so called :doc:`Mappers `. Let's create one in **ownnotes/lib/Db/NoteMapper.php** and add a **find** and **findAll** method: .. code-block:: php @@ -339,7 +339,7 @@ Connect Database & Controllers ============================== The mapper which provides the database access is finished and can be passed into the controller. -You can pass in the mapper by adding it as a type hinted parameter. ownCloud will figure out how to :doc:`assemble them by itself `. Additionally we want to know the userId of the currently logged in user. Simply add a **$UserId** parameter to the constructor (case sensitive!). To do that open **ownnotes/controller/notecontroller.php** and change it to the following: +You can pass in the mapper by adding it as a type hinted parameter. ownCloud will figure out how to :doc:`assemble them by itself `. Additionally we want to know the userId of the currently logged in user. Simply add a **$UserId** parameter to the constructor (case sensitive!). To do that open **ownnotes/lib/Controller/NoteController.php** and change it to the following: .. code-block:: php @@ -446,7 +446,7 @@ Let's say our app is now on the app store and and we get a request that we shoul The filesystem API is quite different from the database API and throws different exceptions, which means we need to rewrite everything in the **NoteController** class to use it. This is bad because a controller's only responsibility should be to deal with incoming Http requests and return Http responses. If we need to change the controller because the data storage was changed the code is probably too tightly coupled and we need to add another layer in between. This layer is called **Service**. -Let's take the logic that was inside the controller and put it into a separate class inside **ownnotes/service/noteservice.php**: +Let's take the logic that was inside the controller and put it into a separate class inside **ownnotes/lib/Service/NoteService.php**: .. code-block:: php @@ -527,7 +527,7 @@ Let's take the logic that was inside the controller and put it into a separate c } -Following up create the exceptions in **ownnotes/service/serviceexception.php**: +Following up create the exceptions in **ownnotes/lib/Service/ServiceException.php**: .. code-block:: php @@ -538,7 +538,7 @@ Following up create the exceptions in **ownnotes/service/serviceexception.php**: class ServiceException extends Exception {} -and **ownnotes/service/notfoundexception.php**: +and **ownnotes/lib/Service/NotFoundException.php**: .. code-block:: php @@ -550,7 +550,7 @@ and **ownnotes/service/notfoundexception.php**: Remember how we had all those ugly try catches that where checking for **DoesNotExistException** and simply returned a 404 response? Let's also put this into a reusable class. In our case we chose a `trait `_ so we can inherit methods without having to add it to our inheritance hierarchy. This will be important later on when you've got controllers that inherit from the **ApiController** class instead. -The trait is created in **ownnotes/controller/errors.php**: +The trait is created in **ownnotes/lib/Controller/Errors.php**: .. code-block:: php @@ -671,12 +671,12 @@ Unit Tests ---------- A unit test is a test that tests a class in isolation. It is very fast and catches most of the bugs, so we want many unit tests. -Because ownCloud uses :doc:`Dependency Injection ` to assemble your app, it is very easy to write unit tests by passing mocks into the constructor. A simple test for the update method can be added by adding this to **ownnotes/tests/unit/controller/NoteControllerTest.php**: +Because ownCloud uses :doc:`Dependency Injection ` to assemble your app, it is very easy to write unit tests by passing mocks into the constructor. A simple test for the update method can be added by adding this to **ownnotes/tests/Unit/Controller/NoteControllerTest.php**: .. code-block:: php `_ we can run the tests inside **o phpunit -.. note:: You need to adjust the **ownnotes/tests/unit/controller/PageControllerTest** file to get the tests passing: remove the **testEcho** method since that method is no longer present in your **PageController** and do not test the user id parameters since they are not passed anymore +.. note:: You need to adjust the **ownnotes/tests/Unit/Controller/PageControllerTest** file to get the tests passing: remove the **testEcho** method since that method is no longer present in your **PageController** and do not test the user id parameters since they are not passed anymore Integration Tests ----------------- @@ -813,12 +813,12 @@ Integration tests are slow and need a fully working instance but make sure that In our case we want to create an integration test for the udpate method without mocking out the **NoteMapper** class so we actually write to the existing database. -To do that create a new file called **ownnotes/tests/integration/NoteIntegrationTest.php** with the following content: +To do that create a new file called **ownnotes/tests/Integration/NoteIntegrationTest.php** with the following content: .. code-block:: php ` allows other apps such as Android or iPhone apps to a Because we put our logic into the **NoteService** class it is very easy to reuse it. The only pieces that need to be changed are the annotations which disable the CSRF check (not needed for a REST call usually) and add support for `CORS `_ so your API can be accessed from other webapps. -With that in mind create a new controller in **ownnotes/controller/noteapicontroller.php**: +With that in mind create a new controller in **ownnotes/lib/Controller/NoteApiController.php**: .. code-block:: php @@ -1002,12 +1002,12 @@ You can test the API by running a GET request with **curl**:: curl -u user:password http://localhost:8080/index.php/apps/ownnotes/api/0.1/notes -Since the **NoteApiController** is basically identical to the **NoteController**, the unit test for it simply inherits its tests from the **NoteControllerTest**. Create the file **ownnotes/tests/unit/controller/NoteApiControllerTest.php**: +Since the **NoteApiController** is basically identical to the **NoteController**, the unit test for it simply inherits its tests from the **NoteControllerTest**. Create the file **ownnotes/tests/Unit/Controller/NoteApiControllerTest.php**: .. code-block:: php