Laravel: costruire tabelle, model, controller e creare dati con Faker

In questo articolo su Laravel popoleremo le tabelle di dati di test con il celebre plugin Faker: creeremo dapprima due tabelle database con una relazione 1 a molti con le migrazioni di Laravel, creeremo il model che le rappresenta, il Controller che le gestisce e le .

Creazione delle migrazioni

Creiamo le migrazioni due migrazioni per queste tabelle collegate da una relazione 1 a molti:

Diagramma  E/R per utilizzare Faker
Diagramma E/R per utilizzare Faker

Utilizziamo il comando make:model che ci consente di fare tutto in un colpo: creare la tabella, il model, il controller, e le factory per poter poplare con dati di test le tabelle.

Convenzione: creiamo il modello con il nome al singolare. Eloquent creerà la tabella con il nome al plurale, il Controller con il nome del model, le factories con il nome del model.

Laravel gestisce correttamente anche il fatto che Company è un plurale irregolare nella lingua inglese 🙂

$ php artisan make:model Company -mfscr
Model created successfully.
Factory created successfully.
Created Migration: 2021_11_24_182740_create_companies_table
Seeder created successfully.
Controller created successfully.

Questo comando crea:

  • un model (m) vuoto di nome Company;
  • una factory (f) vuota di nome CompanyFactory che ci servirà per definire i tipi di dati di test associare ai record della tabella;
  • una migrazione con la creazionedella tabella companies;
  • un seeder (s) per riempire di dati la tabella
  • un controller (c) di nome CompanyController
  • un resource (r) che popola il Controller con metodi standard CRUD (index(), create(), show(), edit(), update() e destroy())

Personalizzazione della migrazione

Dobbiamo definire come sarà la tabella:

class CreateCompaniesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('companies', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('website');
            $table->string('email')->unique();
            $table->float('latitude');
            $table->float('longitude');

            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('companies');
    }
}

Facciamo lo stesso per CompanyType:

$ php artisan make:model CompanyType -mfscr
Model created successfully.
Factory created successfully.
Created Migration: 2021_11_24_183431_create_company_types_table
Seeder created successfully.
Controller created successfully.

Sono stati creati i model, i controller con i 4 metodi standard, i factory.

Lancio della migrazione

Successivamente lanciamo le migrazioni:

$ php artisan migrate
Migrating: 2021_11_24_184611_create_company_types_table
Migrated:  2021_11_24_184611_create_company_types_table (136.22ms)
Migrating: 2021_11_24_184622_create_companies_table
Migrated:  2021_11_24_184622_create_companies_table (468.33ms)

Sono state create le tabelle e le relazioni:

faker_dbeaver_tables
Dbeaver mostra tabelle e relazionetra esse

Popolamento con dati di test con Faker

Per ultimo lanciamo il seeder che popola di dati la tabella Company Types. Lo facciamo rispettando la prirità che per rispettare la relazione di chiave esterna ci porta a popolare prima la tabella madre (company_types) e poi la figlia (companies).

Occorre dapprima scrivere cosa vogliamo che faccia il Seeder.

Per CompanyType definiamo innazitutto la factory (che definisce come devono essere popolati i campi)

<?php
// <APP>/database/factories/CompanyTypeFactory.php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class CompanyTypeFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'name' => $this->faker->name(),
        ];
    }
}

In questo caso abbiamo unsolo campo per cui utilizziamo un metodo “name” che genererà un nome proprio di persona. Non è questo il caso, ma Faker da la possibilità di generare anche indirizzi random, numeri di telefono, url, email latitudini e longitudini e molto altro ancora.

E poi scriviamo il seeder (definiamo quanti record devono essere creati, in questo caso 3):

<?php

// <APP>/database/seeders/CompanyTypeSeeder.php

namespace Database\Seeders;

use App\Models\CompanyType;
use Illuminate\Database\Seeder;

class CompanyTypeSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        CompanyType::factory()
            ->count(3)
            ->create();
    }
}

Infine per Company specifichiamo una chiave esterna fissa a 1

<?php

// <APP>/database/factories/CompanyFactory.php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class CompanyFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'name' => $this->faker->name(),
            'website' => $this->faker->url,
            'email' => $this->faker->email,
            'latitude' => $this->faker->latitude,
            'longitude' => $this->faker->longitude,
            'company_type_id' => 1,
        ];
    }
}

e popoliamo con 10 record:

<?php

// <APP>/database/seeders/CompanySeeder.php

namespace Database\Seeders;

use App\Models\Company;
use Illuminate\Database\Seeder;

class CompanySeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        Company::factory()
            ->count(10)
            ->create();
    }
}

Per lanciare il comando si torna alla console e si scrive:

$ php artisan db:seed --class=CompanySeederSe

Se non specifichiamo la classe, lancerà tutti i seeder che abbiamo scritto.

Alla fine questo è il risultato:

Come Faker popola i record della tabella companies
Come Faker popola i record della tabella companies

È possibile anche specificare una localizzazione diversa dalla standard inglese, si va nel file di configurazione dell’applicazione <APP>/config/app.php e si modifica così la linea:

    'faker_locale' => 'it_IT',

Con la nuova localizzazione il risultato è questo

Laravel: Faker con localizzzione italiana
Laravel: Faker con localizzzione italiana

Risorse

CSRF come ti altero info riservate nel sito

Hacking technique csrf
L’attacco di tipo CSRF

Come funziona CSRF

Facciamo un po’ di personaggi e interpreti di questo articolo (CSRF = Cross Site Request Forgery = falsa richiesta tra siti):

  • la vittima: un utente che abitualmente visita e si autentica in
  • un sito che contiene un difetto per il quale si merita l’appellativo di sitovulnerabile.com;
  • un attaccante che è molto bravo e conduce
  • un sitomalevolo.com


Supponiamo che il servizio Web vulnerabile sia quello che consente agli utenti di modificare la loro email utilizzando solo un cookie per l’autenticazione e l’invio di un modulo.

Il sitomalevolo.com sembra innocuo ma ha al suo interno una pagina web con un programma JavaScript che invia di nascosto una richiesta al sitovulnerabile.com con una mail fasulla nel modulo e chiama semplicemente l’URL vulnerabile.

Facciamo un esempio. Il servizio del sitovulnerabile.com consente di modificare l’email dell’utente autenticato attraverso una form HTML: ebbene, l’Attaccante pubblica in sitomalevolo.com un form simile, che invia la stessa informazione al sitovulnerabile.com

<form action="https://sitovulnerabile.com/user/email" method="POST" onLoad="this.submit()">
    <input type="hidden" name="email" value="malicious-email@example.com">
    <input type="hidden" name="user_id" value="12345">
</form>

Requisito 1: l’attacco avrà successo se e solo se l’utente12345 esiste,

Poi l’Attaccante siede sulla riva del fiume ad attendere che passi la vittima, ovvero l’utente che viene in qualche modo indotto a visitare il sitomalevolo.com. Per esempio l’Attaccante adesca la vittima sottoponendola a SPAM e inviandogli una mail “Grossi premi alla Lotteria Web”; all’interno di questa mail mette un link che punta alla form sopra. Inoltre la form è auto inviante quindi la vittima il sito malevolo non lo vede neanche passare (può farlo se apre gli strumenti per sviluppatori Alt+Shift+I e controlla la scheda Network).

Requisito 2: la vittima dev’essersi precedentemente autenticata sul sito vulnerabile, per poter consegnare al sitovulnerabile.com, quando ritorna dopo l’hop nel sito malevolo, il cookie di sessione.

La richiesta quindi parte dal browser della vittima che attualmente era atterrato sul sito malevolo e invoca il sito vulnerabile chiamando la API di cambio mail e passandogli l’id_utente e il cookie di sessione.

Queste informazioni sono sufficienti a far sì che il sito vulnerabile aggiorni davvero la mail a insaputa dell’utente. Il danno può emergere anche dopo molto tempo, quando l’utente cercherà di entrare nuovamente nel sito vulnerabile (sarebbe meglio dire, a questo punto, sito vulnerato) ma questo gli risponde “User not known”.

Rimedio

Occorre aggiungere una terza informazione che certifica qual è l’origine del dato.

Laravel genera automaticamente un “token” CSRF per ogni sessione utente attiva gestita dall’applicazione. Questo token viene utilizzato per verificare che l’utente autenticato sia la persona che effettua effettivamente le richieste all’applicazione. Poiché questo token è archiviato nella sessione dell’utente e cambia ogni volta che la sessione viene rigenerata, un’applicazione dannosa non è in grado di accedervi.

È possibile accedere al token CSRF della sessione corrente tramite la sessione della richiesta o tramite l’helper csrf_token:

use Illuminate\Http\Request;

Route::get('/token', function (Request $request) {
    $token = $request->session()->token();

    $token = csrf_token();

    // ...
});

L’attaccante non può utilizzare questo metodo perché non gli ritornerebbe alcunché visto che non è autenticato (non ha e non avrà mai il cookie di sessione che invece è nel browser della vittima). E dovrebbe farlo per poter inserire nella form il valore del _csrf.

Ogni volta, infatti, che definisci un modulo HTML “POST”, “PUT”, “PATCH” o “DELETE” nella tua applicazione, dovresti includere un campo CSRF _token nascosto nel modulo in modo che il middleware di protezione CSRF possa convalidare la richiesta. Per comodità, puoi utilizzare la direttiva Blade @csrf per generare il campo di input del token nascosto:

<form method="POST" action="/user/email">
    @csrf

    <!-- Equivalente a... -->
    <input type="hidden" name="_token" value="{{ csrf_token() }}" />
</form>

Il middleware App\Http\Middleware\VerifyCsrfToken, incluso per impostazione predefinita nel gruppo middleware Web, verificherà automaticamente che il token nell’input della richiesta corrisponda al token archiviato nella sessione. Quando questi due token corrispondono, sappiamo che l’utente autenticato è quello che ha avviato la richiesta.

Riferimenti

Test Laravel

Laravel Test
Laravel test

In questo articolo porto un esempio di test con Laravel.

Laravel consente di eseguire due tipi di test: Unit per piccoli test su singole funzioni del controller o del model. Feature per testare intere funzionalità dell’applicativo dalla richesta HTTP alla produzione dell’output.

Laravel crea i test in due directory:

<APP>/tests/Feature
<APP>/test/Unit

Per creare un test (di defult si va sulle Features):

$ php artisan make:test UserTest [--unit]

Aggiungiamo --unit solo se vogliamo fare un test di unità (per un singolo metodo per esempio). Il nome del test possiamo sceglierlo come vogliamo, basta che ci ricordi cosa fa. Il nome finisce sempre per “Test”.

Faccio girare i test

$ php artisan test

   PASS  Tests\Unit\ExampleTest
  ✓ example

   PASS  Tests\Feature\ComplexTest
  ✓ example

   PASS  Tests\Feature\ExampleTest
  ✓ example

  Tests:  3 passed
  Time:   0.16s

Infatti quando creo i test la prima volta, lui aggiunge già un file tests/Feature/ExampleTest.php, io ne ho aggiunto un secondo che fa un test standard di risposta all’URL http://myapp.local/ (ho configurato il virtualhost Apache).

In questo esempio costruirò una classe per la gestione del tipo “numero complesso”, la classe Complex.

In una prima versione definisco il costruttore che crea l’oggetto Complex a partire dalla parte reale e immaginaria e poi scrivo qualche setter e qualche getter per consentire accesso e modifica alle variabili private $real e $imag:

<?php

namespace App;

use PHPUnit\Util\Exception;

class Complex
{
    private float $real;
    private float $imag;
    private float $modulus;
    private float $phase;

    public function __construct($x, $y)
    {
        $this->setReal($x);
        $this->setImag($y);
        $this->setModulus();
        $this->setPhase();

    }

    public function getReal(): float
    {
        return $this->real;
    }

    public function getImag(): float
    {
        return $this->imag;
    }

    public function getModulus(): float
    {
        return $this->modulus;
    }

    public function getPhase(): float
    {
        return $this->phase;
    }

    public function setReal($x)
    {
        $this->real = ($x == null) ? 0 : $x;
    }

    public function setImag($y)
    {
        $this->imag = ($y == null) ? 0 : $y;
    }

E scrivo alcuni test per mettere alla prova questi medodi: lo faccio nel file <APP>/test/Feature/ComplexTest.php;

class ComplexTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function test_example()
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }

    public function test_setReal()
    {
        $r = 1;
        $z = new Complex(null, null);
        $z->setReal($r);
        $x = $z->getReal();

        $this->assertTrue($x == $r);

    }

    public function test_setImag()
    {
        $i = 1;
        $z = new Complex(null, null);
        $z->setImag($i);
        $y = $z->getImag();

        $this->assertTrue($y == $i);

    }

Lo spirito è il seguente: provo il metodo e confronto il risultato con quello che mi aspetto che faccia. Se non lo fa c’è un problema da risolvere. Per fare questo c’è il metodo assertTrue() che testa che una condizione sia vera. Ad esempio nel metodotest_setReal() viene creato un numero complesso nullo (con parte reale e immaginaria nulle) e poi viene impostata la parte reale a 1 con setReal(). Successivamente la leggo con getReal(); mi aspetto che il risultato sia 1. Per questo utilizzo il metodo assertTrue($x == $r).

Scrivo poi i metodi che impostano il modulo e la fase (già dentro al costruttore così le ho a disposizione fin dall’inizio) e con un metodo print() stampo il numero nella rappresenzazione cartesiana z = x + i y.

Per eseguire il test, lancio da riga di comando:

$ php artisan test
  PASS  Tests\Unit\ExampleTest
  ✓ example

   PASS  Tests\Feature\ComplexTest
  ✓ example
  ✓ set real
  ✓ set imag
  ✓ set modulus
  ✓ set phase
  ✓ print
  ✓ real print
  ✓ imag print
  ✓ modulus
  ✓ phase

   PASS  Tests\Feature\ExampleTest
  ✓ example

  Tests:  12 passed
  Time:   0.14s

$ 

Se invece qualcosa va storto si vede un output del genere:

$ php artisan test

   PASS  Tests\Unit\ExampleTest
  ✓ example

   FAIL  Tests\Feature\ComplexTest
  ✓ example
  ⨯ set real
  ⨯ set imag
  ✓ set modulus
  ✓ set phase
  ✓ print
  ✓ real print
  ✓ imag print
  ✓ modulus
  ✓ phase

   PASS  Tests\Feature\ExampleTest
  ✓ example

  ---

  • Tests\Feature\ComplexTest > set real
   PHPUnit\Util\Exception 

  Phase undefined

  at app/Complex.php:68
     64▕                 $res = M_PI_2;
     65▕             } else if ($this->imag < 0) {
     66▕                 $res = - M_PI_2;
     67▕             } else {
  ➜  68▕                 throw new Exception('Phase undefined');
     69▕             }
     70▕         }
     71▕ 
     72▕         return $this->phase = $res;

  1   app/Complex.php:19
      App\Complex::setPhase()

  2   tests/Feature/ComplexTest.php:27
      App\Complex::__construct()

e abbiamo tutti i dettagli per capire dove dobbiamo correggere.

Automazione dei test

Un aspetto ancora più bello di Laravel è che possiamo automatizzare i test quando vogliamo, ad esempio prima di un commit su Git.

Ma lo faccio in un altro articolo.

Riferimenti

Problema password MySQL 8

MySQL 8.0
MySQL 8.0

Con la versione 8 di MySQL, per la precisione:

8.0.27-0ubuntu0.21.04.1

ho incontrato anche questo fastidioso errore mentre tentavo di collegarmi al database con lo user grantato di quel database utilizzando DBeaver:

Unable to load authentication plugin 'caching_sha2_password'.

Ho trovato in un forum questa soluzione, entrando come root

$ mysql -u root -p logisticmapper
mysql> ALTER USER 'logmap_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'xxxxx';
Query OK, 0 rows affected (0,02 sec)

Ora funziona:

$ mysql -u logmap_user -p logisticmapper
mysql>

Riferimenti

Una curiosità di geometria

Geometria euclidea: I primi due poligoni della successione che tende al cerchio.
Geometria euclidea: I primi due poligoni della successione che tende al cerchio.

Da tempo immemorabile so,o credevo di sapere, il affto di geometria secondo il quale “il cerchio è un poligono con infiniti lati” ma ho sempre avuto paura, o non ho mai avuto abbastanza voglia, di dimostrarlo.

Una pulsione irresistibile mi ha preso e ho fatto sto benedetto calcolo. Sì, si può calcolare la lunghezza ella circonferenza e l’area del cerchio con un’operazione di passagio al limite.

L’articoletto è in allegato. Molto divertimento, come sempre.

Nuovo progetto Laravel senza Docker/Sail

Piccolo tutorial per creare una nuova applicazione Laravel senza l’uso di Docker o Sail.

Installare composer

Composer è uno strumento per la gestione delle dipendenze in PHP. Permette di dichiarare le librerie da cui dipende il tuo progetto e le gestirà per te (installazione o aggiornamento che sia).

Per esempio se utilizzi Faker per popolare di dati di prova il database della tua applicazione, compser ti solleva dal compito di installarea posteriori Faker se non c’è o di aggiornarlo se hai una versione vecchia.

Nella pagina di download c’è lo script PHP da linea di comando per installare Composer. Ovviamente dobbiamo già avere installato una versione di PHP a bordo. Io ho ancora PHP 7.4.16 (cli), mentre attualmente PHP è già alla versione 8.

Dal mio ultimo progetto PHP è passato un po’ di tempo ho questo messaggio:

$ composer create-project laravel/laravel example-app
Warning from https://packagist.org: Support for Composer 1 is deprecated and some packages will not be available. You should upgrade to Composer 2. See https://blog.packagist.com/deprecating-composer-1-support/

Quindi procedo all’upgrade. Tra l’altro gli improvements raggiunti con l’ultima versione sono davvero notevoli:

Laravel composer 2.0
Laravel composer 2.0

Per cui si procede all’update.

$ composer self-update

Un paio di inghippi:

[RuntimeException]
SHA384 is not supported by your openssl extension, could not verify the phar file integrity

Occorre allora rimuovere le versioni precedenti e installare l’ultima versione di Composer:

Rimozione della versione installata e sostituzione con la nuova:

$ sudo rm -f /usr/local/bin/composer
$ sudo curl -s https://getcomposer.org/installer | php
$ sudo mv composer.phar /usr/local/bin/composer

Creare una nuova applicazione Laravel

Utilizzando Composer possiamo creare una nuova applicazione Laravel:

$ composer create-project laravel/laravel logisticmapper
Creating a "laravel/laravel" project at "./logisticmapper"
Installing laravel/laravel (v8.6.5)
  - Downloading laravel/laravel (v8.6.5)
  - Installing laravel/laravel (v8.6.5): Extracting archive
Created project in /home/marcob/IdeaProjects/PHP/camon/logisticmapper
> @php -r "file_exists('.env') || copy('.env.example', '.env');"
Loading composer repositories with package information
Updating dependencies
Lock file operations: 111 installs, 0 updates, 0 removals
  - Locking asm89/stack-cors (v2.0.3)
  - Locking brick/math (0.9.3)
  - Locking dflydev/dot-access-data (v3.0.1)
	...
  - Locking webmozart/assert (1.10.0)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 111 installs, 0 updates, 0 removals
  - Downloading doctrine/inflector (2.0.4)
  - Downloading symfony/polyfill-ctype (v1.23.0)
  - Downloading symfony/polyfill-php80 (v1.23.1)
	...
  - Downloading phpunit/phpunit (9.5.10)
  - Installing doctrine/inflector (2.0.4): Extracting archive
  - Installing doctrine/lexer (1.2.1): Extracting archive
  - Installing symfony/polyfill-ctype (v1.23.0): Extracting archive
	...
  - Installing phpunit/phpunit (9.5.10): Extracting archive
70 package suggestions were added by new dependencies, use `composer suggest` to see details.
Generating optimized autoload files
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover --ansi
Discovered Package: facade/ignition
Discovered Package: fruitcake/laravel-cors
...
Discovered Package: nunomaduro/collision
Package manifest generated successfully.
78 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
> @php artisan vendor:publish --tag=laravel-assets --ansi
No publishable resources for tag [laravel-assets].
Publishing complete.
> @php artisan key:generate --ansi
Application key set successfully.
$ cd logisticmapper
$ ls -l
totale 388
drwxrwxr-x 12 marcob marcob   4096 nov  8 15:21 ./
drwxrwxr-x  3 marcob marcob   4096 nov  8 15:21 ../
drwxrwxr-x  7 marcob marcob   4096 ott 26 17:20 app/
-rwxr-xr-x  1 marcob marcob   1686 ott 26 17:20 artisan*
drwxrwxr-x  3 marcob marcob   4096 ott 26 17:20 bootstrap/
-rw-rw-r--  1 marcob marcob   1737 ott 26 17:20 composer.json
-rw-rw-r--  1 marcob marcob 291109 nov  8 15:21 composer.lock
drwxrwxr-x  2 marcob marcob   4096 ott 26 17:20 config/
drwxrwxr-x  5 marcob marcob   4096 ott 26 17:20 database/
-rw-rw-r--  1 marcob marcob    258 ott 26 17:20 .editorconfig
-rw-rw-r--  1 marcob marcob    950 nov  8 15:22 .env
-rw-rw-r--  1 marcob marcob    899 ott 26 17:20 .env.example
-rw-rw-r--  1 marcob marcob    111 ott 26 17:20 .gitattributes
-rw-rw-r--  1 marcob marcob    207 ott 26 17:20 .gitignore
-rw-rw-r--  1 marcob marcob    473 ott 26 17:20 package.json
-rw-rw-r--  1 marcob marcob   1202 ott 26 17:20 phpunit.xml
drwxrwxr-x  2 marcob marcob   4096 ott 26 17:20 public/
-rw-rw-r--  1 marcob marcob   3999 ott 26 17:20 README.md
drwxrwxr-x  6 marcob marcob   4096 ott 26 17:20 resources/
drwxrwxr-x  2 marcob marcob   4096 ott 26 17:20 routes/
-rw-rw-r--  1 marcob marcob    563 ott 26 17:20 server.php
drwxrwxr-x  5 marcob marcob   4096 ott 26 17:20 storage/
-rw-rw-r--  1 marcob marcob    194 ott 26 17:20 .styleci.yml
drwxrwxr-x  4 marcob marcob   4096 ott 26 17:20 tests/
drwxrwxr-x 44 marcob marcob   4096 nov  8 15:22 vendor/
-rw-rw-r--  1 marcob marcob    559 ott 26 17:20 webpack.mix.js

$ php artisan serve
Starting Laravel development server: http://127.0.0.1:8000
[Mon Nov  8 15:27:19 2021] PHP 7.4.16 Development Server (http://127.0.0.1:8000) started

Puntando il browser sull’url visualizzato si ottiene

Laravel nuova applicazione
Laravel nuova applicazione

Artisan è l’interfaccia a riga di comando inclusa in Laravel. Artisan esiste alla radice della tua applicazione come script artisan e fornisce una serie di comandi utili che possono aiutarti mentre crei la tua applicazione. Per visualizzare un elenco di tutti i comandi Artisan disponibili, puoi utilizzare il comando list:

$ php artisan list
Laravel Framework 6.18.8

Usage:
  command [options] [arguments]

Options:
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
      --env[=ENV]       The environment the command should run under
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Available commands:
  clear-compiled       Remove the compiled class file
  down                 Put the application into maintenance mode
  env                  Display the current framework environment
  help                 Displays help for a command
  inspire              Display an inspiring quote
...

E via molte altre righe. È uno script molto ricco di comandi, consente di fare molte cose.

Il comando serve, come visto sopra, permette di utilizzare l’applicazione da http://127.0.0.1:8000.

Possiamo anche creare un VirtualHost Apache attraverso il quale l’applicazione risponda invece ad un indirizzo fittizio del tipo http://example-app.local/.

I VirtualHost sono definiti nella mia installazione di Apache sotto /etc/apache2/sites-available/:

<VirtualHost *:80>
        DocumentRoot /var/www/html/example-app/public/
        ServerName example-app.local
        Options Indexes FollowSymLinks
        <Directory /var/www/html/example-app//public/>
                AllowOverride All
                Require all granted
        </Directory>
        ErrorLog ${APACHE_LOG_DIR}/error_app.log
        CustomLog ${APACHE_LOG_DIR}/access_app.log combined
</VirtualHost>

Abbiamo anche specializzato la produzione dei log di accesso/errore di Apache per la nostra applicazione.

Apache va riavviato per rileggere i file di configurazione:

$ sudo service apache2 restart

Dobbiamo anche definire nel DNS, o nel file host locale del computer in cui stiamo sviluppando, una nuova entry per risolvere il nome host in un indirizzo IP; nel secondo caso:

127.0.0.1    example-app.local

Definizione del model

Il modello è una rappresentazione dello spazio dei dati nell’applicazione, che deve regolarne le dipendenze relazionali e le operazioni su di essi (CRUD).

Per implementare un modello abbiamo bisogno dell’ORM (Object Relationship Mapper) che permette di mappare precisamente il modello ad oggetti PHP con lo strato business (dati) realizzato con un DBMS.

L’ORM di Laravel si chama Eloquent ed è molto potente. Per creare una tabella dobbiamo innanzitutto fornire a Laravel le coordinate del database. Creiamo innanzituto il database:

mysql> create database logisticmapper;
Query OK, 1 row affected (0,02 sec)
mysql> create user 'logmap_user'@'localhost' identified by 'l0gm4pus3r';
Query OK, 0 rows affected (0,03 sec)
mysql> GRANT ALL ON logisticmapper.* TO 'logmap_user'@'localhost';
Query OK, 0 rows affected (0,03 sec)

Dopodiché mettiamo i parametri nel file .env

...
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=logsticmapper
DB_USERNAME=logmap_user
DB_PASSWORD=l0gm4pus3r
...

Creazione di una tabella

Dopo possiamo cominciare a creare le tabelle

$ php artisan make:migration create_client_table
Created Migration: 2021_11_08_145944_create_client_table

Ora definamo la tabella attraverso Eloquent: il comando precedente è una “migrazione”: significa una operazione di definizione di un oggetto Database e del simultaneo collegamento con lo spazio delle calsse PHP. Editando il file 2021_11_08_145944_create_client_table vediamo al suo interno una classe già predisposta da Laravel che noi dobbiamo implementare aggiungendo i campi, i vincoli e le relazioni che ci servono:

class CreateClientTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('client', function (Blueprint $table) {
            $table->id()->autoIncrement();
            $table->string('name');
            $table->string('website')->nullable();
            $table->string('email')->unique();
            $table->string('mainPhoneNo');
            $table->float('latitude')->nullable();
            $table->float('longitude')->nullable();
            $table->timestamps();
            $table->softDeletes();
            $table->index(['name']);

        });
    }
...

Le calssi elle migrazioni hanno due metodi standard: up() e down(); il primo crea la tabella, il secondo la elimina. Viene invocato uno o l’altro a seconda del comando di migrazione.

Ora, se facciamo girare la migrazione si otterrà l’effetto di creare una tabella nel database:

$ php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (281.55ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (587.28ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (465.73ms)
Migrating: 2019_12_14_000001_create_personal_access_tokens_table
Migrated:  2019_12_14_000001_create_personal_access_tokens_table (718.36ms)
Migrating: 2021_11_08_145944_create_client_table
Migrated:  2021_11_08_145944_create_client_table (257.00ms)

Posso vedere la tabella nel db:

$ mysql -u logmap_user -p logisticmapper
mysql> desc client;
+-------------+-----------------+------+-----+---------+----------------+
| Field       | Type            | Null | Key | Default | Extra          |
+-------------+-----------------+------+-----+---------+----------------+
| id          | bigint unsigned | NO   | PRI | NULL    | auto_increment |
| name        | varchar(255)    | NO   | MUL | NULL    |                |
| website     | varchar(255)    | YES  |     | NULL    |                |
| email       | varchar(255)    | NO   | UNI | NULL    |                |
| mainPhoneNo | varchar(255)    | NO   |     | NULL    |                |
| latitude    | double(8,2)     | YES  |     | NULL    |                |
| longitude   | double(8,2)     | YES  |     | NULL    |                |
| created_at  | timestamp       | YES  |     | NULL    |                |
| updated_at  | timestamp       | YES  |     | NULL    |                |
| deleted_at  | timestamp       | YES  |     | NULL    |                |
+-------------+-----------------+------+-----+---------+----------------+
10 rows in set (0,01 sec)

Per eliminarla, si torna indietro col comando rollback:

$ php artisan migrate:rollback

e verrà lanciato il metodo down()

Creazione di una tabella con una relazione

Una seconda tabella potrà avere un vincolo relazionale con la precedente; supponiamo di avere bisogno di una tabella di veicoli che fanno parte del parco automezzi del Cliente:

$ php artisan make:migration create_vehicle_table
Created Migration: 2021_11_10_154349_create_vehicle_table

Implementiamo i campi della tabella:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateVehicleTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('vehicle', function (Blueprint $table) {
            $table->id();
            $table->string('model');
            $table->string('licensePlate');
            $table->integer('year');
            $table->bigInteger('client_id')->unsigned();
            $table->foreign('client_id')->references('id')->on('client');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('vehicle');
    }
}

Nota bene: nella definizione dell’identificativo del record, la convenzione con Eloquent è che se dichiariamo un campo id, per Eloquent sarà implicitamente:

  • integer
  • not null
  • auto incrementing

cioè verrò dichiara auto_increment nella definizione in DDL.

È comunque consentito di derogare dalla convenzione quando si vuole (esempio, una chiave che non si chiama id, che non è autoincrement e che non è un numero intero).

In particolare, il metodo $table->foreign() consente di creare la relazione di chiave esterna. lanciamo la mirgrazione:

$ php artisan migrate
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (179.97ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (177.75ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (156.99ms)
Migrating: 2019_12_14_000001_create_personal_access_tokens_table
Migrated:  2019_12_14_000001_create_personal_access_tokens_table (359.57ms)
Migrating: 2021_11_08_145944_create_client_table
Migrated:  2021_11_08_145944_create_client_table (792.39ms)
Migrating: 2021_11_10_154349_create_vehicle_table
Migrated:  2021_11_10_154349_create_vehicle_table (748.10ms)

Attenzione!

Nella nuova versione di Laravel, le chiavi primarie sono definite come binginteger unsigned. Qualunque campo che debba riferirsi a questi campi va dichiarato allo stesso modo, biginteger unsigned. Vedi ad esempio l’attributo vehicle.client_id che ospita la chiave esterna di client.id.

Se guardiamo nel database vedremo:

mysql> desc vehicle;
+--------------+-----------------+------+-----+---------+----------------+
| Field        | Type            | Null | Key | Default | Extra          |
+--------------+-----------------+------+-----+---------+----------------+
| id           | bigint unsigned | NO   | PRI | NULL    | auto_increment |
| model        | varchar(255)    | NO   |     | NULL    |                |
| licensePlate | varchar(255)    | NO   |     | NULL    |                |
| year         | int             | NO   |     | NULL    |                |
| client_id    | bigint unsigned | NO   | MUL | NULL    |                |
| created_at   | timestamp       | YES  |     | NULL    |                |
| updated_at   | timestamp       | YES  |     | NULL    |                |
+--------------+-----------------+------+-----+---------+----------------+

E alla fine la creazione del modello

Artisan creera la classe collegandola alla tabella del database:

$ php artisan make:model Client --migration
Model created successfully.
Created Migration: 2021_11_10_164311_create_clients_table

La nuova classe la troviamo in app/Models/Client.php:

All’inizio è veramente neat:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Client extends Model
{
    use HasFactory;
}

Errore CURL su invocazione php_cli

CURL è una libreria clienti per effettuare connessioni

Errore CURL da linea di comando

Qualsiasi comando che invoca il client PHP, ad esempio:

$ php -v

mi ritorna questo errore:

PHP Warning: PHP Startup: Unable to load dynamic library 'curl.so' (tried: /usr/lib/php/20190902/curl.so (/usr/lib/php/20190902/curl.so: undefined symbol: curl_mime_addpart, version CURL_OPENSSL_4), /usr/lib/php/20190902/curl.so.so (/usr/lib/php/20190902/curl.so.so: undefined symbol: curl_mime_addpart, version CURL_OPENSSL_4)) in Unknown on line 0

Per fixarlo ho eliminato questo link smbolico:

$ cd /usr/local/lib
$ ll
totale 14360
drwxr-xr-x  9 root root     4096 set 29 00:04 ./
drwxr-xr-x 19 root root     4096 set 24 15:33 ../
-rw-r--r--  1 root root  1006444 mag 23  2017 libcurl.a
-rwxr-xr-x  1 root root     1042 mag 23  2017 libcurl.la*
lrwxrwxrwx  1 root root       16 lug  4 16:26 libcurl.so.4 -> libcurl.so.4.4.0*
-rwxr-xr-x  1 root root   542272 mag 23  2017 libcurl.so.4.4.0*
-rw-r--r--  1 root root  4129514 gen 30  2018 libfilezilla.a
-rwxr-xr-x  1 root root      976 gen 30  2018 libfilezilla.la*
lrwxrwxrwx  1 root root       21 gen 30  2018 libfilezilla.so -> libfilezilla.so.0.0.0*
lrwxrwxrwx  1 root root       21 gen 30  2018 libfilezilla.so.0 -> libfilezilla.so.0.0.0*
-rwxr-xr-x  1 root root  1823960 gen 30  2018 libfilezilla.so.0.0.0*
-rwxr--r--  1 root root  3090092 mag 14  2016 libOpenBUGS.so*
-rwxr-xr-x  1 root root     1059 ott 27  2020 libopenconnect.la*
lrwxrwxrwx  1 root root       23 ott 27  2020 libopenconnect.so -> libopenconnect.so.5.6.0*
lrwxrwxrwx  1 root root       23 ott 27  2020 libopenconnect.so.5 -> libopenconnect.so.5.6.0*
-rwxr-xr-x  1 root root  1458464 ott 27  2020 libopenconnect.so.5.6.0*
-rw-r--r--  1 root root  1681976 mag 22  2017 libssh2.a
-rwxr-xr-x  1 root root      950 mag 22  2017 libssh2.la*
lrwxrwxrwx  1 root root       16 mag 22  2017 libssh2.so -> libssh2.so.1.0.1*
lrwxrwxrwx  1 root root       16 mag 22  2017 libssh2.so.1 -> libssh2.so.1.0.1*
-rwxr-xr-x  1 root root   893968 mag 22  2017 libssh2.so.1.0.1*
drwxr-xr-x  5 root root     4096 gen 10  2018 php/
-rwxr-xr-x  1 root root     1278 mar 28  2018 phpunit*
drwxr-xr-x  2 root root     4096 ott 27  2020 pkgconfig/
drwxrwsr-x  4 root staff    4096 set 28 23:55 python2.7/
drwxrwsr-x  3 root staff    4096 ott 21  2015 python3.4/
drwxrwsr-x  3 root staff    4096 ott 21  2015 python3.5/
drwxrwsr-x  3 root staff    4096 ago  6  2019 python3.6/
drwxr-xr-x  3 root root     4096 set 28 22:12 python3.9/

$ sudo unlink libcurl.so.4 

Il problema scompare

$ php -v
PHP 7.4.16 (cli) (built: Oct 26 2021 16:46:20) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
with Zend OPcache v7.4.16, Copyright (c), by Zend Technologies

Attenzione: tra qualche tempo, si ripresenterà il problema e so che dovrò reintrodurre il link simbolico.

Incredibile!

Ma non ho tempo di studiare il perché, so che facendo flip-flop temporaneamente si risolve il problema.

Mi spiace ma è così.

Risorse web su CURL

Considerazioni su database NoSQL

Una veloce incursione nella sfera dei databse NoSQL.

NoSQL
NoSQL

NoSQL è un movimento che promuove sistemi software dove la persistenza dei dati è in generale caratterizzata dal fatto di non utilizzare il modello relazionale, di solito usato dalle basi di dati tradizionali (RDBMS).

L’espressione fa infatti riferimento al linguaggio SQL, che è il più comune linguaggio di interrogazione dei dati nei database relazionali, qui preso a simbolo dell’intero paradigma relazionale.

Questi archivi di dati il più delle volte non richiedono uno schema fisso (schemeless), evitano spesso le operazioni di giunzione (join) e puntano a scalare in modo orizzontale. Gli accademici e gli articoli si riferiscono a queste basi di dati come memorizzazione strutturata (structured storage).

I creatori di MongoDB, il database non relazionale più popolare, individuano queste cause nell’affermarsi di database NoSQL:

  • Le richieste di maggiore produttività e tempi di commercializzazione più rapidi sono frenate da rigidi modelli di dati relazionali che non corrispondono al codice moderno e impongono complesse interdipendenze tra i team di progettazione.
  • Le organizzazioni non sono in grado di lavorare ed estrarre informazioni dai massicci aumenti dei nuovi dati strutturati, semistrutturati e polimorfici in rapida evoluzione generati dalle applicazioni odierne.
  • I vecchi (legacy) database monolitici e fragili inibiscono il passaggio estensivo a sistemi distribuiti e cloud computing che forniscono la resilienza e la scalabilità di cui le aziende hanno bisogno, rendendo più difficile soddisfare i nuovi requisiti normativi per la privacy dei dati.
  • I carichi di lavoro transazionali, analitici, di ricerca e mobili precedentemente separati stanno convergendo per creare applicazioni ricche e basate sui dati e sulle esperienze degli utilizzatori. Tuttavia, ogni carico di lavoro è stato tradizionalmente alimentato dal proprio database, creando silos di dati duplicati connessi con fragili pipeline ETL a cui si accede da diverse API di sviluppatori.

Processi ETL di esempio possono essere:

  • Migrazione dei dati da un’applicazione a un’altra
  • Replica dei dati per l’esecuzione di backup o analisi della ridondanza
  • Processi operativi quali il trasferimento di dati da un sistema CRM in un ODS (Operational Data Store) per l’ottimizzazione e l’arricchimento, per poi restituire i dati al sistema CRM
  • Inserimento dei dati in un Data Warehouse per l’assimilazione, l’ordinamento e la trasformazione in business intelligence
  • Migrazione delle applicazioni locali in infrastrutture Cloud, Cloud ibride o multi-Cloud
  • Sincronizzazione di sistemi chiave

Vengono formulate anche considerazioni critiche basate su alcune dimensioni che definiscono questi sistemi:

  • modello di dati;
  • modello di interrogazione;
  • consistenza e modello transazionale;
  • API;
  • dati mobili;
  • piattaforma dati e supporto commerciale,
  • forza della comunità e
  • libertà dal lock-in (abilità di non chiudersi dentro ad una soluzione che non cresce).

Modello di dati per database NoSQL

Il modo principale in cui i database non tabulari differiscono dai database relazionali è il modello di dati. Sebbene ci siano dozzine di database non tabulari, essi generalmente rientrano in tre categorie:

  • database di documenti,
  • database di grafi e
  • database chiave-valore o archivi a colonne larghe.

Modello documentale

Mentre i database relazionali archiviano i dati in righe e colonne, i database di documenti archiviano i dati in documenti utilizzando JavaScript Object Notation (JSON), un formato di scambio dati basato su testo popolare tra gli sviluppatori. I documenti forniscono un modo intuitivo e naturale per modellare i dati che è strettamente allineato con la programmazione orientata agli oggetti: ogni documento è effettivamente un oggetto che corrisponde agli oggetti con cui gli sviluppatori lavorano nel codice.

I documenti contengono uno o più campi e ogni campo contiene un valore digitato come una stringa, una data, un valore binario, decimale o una matrice. Anziché distribuire un record su più colonne e tabelle collegate a chiavi esterne, ogni record viene archiviato insieme ai dati associati (ovvero correlati) in un unico documento gerarchico.

Questo modello accelera la produttività degli sviluppatori, semplifica l’accesso ai dati e, in molti casi, elimina la necessità di costose operazioni di join e livelli di astrazione complessi come il mapping relazionale degli oggetti (ORM).

Lo schema di un database relazionale è definito da tabelle; in un database di documenti, la nozione di schema è dinamica: ogni documento può contenere campi diversi. Questa flessibilità può essere particolarmente utile per la modellazione dei dati in cui le strutture possono cambiare da un record all’altro, ad esempio dati polimorfici.

Inoltre, diventa più semplice l’evoluzione di un’applicazione durante il suo ciclo di vita, ad esempio se si devono aggiungere nuovi campi. Inoltre, alcuni database di documenti forniscono l’espressività delle query che gli sviluppatori si aspettano dai database relazionali.

In particolare, i dati possono essere interrogati in base a qualsiasi combinazione di campi in un documento, con ricchi indici secondari che forniscono percorsi di accesso efficienti per supportare quasi tutti i modelli di query. Alcuni database di documenti offrono anche la possibilità di applicare uno schema ai documenti.

Applicazioni

I database di documenti sono utili per un’ampia varietà di applicazioni grazie alla flessibilità del modello di dati, alla capacità di eseguire query su qualsiasi campo e alla mappatura naturale del modello di dati del documento agli oggetti nei moderni linguaggi di programmazione.

Esempi

MongoDB, Azure CosmosDB, Apache CouchDB

Nella sostanza, per visualizzare un database i tipo documentale, l’unico modo è stampare i file JSON contente gli archivi. Questo esempio è tratto da miaplatfrom.eu:

Modello documentale, documento 1 "AUTORI"
Modello documentale, documento 1 “AUTORI”
Modello documentale, documento 2 "LIBRI"
Modello documentale, documento 2 “LIBRI”

Modello a grafi

I database di grafi utilizzano strutture di grafi con nodi, bordi e proprietà per rappresentare i dati. In sostanza, i dati sono modellati come una rete di relazioni tra elementi specifici. Sebbene il modello a grafi possa essere controintuitivo, può essere utile per una classe specifica di query. Il suo principale vantaggio è che semplifica la modellazione e la navigazione delle relazioni tra le entità in un’applicazione.

Applicazioni

I database di grafi sono utili nei casi in cui l’attraversamento delle relazioni è fondamentale per l’applicazione, come la navigazione nelle connessioni di social network, nelle topologie di rete o nelle catene di approvvigionamento.

Esempi

Neo4j, Amazon Neptune

In sostanza i nodi sono tabelle normali dichiarate come node (AS NODE), le relazioni sono anch’esse tabelle (come le tabelle di join) dichiarate come edge (AS EDGE). Si chiamano anche tabelle di bordo, o tabelle perimetrali.

Il seguente esempio è tratto da Microsoft:

Database a grafo
Database NoSQL a grafo

Database chiave-valore e modelli a colonna larga

Dal punto di vista del modello dati, i database chiave-valore sono il tipo più elementare di database non tabulare. Ogni elemento nel database viene memorizzato come nome di attributo o chiave, insieme al suo valore. Il valore, tuttavia, è completamente opaco per il sistema: i dati possono essere interrogati solo dalla chiave. Questo modello può essere utile per rappresentare dati polimorfici e non strutturati, perché il database non impone uno schema impostato tra coppie chiave-valore.

Gli archivi a colonne larghe, o archivi di famiglie di colonne, utilizzano una mappa ordinata sparsa, distribuita e multidimensionale per archiviare i dati. Ogni record può variare nel numero di colonne memorizzate. Le colonne possono essere raggruppate in famiglie di colonne o distribuite su più famiglie. I dati vengono recuperati dalla chiave primaria per famiglia di colonne.

Applicazioni

I database a chiave-valore e gli archivi a colonne larghe sono utili per un set specializzato di applicazioni che eseguono query sui dati utilizzando un singolo valore di chiave. L’attrattiva di questi sistemi è la loro prestazione e scalabilità, che possono essere altamente ottimizzate grazie alla semplicità dei modelli di accesso ai dati e all’opacità dei dati stessi.

Esempi

Redis, Amazon DynamoDB (key-value); Apache HBase, Apache Cassandra (wide-column)

Per esempio il seguente database chiave-valore è tratto AmazonWS:

Key-Value database AmazonWS
Key-Value database NoSQL AmazonWS

Punti chiave

  • I modelli di dati chiave-valore e a colonna ampia sono opachi nel sistema: è possibile eseguire query solo sulla chiave primaria (ad esempio non si può cercare nel testo).
  • Il modello dati documentale ha la più ampia applicabilità.
  • Il modello a colonne larghe fornisce un accesso più granulare ai dati rispetto al modello chiave-valore, ma è meno flessibile rispetto al modello documento.
  • Il modello dati documentale è il più naturale e produttivo perché mappa direttamente gli oggetti nei linguaggi OO.

Bibliografia

Installazione GDAL

gdal logo
gdal logo

GDAL (Geospatial Data Abstraction Library) è una libreria di traduzione per formati di dati geospaziali raster e vettoriali rilasciata con una licenza Open Source in stile X/MIT dalla Open Source Geospatial Foundation. Come libreria, presenta un singolo modello di dati astratti raster e un singolo modello di dati astratti vettoriali all’applicazione chiamante per tutti i formati supportati. Inoltre viene fornito con una varietà di utili utilità della riga di comando per la traduzione e l’elaborazione dei dati.

PPA

A differenza di quanto riportato in diversi “how to”, in un forum ho trovato un suggerimento che mi segnalava che per la Hirsute Hippo di Ubuntu (21.04) non era necessario aggiugere la PPA di unbuntugis. L’avevo fatto in precedenza e incorrevo nell’errore

E: Il repository "http://ppa.launchpad.net/ubuntugis/ppa/ubuntu hirsute Release" non ha un file Release.

La spiegazione è che dalla versione 20 di Ubuntu, il repostiory di qgis è già incluso nel core. Quindi ho applicato i soli comandi di installazione:

$ sudo apt -y install gdal-bin python3-gdal
$ gdalinfo --version
GDAL 3.2.2, released 2021/03/05

Risorse internet su GDAL

Ubuntu – errore architettura in update

Ubuntu 21.04 Hirsute Hippo
Ubuntu 21.04 Hirsute Hippo

Installando QGIS ho questo errore:

N: Acquisizione del file "main/binary-i386/Packages" saltata in quanto il repository "https://qgis.org/ubuntu hirsute InRelease" non supporta l'architettura "i386"

Soluzione

Modificare così il file /etc/apt/sources.list.d/archive_uri-https_qgis_org_ubuntu-hirsute.list

deb [arch=amd64] https://qgis.org/ubuntu hirsute main

(aggiungere la parte in rosso).

L’update termina ora senza errori.

Ovviamente non è un errore specifico di QGIS, ma capita sempre se cerchiamo di scaricare pacchetti non corrispondenti all’architettura del processore.