Taskx mittels Laraknife: Unterschied zwischen den Versionen

Aus Vokabulabor
Zur Navigation springen Zur Suche springen
Zeile 12: Zeile 12:
* Benutzerverwaltung: nur angemeldete Benutzer können Applikation nutzen
* Benutzerverwaltung: nur angemeldete Benutzer können Applikation nutzen
* Rechteverwaltung mittels Rollen
* Rechteverwaltung mittels Rollen
* Verwaltung von Notizen: Titel, Text, Kategorie, Text, Status: offen, erledigt
* Verwaltung von Notizen: Titel, Text, Kategorie, Gruppen, Status: offen, erledigt


= Installation =
= Installation =

Version vom 31. Dezember 2023, 12:53 Uhr

Links

Zielsetzung

Es soll eine minimale Web-Applikation mit den Werkzeugen Laravel und Laraknife erstellt werden: Eine Verwaltung von Notizen.

Name

Eine Verballhornung des Wortes "Tasks": Die schwäbische Aussprache spricht ein x am Ende.

Eigenschaften

  • Benutzerverwaltung: nur angemeldete Benutzer können Applikation nutzen
  • Rechteverwaltung mittels Rollen
  • Verwaltung von Notizen: Titel, Text, Kategorie, Gruppen, Status: offen, erledigt

Installation

  • als normaler Benutzer (nicht root):
sudo apt install php-laravel-framework npm php-intl

PROJ=taskx
PASSW=topsecret
BASE=/home/ws/php/$PROJ
cd $(dirname $BASE)
composer create-project laravel/laravel $PROJ
cd $BASE
composer require laravel/ui
composer require spatie/laravel-permission
php artisan ui bootstrap --auth
dbtool create-db-and-user lrv$PROJ $PROJ "$PASSW"
sed -i -e "s/DB_DATABASE=.*/DB_DATABASE=lrv$PROJ/" \
  -e "s/DB_USERNAME=.*/DB_USERNAME=$PROJ/" \
  -e "s/DB_PASSWORD=.*/DB_PASSWORD=$PASSW/" .env
M_HOST=mail.gmx.net
M_PORT=587
M_USER=example@gmx.de
M_PW=Top.Secret42
sed -i \
  -e "s/APP_NAME=.*/APP_NAME=$PROJ"/ \
  -e "s/MAIL_MAILER=.*/MAIL_MAILER=smtp/" \
  -e "s/MAIL_HOST=.*/MAIL_HOST=$M_HOST/" \
  -e "s/MAIL_PORT=.*/MAIL_PORT=$M_PORT/" \
  -e "s/MAIL_USERNAME=.*/MAIL_USERNAME=$M_USER/" \
  -e "s/MAIL_PASSWORD=.*/MAIL_PASSWORD=$M_PW/" \
  -e "s/MAIL_ENCRYPTION=.*/MAIL_ENCRYPTION=STARTTLS/" \
  -e "s/MAIL_FROM_ADDRESS=.*/MAIL_FROM_ADDRESS=\"$M_USER\"/" .env
sudo sed -i -e "3 i 127.0.0.1 $PROJ.test" /etc/hosts
grep "$PROJ.test" /etc/hosts
php artisan migrate
npm install
npm run dev
# Abbruch mit Strg-C

Einrichten LaraKnife

composer config repositories.laraknife vcs https://github.com/hamatoma/laraknife
# Branch main:
composer require hamatoma/laraknife:dev-main

composer update
vendor/hamatoma/laraknife/scripts/laraknife-tool.sh build-links

Mehrsprachigkeit (I18N)

  • config/app.php
'locale' => 'en',
# ersetzen durch:
'locale' => 'de_DE',
'available_locales' => [
  'English' => 'en',
  'German' => 'de_DE',
],
  • Übersetzungen aus LaraKnife aktivieren:
# App-spezifische Übersetzung anlegen:
cat <<EOS >resources/lang/sources/$PROJ.de.json
{
"!comment": "Bitte alphabetisch sortiert eintragen",
"ZZZZZ_last": ""
}
EOS
./Join
  • Die neuen Übersetzungen müssen in resources/lang/sources/taskx.de.json eingetragen werden.

Roles und SProperties füllen

php artisan migrate
sudo mysql lrv$PROJ <<'EOS'
insert into roles (name, priority, created_at, updated_at) values 
('Administrator', 10, '2023.12.28', '2023-12-28'),
('Manager', 20, '2023.12.28', '2023-12-28'),
('User', 30, '2023.12.28', '2023-12-28'),
('Guest', 90, '2023.12.28', '2023-12-28');
insert into sproperties (id, scope, name, `order`, shortname, created_at) values
(1001, 'status', 'active', 10, 'A', '2023-12-28'),
(1002, 'status', 'inactive', 20, 'I', '2023-12-28'),
(2001, 'category', 'standard', 10, '-', '2023-12-28'),
(2002, 'category', 'private', 20, 'P', '2023-12-28'),
(2003, 'category', 'work', 30, 'W', '2023-12-28'),
(2501, 'group', 'standard', 10, '-', '2023-12-28'),
(2502, 'group', 'private', 20, 'P', '2023-12-28'),
(2503, 'group', 'work', 20, 'W', '2023-12-28'),
(2601, 'notestatus', 'open', 10, 'O', '2023-12-28'),
(2602, 'notestatus', 'closed', 20, 'C', '2023-12-28');
EOS

User anpassen

  • Datei app/Models/User.php ergänzen:
    protected $fillable = [
        'name',
        'email',
        'password',
        'role_id', // <====
    ];

Modul Notes erstellen

Tabellenbeschreibung erstellen

  • Konvention: Tabelle wird kleingeschrieben, im Plural
TABLE=notes
php artisan make:migration create_${TABLE}_table
  • Es wird die Datei database/migrations/2023_12_29_180821_create_notes_table.php erzeugt
  • Diese Datei anpassen:
        Schema::create('notes', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
            $table->string('title');
            $table->text('body');
            // foreign key of sproperties.
            $table->integer('category_scope');
            // foreign key of sproperties.
            $table->integer('status_scope');
            // foreign key of sproperties.
            $table->integer('group_scope');
            $table->foreignId('user_id')->references('id')->on('users')->nullable();
        });

Modul erzeugen

php artisan migrate
./Lara create:module database/migrations/*_create_notes_table.php

Layout erstellen

  • Datei resources/views/layouts/taskx.blade.php erstellen:
cat <<'EOS' >resources/views/layouts/taskx.blade.php
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>{{ config('app.name', 'Laravel') }}</title>
    <!-- Fonts -->
    <!-- link rel="dns-prefetch" href="//fonts.bunny.net">
    <link href="https://fonts.bunny.net/css?family=Nunito" rel="stylesheet" -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" rel="stylesheet">
    <!-- Scripts -->
    @vite(['resources/sass/app.scss', 'resources/js/app.js'])
    <link href="/css/laraknife.css" rel="stylesheet">
    <link href="/css/taskx.css" rel="stylesheet">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js"></script>
    <script src="/js/laraknife.js"></script>
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-md bg-primary ">
            <a class="navbar-brand" href="/home">Booking</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
              <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarTop">
                <ul class="navbar-nav mr-auto">
                    <li class="nav-item active">
                        <a class="nav-link" href="#">Start</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/user-edit">{{ __('Settings') }}</a>
                    </li>
                    <li>
                        <a class="nav-link" href="/public/doc/Impressum.pdf" target="_blank">{{ __('Imprint') }}</a>
                    </li>
                    <li>
                        <a class="nav-link" href="/public/doc/Datenschutz.pdf" target="_blank">{{ __('Privacy') }}</a>
                    </li>
                </ul>
                <form class="form-inline my-2 my-lg-0" action="/logout" method="post">
                    <button class="btn btn-outline-success my-2 my-sm-0 logout" name="btnLogout"
                        type="submit"> {{ __('Logout') }}</button>
                </form>
                </div>                
        </nav>
    </header>
        @yield('content')
</body>
</html>
EOS


cd resources/views/layouts
ln -sv taskx.blade.php backend.blade.php
cd ../../..

Homepage einrichten

  • Datei resources/views/home.blade.php
cat <<'EOS' >resources/views/home.blade.php
@extends('layouts.taskx')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Dashboard') }}</div>
                <div class="card-body">
                    @if (session('status'))
                        <div class="alert alert-success" role="alert">
                            {{ session('status') }}
                        </div>
                    @endif
                    {{ __('You are logged in!') }}
                    <ul>
                        <li><a href="/sproperty-index">SProperties</a></li>
                        <li><a href="/user-index">Benutzer</a></li>
                        <li><a href="/role-index">Rollen</a></li>
                        <li><a href="/note-index">Notizen</a></li>
                    </ul>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection
EOS

Routing einrichten

  • In Datei routes/web.php einfügen:
SPropertyController::routes();
RoleController::routes();
UserController::routes();
NoteController::routes();
  • Es sind 4 Namen unterstrichen: mit Strg-Alt-I Klassen einbinden.

Starten, Registrieren, Login

  • Den Webserver starten:
./Build
php artisan serve

Controller und Views anpassen

Übersicht (index)

  • Datei app/Http/Controllers/NoteController.php
    public function index()
    {
        if (array_key_exists('btnSubmit', $_POST) && $_POST['btnSubmit'] === 'btnNew') {
            return redirect('/note-create');
        } else {
            $sql = 'SELECT id, title, body, user_id, cast(body AS VARCHAR(60)) as body2 FROM notes';
            $userId = auth()->id();
            $parameters = [':user_id' => $userId];
            $conditions = ['user_id = :user_id'];
            if (count($_POST) == 0) {
                $fields = [
                    'text' => '',
                    '_sortParams' => 'id:desc'
                ];
            } else {
                $fields = $_POST;
                ViewHelper::addConditionPattern($conditions, $parameters, 'title,body', 'text');
            }
            $sql = DbHelper::addConditions($sql, $conditions);
            $sql = DbHelper::addOrderBy($sql, $fields['_sortParams']);
            $records = DB::select($sql, $parameters);
            $pagination = new Pagination($sql, $parameters, $fields);
            return view('note.index', [
                'records' => $records,
                'fields' => $fields,
                'pagination' => $pagination
            ]);
        }
    }
  • Datei resources/views/note/index.blade.php
<form id="note-index" action="/note-index" method="POST">
    @csrf
    <x-laraknife.index-panel title="{{ __('Notes') }}">
      <x-laraknife.filter-panel legend="{{ $pagination->legendText() }}">
        <x-laraknife.text position="alone" name="text" label="Text" value="{{$fields['text']}}" width2="4" />
      </x-laraknife.filter-panel>
      <x-laraknife.index-button-panel buttonType="new"/>
      <x-laraknife.sortable-table-panel :fields="$fields" :pagination="$pagination">
        <thead>
          <tr>
            <th></th>
            <th sortId="id">{{__('Id')}}</th>
            <th sortId="title">{{__('Title')}}</th>
            <th sortId="body">{{__('Body')}}</th>
            <th></th>
          </tr>
        </thead>
        <tbody>
      @foreach ($records as $note)
        <tr>
          <td><a href="/note-edit/{{$note->id}}">{{ __('Change')}}</a></td>
          <td>{{$note->id}}</td>
          <td>{{$note->title}}</td>
          <td>{{$note->body2}}</td>
          <td><a href="/note-delete/{{$note->id}}">{{ __('Delete')}}</a></td>
        </tr>
      @endforeach
      </tbody>
    </x-laraknife.sortable-table-panel>
  </x-laraknife.index-panel>

Anlegen (create)

  • Datei app/Http/Controllers/NoteController.php
  • Die drei Methoden ersetzen:
    public function create(Request $request)
    {
        if (array_key_exists('btnSubmit', $_POST) && $_POST['btnSubmit'] === 'btnCancel') {
            $rc = redirect('/note-index');
        } else {
            $rc = null;
            $error = null;
            if (count($_POST) > 0) {
                $fields = $_POST;
                try {
                    $incomingFields = $request->validate($this->rules(true));
                    $rc = $this->store($request);
                } catch (\Exception $e) {
                    $error = $e->getMessage();
                }
            } else {
                $fields = [
                    'title' => '',
                    'body' => '',
                    'category_scope' => '2001',
                    'status_scope' => '2601',
                    'group_scope' => '2501',
                 ];
            }
            if ($rc == null) {
                $optionsCategory = SProperty::optionsByScope('category', $fields['category_scope'], '-');
                $optionsStatus = SProperty::optionsByScope('notestatus', $fields['status_scope'], '-');
                $optionsGroup = SProperty::optionsByScope('group', $fields['group_scope'], '-');
                $rc = view('note.create', ['fields' => $fields,
                    'optionsCategory' => $optionsCategory,
                    'optionsStatus' => $optionsStatus,
                    'optionsGroup' => $optionsGroup,
                    'error' => $error]);
            }
        }
        return $rc;
    }
    private function rules(bool $isCreation=false): array
    {
        $rc = [
            'title' => 'required',
            'body' => 'required',
            'category_scope' => 'required',
            'status_scope' => 'required',
            'group_scope' => 'required',
        ];
        return $rc;
    }
    public function store(Request $request)
    {
        if ($request->btnSubmit === 'btnStore') {
            $incomingFields = $request->validate($this->rules());
            $incomingFields['user_id'] = auth()->id();
            $incomingFields['body'] = strip_tags($incomingFields['body']);
            Note::create($incomingFields);
        }
        return redirect('/note-index');
    }
  • Datei resources/views/note/create.blade.php
    <form id="note-create" action="/note-create" method="POST">
        @csrf
        <x-laraknife.create-panel title="{{ __('Creation of a Note') }}" error="{{$error}}">
            <x-laraknife.combobox position="first" name="status_scope" label="Status" value="{{$fields['status_scope']}}" :options="$optionsStatus" width2="4" />
            <x-laraknife.combobox position="last" name="group_scope" label="Group" value="{{$fields['group_scope']}}" :options="$optionsGroup" width2="4" />
            <x-laraknife.combobox position="alone" name="category_scope" label="Category" value="{{$fields['category_scope']}}" :options="$optionsCategory" width2="4" />
            <x-laraknife.text position="alone" name="title" label="Title" value="{{$fields['title']}}"  />
            <x-laraknife.bigtext position="alone" name="body" label="Body" value="{{$fields['body']}}" rows="10" />
        </x-laraknife.create-panel>
    </form>
  • Übersetzungen in resources/lang/sources/taskx.de.json
"Body": "Info",
"Category": "Kategorie",
"Creation of a Note": "Anlegen einer Notiz",
"Group": "Gruppe",
"open": "offen",
"standard": "Standard",
"Status": "Status",
"Title": "Titel",
  • Aktivieren:
./Join

Ändern (edit)

  • Datei app/Http/Controllers/NoteController.php
    public function edit(Note $note)
    {
        if (array_key_exists('btnSubmit', $_POST) && $_POST['btnSubmit'] === 'btnCancel') {
            $rc = redirect('/note-index');
        } else {
            $optionsCategory = SProperty::optionsByScope('category', $note->category_scope, '-');
            $optionsStatus = SProperty::optionsByScope('notestatus', $note->status_scope, '-');
            $optionsGroup = SProperty::optionsByScope('group', $note->group_scope, '-');
            $rc = view('note.edit', [
                'note' => $note,
                'optionsCategory' => $optionsCategory,
                'optionsStatus' => $optionsStatus,
                'optionsGroup' => $optionsGroup,
            ]);
        }
        return $rc;
    }
    public function update(Note $note, Request $request)
    {
        $rc = null;
        if ($request->btnSubmit === 'btnStore') {
            try {
                $incomingFields = $request->validate($this->rules());
                $incomingFields['body'] = strip_tags($incomingFields['body']);
                $note->update($incomingFields);
                $rc = redirect('/note-index');
            } catch (\Exception $exc) {
                $msg = $exc->getMessage();
                $rc = back();
            }
        }
        if ($rc == null) {
            $rc = redirect('/note-index');
        }
        return $rc;
    }
  • Datei resources/views/note/edit.blade.php
    <form id="note-edit" action="/note-update/{{ $note->id }}" method="POST">
        @csrf
        <x-laraknife.edit-panel title="{{ __('Change of a Note') }}">
            <x-laraknife.combobox position="first" name="status_scope" label="Status" value="{{$note->status_scope}}" :options="$optionsStatus" width2="4" />
            <x-laraknife.combobox position="last" name="group_scope" label="Group" value="{{$note->group_scope}}" :options="$optionsGroup" width2="4" />
            <x-laraknife.combobox position="alone" name="category_scope" label="Category" value="{{$note->category_scope}}" :options="$optionsCategory" width2="4" />
            <x-laraknife.text position="alone" name="title" label="Title" value="{{$note->title}}"  />
            <x-laraknife.bigtext position="alone" name="body" label="Body" value="{{$note->body}}" rows="10" />
        </x-laraknife.edit-panel>
    </form>
  • Übersetzungen in resources/lang/sources/taskx.de.json
"Change of a Note": "Ändern einer Notiz",
  • Aktivieren:
./Join

Anzeigen (show/delete)

  • Datei app/Http/Controllers/NoteController.php
    public function show(Note $note)
    {
        if (array_key_exists('btnSubmit', $_POST) && $_POST['btnSubmit'] === 'btnCancel') {
            $rc = redirect('/note-index');
        } else {
            $optionsCategory = SProperty::optionsByScope('category', 'category', '');
            $optionsGroup = SProperty::optionsByScope('group', 'group', '');
            $optionsStatus = SProperty::optionsByScope('notestatus', 'status', '');
            $rc = view('note.show', [
                'note' => $note,
                'optionsCategory' => $optionsCategory,
                'optionsGroup' => $optionsGroup,
                'optionsStatus' => $optionsStatus,
                'mode' => 'delete'
            ]);
        }
        return $rc;
    }
  • Datei resources/views/note/show.blade.php
    <form id="note-show" action="/note-show/{{ $note->id }}/{{ $mode }}" method="POST">
        @csrf
        @if ($mode === 'delete')
            @method('DELETE')
        @endif
        <x-laraknife.show-panel title="{{ __($mode !== 'delete' ? 'A Note' : 'Deletion of a Note') }}"
            mode="{{ $mode }}">
            <x-laraknife.combobox position="first" name="status_scope" label="Status" value="{{ $note->status_scope }}"
                :options="$optionsStatus" width2="4" attribute="readonly" />
            <x-laraknife.combobox position="last" name="group_scope" label="Group" value="{{ $note->group_scope }}"
                :options="$optionsGroup" width2="4" attribute="readonly" />
            <x-laraknife.combobox position="first" name="category_scope" label="Category" value="{{ $note->category_scope }}"
                :options="$optionsCategory" width2="4" attribute="readonly" />
            <x-laraknife.text position="last" name="id" label="Id" value="{{ $note->id }}" width2="4"
                attribute="readonly" />
            <x-laraknife.text position="alone" name="title" label="Title" value="{{ $note->title }}"
                attribute="readonly" />
            <x-laraknife.bigtext position="alone" name="body" label="Body" value="{{ $note->body }}" rows="10"
                attribute="readonly" />
        </x-laraknife.show-panel>
    </form>
  • Übersetzungen in resources/lang/sources/taskx.de.json
"A Note" : "Eine Notiz",
"Deletion of a Note": "Löschen einer Notiz",
  • Aktivieren:
./Join