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
fi

Command 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.json inside configuration named as Listen 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_password

Quality 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 init
default:
  autoload:
    '': '%paths.base%/tests/features/bootstrap'
  suites:
    default:
      paths:
        - '%paths.base%/tests/features'
  extensions:
    PhpBuiltin\Server:
      verbose: false
      rootDir: /var/www/html
      host: localhost

behat.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 World

First 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.svg

Routes / 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 )