App Tutorial
A Simple Nextcloud App Tutorial
Quem sou eu?
Realizador de sonhos desde 2003
Amante de opensource
Palestrante
PHP Zend Certified Engineer ( ZEND024235 )
PHPRio ( https://telegram.me/phprio )
CTO LibreCode
Redes sociais: ( VitorMattos ou VitorMattosRJ )
Command line tools
# PRESENTING CODE
OCC
Command line tools
> alias
# PRESENTING CODE
~/.bash_aliases
alias occ='docker compose exec -u www-data nextcloud ./occ'~/.bashrc
if [ -f ~/.bash_aliases ]; then
. ~/.bash_aliases
fiCommand line tools
> debug
# PRESENTING CODE
occ config:system:get debug
occ config:system:set debug 1'debug' => true,config/config.php
Command line tools
> xdebug
# PRESENTING CODE
How to use xdebug on VSCode or Codium?
- Open the IDE on folder
volumes/nextcloud - setup the extension PHP Debug or PHP Extension Pack
- Press F5 to start debugging or go to "
Run > Start debugging" - Create a launch.json file to PHP
- Add the follow to your
launch.jsoninside configuration named asListen for Xdebug:
"pathMappings": {
"/var/www/html": "${workspaceFolder}"
}Types of apps
App architecture
# PRESENTING CODE
-
appinfo/: Contains app metadata and configuration
-
css/: Contains the CSS
-
img/: Contains icons and images
-
js/: Contains the JavaScript files
-
lib/: Contains the PHP class files of your app
-
src/: Contains the source code of your vue.js app
-
templates/: Contains the templates
-
tests/: Contains the tests
Essentials
> appinfo/info.xml
# PRESENTING CODE
<?xml version="1.0"?>
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>wifi_password</id>
<name>Wifi - Password</name>
<summary>Create, manage and share wi-fi passwords</summary>
<description>Create, manage and share wi-fi passwords using qrcode</description>
<version>1.0.0-alfa</version>
<licence>agpl</licence>
<author>Vinicius Reis</author>
<author>Vitor Mattos</author>
<namespace>WifiPassword</namespace>
<category>files</category>
<category>tools</category>
<bugs>https://github.com/vinicius73/wifi-password/issues</bugs>
<dependencies>
<nextcloud min-version="22" max-version="25" />
</dependencies>
</info>
Essentials
> lib/AppInfo/Application.php
# PRESENTING CODE
<?php
declare(strict_types=1);
namespace OCA\WifiPassword\AppInfo;
use OCP\AppFramework\App;
class Application extends App {
public const APP_ID = 'wifi_password';
public function __construct() {
parent::__construct(self::APP_ID);
}
}
Essentials
composer init
# PRESENTING CODE
{
"name": "vinicius73/wifi-password",
"type": "project",
"license": "AGPL",
"authors": [
{
"name": "Vinicius Reis",
"email": "luiz.vinicius73@gmail.com"
},
{
"name": "Vitor Mattos",
"email": "vitor@php.rio"
}
],
"autoload": {
"psr-4": {
"Root\\Teste\\": "src/"
}
}
}
Essentials
composer init
# PRESENTING CODE
{
"name": "vinicius73/wifi-password",
"type": "project",
"license": "AGPL",
"authors": [
{
"name": "Vinicius Reis",
"email": "luiz.vinicius73@gmail.com"
},
{
"name": "Vitor Mattos",
"email": "vitor@php.rio"
}
],
"autoload": {
"psr-4": {
"OCA\\WifiPassword\\": "lib/"
}
}
}
Info: fix the namespace
Enabling the app
# PRESENTING CODE
occ app:enable wifi_passwordQuality assurance
# PRESENTING CODE
TDD
Quality assurance
# PRESENTING CODE
Quality assurance
composer-bin-plugin
# PRESENTING CODE
composer require --dev bamarni/composer-bin-plugin{
...
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": true
}
}
}Quality assurance
phpcs
# PRESENTING CODE
composer bin coding-standard require --dev \
nextcloud/coding-standard"scripts": {
"lint": "find . -name \\*.php -not -path './vendor/*' -not -path './build/*' -print0 | xargs -0 -n1 php -l",
"cs:check": "php-cs-fixer fix --dry-run --diff",
"cs:fix": "php-cs-fixer fix"
}Quality assurance
phpcs
# PRESENTING CODE
<?php
declare(strict_types=1);
require_once './vendor-bin/coding-standard/vendor/autoload.php';
use Nextcloud\CodingStandard\Config;
$config = new Config();
$config
->getFinder()
->ignoreVCSIgnored(true)
->notPath('l10n')
->notPath('src')
->notPath('vendor')
->in(__DIR__);
return $config;
.php-cs-fixer.dist.php
PS: add .php-cs-fixer.cache to .gitignore
Quality assurance
psalm
# PRESENTING CODE
composer bin psalm require --dev vimeo/psalm
composer bin psalm require --dev \
nextcloud/ocp:dev-master"scripts": {
"lint": "find . -name \\*.php -not -path './vendor/*' -not -path './build/*' -print0 | xargs -0 -n1 php -l",
"cs:check": "php-cs-fixer fix --dry-run --diff",
"cs:fix": "php-cs-fixer fix",
"psalm": "psalm",
"psalm:update-baseline": "psalm --threads=1 --update-baseline --set-baseline=tests/psalm-baseline.xml",
"psalm:clear": "psalm --clear-cache && psalm --clear-global-cache"
}Quality assurance
# PRESENTING CODE
<?xml version="1.0"?>
<psalm
errorLevel="5"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="lib" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
<issueHandlers>
<UndefinedClass>
<errorLevel type="suppress">
<referencedClass name="OC\*" />
<referencedClass name="OC" />
</errorLevel>
</UndefinedClass>
</issueHandlers>
</psalm>
psalm.xml
Quality assurance
Behat
# PRESENTING CODE
composer bin behat require --dev libresign/nextcloud-behat
vendor/bin/behat initdefault:
autoload:
'': '%paths.base%/tests/features/bootstrap'
suites:
default:
paths:
- '%paths.base%/tests/features'
extensions:
PhpBuiltin\Server:
verbose: false
rootDir: /var/www/html
host: localhostbehat.yml
Quality assurance
# PRESENTING CODE
Show me the code

Continuous Integration
GitHub Actions
# PRESENTING CODE
.github

Continuous Integration
# PRESENTING CODE
Show me the code

First page
> appinfo/routes.php
# PRESENTING CODE
<?php
return [
'routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
],
];
Routes / API endpoints
connecting controller to route
# PRESENTING CODE
authorApi#someMethod
Name
# PRESENTING CODE
authorApi someMethod
Split at #
and uppercase the first letter of the left part:
Routes / API endpoints
connecting controller to route
# PRESENTING CODE
AuthorApiController someMethod
Append Controller to the first part:
Routes / API endpoints
connecting controller to route
First page
> lib/Controller/PageController.php
# PRESENTING CODE
<?php
declare(strict_types=1);
namespace OCA\WifiPassword\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;
use OCA\WifiPassword\AppInfo\Application;
class PageController extends Controller {
/**
* @NoAdminRequired
* @NoCSRFRequired
*/
public function index(): TemplateResponse
{
return new TemplateResponse(Application::APP_ID, "page-main");
}
}
First page
> templates/page-main.php
# PRESENTING CODE
Hello WorldFirst page
Add to menu
# PRESENTING CODE
...
<dependencies>
<nextcloud min-version="26" max-version="26" />
</dependencies>
<navigations>
<navigation>
<name>Wifi Password</name>
<route>wifi_password.page.index</route>
</navigation>
</navigations>
</info>
First page
Add to menu
# PRESENTING CODE
img/app.svg
img/app-dark.svgRoutes / API endpoints
# PRESENTING CODE
return [
'routes' => [
['name' => 'author#index', 'url' => '/authors', 'verb' => 'GET'],
['name' => 'author#show', 'url' => '/authors/{id}', 'verb' => 'GET'],
['name' => 'author#create', 'url' => '/authors', 'verb' => 'POST'],
['name' => 'author#update', 'url' => '/authors/{id}', 'verb' => 'PUT'],
['name' => 'author#destroy', 'url' => '/authors/{id}', 'verb' => 'DELETE'],
// your other routes here
],
];this...
API endpoints
# PRESENTING CODE
return [
'resources' => [
'author' => ['url' => '/authors'],
],
'routes' => [
// your other routes here
],
];can be abbreviated by using the resources key:
API
Tip
# PRESENTING CODE
Getting request parameters
# PRESENTING CODE
Parameters can be passed in many ways:
-
Extracted from the URL using curly braces like {key} inside the URL (see Routing)
-
Appended to the URL as a GET request (e.g. ?something=true)
-
application/x-www-form-urlencoded from a form or jQuery
-
application/json from a POST, PATCH or PUT request
Getting request parameters
# PRESENTING CODE
<?php
namespace OCA\MyApp\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Response;
class PageController extends Controller {
/**
* @param int $id
*/
public function doSomething(int $id, string $name='john', string $job='author'): Response {
// GET ?id=3&job=Developer
// $id = 3
// $name = 'john'
// $job = 'Developer'
}
}Getting request parameters
json
# PRESENTING CODE
POST /index.php/apps/myapp/authors
Content-Type: application/json
{
"name": "test",
"number": 3,
"publisher": true,
"customFields": {
"mail": "test@example.com",
"address": "Somewhere"
}
}Getting request parameters
json
# PRESENTING CODE
<?php
namespace OCA\MyApp\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Response;
class PageController extends Controller {
public function create(string $name, int $number, string $publisher, array $customFields): Response {
// $name = 'test'
// $number = 3
// $publisher = true
// $customFields = array("mail" => "test@example.com", "address" => "Somewhere")
}
}Getting anything else
# PRESENTING CODE
<?php
namespace OCA\MyApp\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Response;
use OCP\IRequest;
class PageController extends Controller {
public function someMethod(): Response {
$type = $this->request->getHeader('Content-Type'); // $_SERVER['HTTP_CONTENT_TYPE']
$cookie = $this->request->getCookie('myCookie'); // $_COOKIES['myCookie']
$file = $this->request->getUploadedFile('myfile'); // $_FILES['myfile']
$env = $this->request->getEnv('SOME_VAR'); // $_ENV['SOME_VAR']
}
}If you want more, look the interface:
More about controllers
# PRESENTING CODE
Session
Cookies
and more...
Dependency injection
# PRESENTING CODE
Dependency injection
# PRESENTING CODE
<?php
use OCP\IDBConnection;
// without dependency injection
class AuthorMapper {
private IDBConnection $db;
public function __construct() {
$this->db = new Db();
}
}Don’t put new dependencies in your constructor or methods but pass them in.
So this...
Dependency injection
# PRESENTING CODE
<?php
use OCP\IDBConnection;
// with dependency injection
class AuthorMapper {
private IDBConnection $db;
public function __construct(IDBConnection $db) {
$this->db = $db;
}
}would turn into this by using Dependency Injection:
Middlewares
# PRESENTING CODE
Middleware is logic that is run before and after each request
Events
# PRESENTING CODE
Communicate between different aspects of the Nextcloud eco system.
Implement a Listener to intercept events
Disptatch events in your app
Migrations
> occ commands
# PRESENTING CODE
-
migrations:execute: Executes a single migration version manually
-
migrations:generate: Create a new migration file
-
migrations:migrate: Execute all pending migrations of an app.
-
migrations:status: View the status of a set of migrations
Migrations
> creating new migration
# PRESENTING CODE
occ migrations:migrate <appid> <version>Migrations
> creating new migration
# PRESENTING CODE
Migration was named:
Version<version>Date<date>
Example:
Version16000Date20221116163301.php
Migrations
> creating new migration
# PRESENTING CODE
Version
I recommend to use Semantic Version without dots.
Example:
v1.2.3
will be:
10203
Frontend
> Adding scripts
# PRESENTING CODE
<?php
declare(strict_types=1);
namespace OCA\WifiPassword\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;
use OCA\WifiPassword\AppInfo\Application;
class PageController extends Controller {
/**
* @NoAdminRequired
* @NoCSRFRequired
*/
public function index(): TemplateResponse
{
Util::addScript(Application::APP_ID, 'page-main');
return new TemplateResponse(Application::APP_ID, "page-main");
}
}
Frontend
> Initial state
# PRESENTING CODE
class PageController extends Controller {
private IInitialState $initialState;
public function __construct(
IRequest $request,
IInitialState $initialState
) {
parent::__construct(Application::APP_ID, $request);
$this->initialState = $initialState;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*/
public function index(): TemplateResponse
{
Util::addScript(Application::APP_ID, 'page-main');
$this->initialState->provideInitialState('config', 'value');
return new TemplateResponse(Application::APP_ID, "page-main");
}
}
Frontend
> Initial state
# PRESENTING CODE
Load in frontend:
Press F12 and check the input with all initial states
Frontend
> Vue Components
# PRESENTING CODE

Analyzing the code
# PRESENTING CODE
Obrigado!
Redes sociais:
( VitorMattos ou VitorMattosRJ )



