A Simple Nextcloud App Tutorial
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 )
# PRESENTING CODE
# PRESENTING CODE
~/.bash_aliases
alias occ='docker compose exec -u www-data nextcloud ./occ'
~/.bashrc
if [ -f ~/.bash_aliases ]; then
. ~/.bash_aliases
fi
# PRESENTING CODE
occ config:system:get debug
occ config:system:set debug 1
'debug' => true,
config/config.php
# PRESENTING CODE
How to use xdebug on VSCode or Codium?
volumes/nextcloud
Run > Start debugging
"launch.json
inside configuration named as Listen for Xdebug
:"pathMappings": {
"/var/www/html": "${workspaceFolder}"
}
# 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
# 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>
# 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);
}
}
# 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/"
}
}
}
# 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
# PRESENTING CODE
occ app:enable wifi_password
# PRESENTING CODE
# PRESENTING CODE
# PRESENTING CODE
composer require --dev bamarni/composer-bin-plugin
{
...
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": true
}
}
}
# 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"
}
# 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
# 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"
}
# 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
# 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
# PRESENTING CODE
Show me the code
# PRESENTING CODE
.github
# PRESENTING CODE
Show me the code
# PRESENTING CODE
<?php
return [
'routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
],
];
# PRESENTING CODE
authorApi#someMethod
Name
# PRESENTING CODE
authorApi someMethod
Split at #
and uppercase the first letter of the left part:
# PRESENTING CODE
AuthorApiController someMethod
Append Controller to the first part:
# 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");
}
}
# PRESENTING CODE
Hello World
# 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>
# PRESENTING CODE
img/app.svg
img/app-dark.svg
# 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...
# PRESENTING CODE
return [
'resources' => [
'author' => ['url' => '/authors'],
],
'routes' => [
// your other routes here
],
];
can be abbreviated by using the resources key:
# PRESENTING CODE
# 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
# 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'
}
}
# 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"
}
}
# 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")
}
}
# 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:
# PRESENTING CODE
Session
Cookies
and more...
# PRESENTING CODE
# 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...
# 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:
# PRESENTING CODE
Middleware is logic that is run before and after each request
# PRESENTING CODE
Communicate between different aspects of the Nextcloud eco system.
Implement a Listener to intercept events
Disptatch events in your app
# 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
# PRESENTING CODE
occ migrations:migrate <appid> <version>
# PRESENTING CODE
Migration was named:
Version<version>Date<date>
Example:
Version16000Date20221116163301.php
# PRESENTING CODE
Version
I recommend to use Semantic Version without dots.
Example:
v1.2.3
will be:
10203
# 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");
}
}
# 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");
}
}
# PRESENTING CODE
Load in frontend:
Press F12 and check the input with all initial states
# PRESENTING CODE
# PRESENTING CODE
Redes sociais:
( VitorMattos ou VitorMattosRJ )