Merge pull request #2414 from owncloud/psr-4-for-apps

Document PSR-4 autoloading for apps
This commit is contained in:
Morris Jobke
2016-05-11 15:23:43 +02:00
6 changed files with 63 additions and 31 deletions

View File

@@ -4,9 +4,40 @@ Classloader
.. sectionauthor:: Bernhard Posselt <dev@bernhard-posselt.com>
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
<?xml version="1.0"?>
<info>
<namespace>MyBeautifulApp</namespace>
<!-- other options here ... -->
</info>
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`
**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`

View File

@@ -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

View File

@@ -4,7 +4,7 @@ Controllers
.. sectionauthor:: Bernhard Posselt <dev@bernhard-posselt.com>
Controllers are used to connect :doc:`routes <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 <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:

View File

@@ -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

View File

@@ -57,7 +57,7 @@ would look like this:
.. code-block:: php
<?php
// tests/storage/AuthorStorageTest.php
// tests/Storage/AuthorStorageTest.php
namespace OCA\MyApp\Tests\Storage;
class AuthorStorageTest extends \Test\TestCase {

View File

@@ -81,7 +81,7 @@ On the server side we need to register a callback that is executed once the requ
['name' => '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 <templates>`, 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 <templates>`, 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 <info>` 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 <database>` 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 <database>` 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 <database>`. Let's create one in **ownnotes/db/notemapper.php** and add a **find** and **findAll** method:
Entities are returned from so called :doc:`Mappers <database>`. 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 <container>`. 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 <container>`. 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 <http://php.net/manual/en/language.oop5.traits.php>`_ 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 <container>` 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 <container>` 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
<?php
namespace OCA\OwnNotes\Controller;
namespace OCA\OwnNotes\Tests\Unit\Controller;
use PHPUnit_Framework_TestCase;
@@ -738,7 +738,7 @@ We can and should also create a test for the **NoteService** class:
.. code-block:: php
<?php
namespace OCA\OwnNotes\Service;
namespace OCA\OwnNotes\Tests\Unit\Service;
use PHPUnit_Framework_TestCase;
@@ -805,7 +805,7 @@ If `PHPUnit is installed <https://phpunit.de/>`_ 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
<?php
namespace OCA\OwnNotes\Controller;
namespace OCA\OwnNotes\Tests\Integration\Controller;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\App;
@@ -888,7 +888,7 @@ A :doc:`RESTful API <api>` 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 <https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_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
<?php
namespace OCA\OwnNotes\Controller;
namespace OCA\OwnNotes\Tests\Unit\Controller;
require_once __DIR__ . '/NoteControllerTest.php';