This commit is contained in:
2022-01-13 18:41:03 +01:00
commit 0fb9f639da
159 changed files with 13183 additions and 0 deletions

5
.docker/nginx/Dockerfile Normal file
View File

@@ -0,0 +1,5 @@
FROM nginx:alpine
#COPY ./ /var/www/html/
CMD ["nginx"]
EXPOSE 80 443

View File

@@ -0,0 +1,3 @@
upstream php-upstream {
server php:9000;
}

24
.docker/nginx/nginx.conf Normal file
View File

@@ -0,0 +1,24 @@
user nginx;
worker_processes 4;
daemon off;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /dev/stdout;
error_log /dev/stderr;
sendfile on;
keepalive_timeout 65;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-available/*.conf;
}

View File

@@ -0,0 +1,28 @@
server {
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;
server_name localhost;
root /var/www/html/www/;
index index.php;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
try_files $uri /index.php =404;
fastcgi_pass php-upstream;
fastcgi_index index.php;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_read_timeout 600;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}

9
.docker/php/Dockerfile Normal file
View File

@@ -0,0 +1,9 @@
FROM thecodingmachine/php:8.0-v4-fpm
#COPY ./ /var/www/html/
#WORKDIR /var/www/html/
#CMD ["php-fpm"]
EXPOSE 9000

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

16
.editorconfig Normal file
View File

@@ -0,0 +1,16 @@
# EditorConfig is awesome: http://EditorConfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = tab
indent_size = tab
tab_width = 4
[{*.json, *.yaml, *.yml, *.md}]
indent_style = space
indent_size = 2

10
.github/.kodiak.toml vendored Normal file
View File

@@ -0,0 +1,10 @@
version = 1
[merge]
automerge_label = "automerge"
blacklist_title_regex = "^WIP.*"
blacklist_labels = ["WIP"]
method = "rebase"
delete_branch_on_merge = true
notify_on_conflict = true
optimistic_updates = false

9
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
version: 2
updates:
- package-ecosystem: composer
directory: "/"
schedule:
interval: daily
labels:
- "dependencies"
- "automerge"

192
.github/workflows/main.yaml vendored Normal file
View File

@@ -0,0 +1,192 @@
name: "build"
on:
pull_request:
paths-ignore:
- ".docs/**"
push:
branches:
- "*"
schedule:
- cron: "0 8 * * 1" # At 08:00 on Monday
env:
extensions: "json"
cacheVersion: "1"
composerVersion: "v2"
composerInstall: "composer install"
jobs:
qa:
name: "Quality Assurance"
runs-on: "${{ matrix.operating-system }}"
strategy:
matrix:
php-versions: [ "8.0" ]
operating-system: [ "ubuntu-latest" ]
fail-fast: false
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
- name: "Setup PHP cache environment"
id: "extcache"
uses: "shivammathur/cache-extensions@v1"
with:
php-version: "${{ matrix.php-versions }}"
extensions: "${{ env.extensions }}"
key: "${{ env.cacheVersion }}"
- name: "Cache PHP extensions"
uses: "actions/cache@v2"
with:
path: "${{ steps.extcache.outputs.dir }}"
key: "${{ steps.extcache.outputs.key }}"
restore-keys: "${{ steps.extcache.outputs.key }}"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
php-version: "${{ matrix.php-versions }}"
extensions: "${{ env.extensions }}"
tools: "composer:${{ env.composerVersion }} "
- name: "Setup problem matchers for PHP"
run: 'echo "::add-matcher::${{ runner.tool_cache }}/php.json"'
- name: "Get Composer cache directory"
id: "composercache"
run: 'echo "::set-output name=dir::$(composer config cache-files-dir)"'
- name: "Cache PHP dependencies"
uses: "actions/cache@v2"
with:
path: "${{ steps.composercache.outputs.dir }}"
key: "${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}"
restore-keys: "${{ runner.os }}-composer-"
- name: "Validate Composer"
run: "composer validate"
- name: "Install dependencies"
run: "${{ env.composerInstall }}"
- name: "Coding Standard"
run: "make cs"
static-analysis:
name: "Static analysis"
runs-on: "${{ matrix.operating-system }}"
strategy:
matrix:
php-versions: [ "8.0" ]
operating-system: [ "ubuntu-latest" ]
fail-fast: false
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
- name: "Setup PHP cache environment"
id: "extcache"
uses: "shivammathur/cache-extensions@v1"
with:
php-version: "${{ matrix.php-versions }}"
extensions: "${{ env.extensions }}"
key: "${{ env.cacheVersion }}"
- name: "Cache PHP extensions"
uses: "actions/cache@v2"
with:
path: "${{ steps.extcache.outputs.dir }}"
key: "${{ steps.extcache.outputs.key }}"
restore-keys: "${{ steps.extcache.outputs.key }}"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
php-version: "${{ matrix.php-versions }}"
extensions: "${{ env.extensions }}"
tools: "composer:${{ env.composerVersion }} "
- name: "Setup problem matchers for PHP"
run: 'echo "::add-matcher::${{ runner.tool_cache }}/php.json"'
- name: "Get Composer cache directory"
id: "composercache"
run: 'echo "::set-output name=dir::$(composer config cache-files-dir)"'
- name: "Cache PHP dependencies"
uses: "actions/cache@v2"
with:
path: "${{ steps.composercache.outputs.dir }}"
key: "${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}"
restore-keys: "${{ runner.os }}-composer-"
- name: "Install dependencies"
run: "${{ env.composerInstall }}"
- name: "PHPStan"
run: "make phpstan"
tests:
name: "Tests"
runs-on: "${{ matrix.operating-system }}"
strategy:
matrix:
php-versions: [ "8.0" ]
operating-system: [ "ubuntu-latest" ]
fail-fast: false
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
- name: "Setup PHP cache environment"
id: "extcache"
uses: "shivammathur/cache-extensions@v1"
with:
php-version: "${{ matrix.php-versions }}"
extensions: "${{ env.extensions }}"
key: "${{ env.cacheVersion }}"
- name: "Cache PHP extensions"
uses: "actions/cache@v2"
with:
path: "${{ steps.extcache.outputs.dir }}"
key: "${{ steps.extcache.outputs.key }}"
restore-keys: "${{ steps.extcache.outputs.key }}"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
php-version: "${{ matrix.php-versions }}"
extensions: "${{ env.extensions }}"
tools: "composer:${{ env.composerVersion }} "
- name: "Setup problem matchers for PHP"
run: 'echo "::add-matcher::${{ runner.tool_cache }}/php.json"'
- name: "Get Composer cache directory"
id: "composercache"
run: 'echo "::set-output name=dir::$(composer config cache-files-dir)"'
- name: "Cache PHP dependencies"
uses: "actions/cache@v2"
with:
path: "${{ steps.composercache.outputs.dir }}"
key: "${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}"
restore-keys: "${{ runner.os }}-composer-"
- name: "Install dependencies"
run: "${{ env.composerInstall }}"
- name: "Setup problem matchers for PHPUnit"
run: 'echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"'
- name: "Tests"
run: "make tests"

16
.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
# Nette
/config/local.neon
# Composer
/vendor
# NodeJS
/node_modules
# Assets
/www/dist
# Tests
/tests/*.log
/tests/tmp
/tests/coverage.html

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Contributte
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

73
Makefile Normal file
View File

@@ -0,0 +1,73 @@
############################################################
# PROJECT ##################################################
############################################################
.PHONY: project install setup clean
project: install setup
install:
composer install
setup:
mkdir -p var/tmp var/log
chmod +0777 var/tmp var/log
clean:
find var/tmp -mindepth 1 ! -name '.gitignore' -type f,d -exec rm -rf {} +
find var/log -mindepth 1 ! -name '.gitignore' -type f,d -exec rm -rf {} +
############################################################
# DEVELOPMENT ##############################################
############################################################
.PHONY: qa dev cs csf phpstan tests coverage dev build
qa: cs phpstan
cs:
vendor/bin/codesniffer app tests
csf:
vendor/bin/codefixer app tests
phpstan:
vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=512M app tests/toolkit
tests:
vendor/bin/tester -s -p php --colors 1 -C tests
coverage:
vendor/bin/tester -s -p phpdbg --colors 1 -C --coverage ./coverage.xml --coverage-src ./app tests
dev:
NETTE_DEBUG=1 NETTE_ENV=dev php -S 0.0.0.0:8000 -t www
build:
NETTE_DEBUG=1 bin/console orm:schema-tool:drop --force --full-database
NETTE_DEBUG=1 bin/console migrations:migrate --no-interaction
NETTE_DEBUG=1 bin/console doctrine:fixtures:load --no-interaction --append
############################################################
# DEPLOYMENT ###############################################
############################################################
.PHONY: deploy
deploy: clean project build clean
############################################################
# DOCKER ###################################################
############################################################
.PHONY: docker-postgres docker-postgres-stop docker-adminer docker-adminer-stop
docker-postgres: docker-postgres-stop
docker run -it -d -p 5432:5432 --name webapp_postgres -e POSTGRES_PASSWORD=webapp -e POSTGRES_USER=webapp dockette/postgres:12
docker-postgres-stop:
docker stop webapp_postgres || true
docker rm webapp_postgres || true
docker-adminer: docker-adminer-stop
docker run -it -d -p 9999:80 --name webapp_adminer dockette/adminer:dg
docker-adminer-stop:
docker stop webapp_adminer || true
docker rm webapp_adminer || true

236
README.md Normal file
View File

@@ -0,0 +1,236 @@
![](https://heatbadger.now.sh/github/readme/contributte/webapp-skeleton/)
<p align=center>
<a href="https://github.com/contributte/webapp-skeleton/actions"><img src="https://badgen.net/github/checks/contributte/webapp-skeleton/master"></a>
<a href="https://coveralls.io/r/contributte/webapp-skeleton"><img src="https://badgen.net/coveralls/c/github/contributte/webapp-skeleton"></a>
<a href="https://packagist.org/packages/contributte/webapp-skeleton"><img src="https://badgen.net/packagist/dm/contributte/webapp-skeleton"></a>
<a href="https://packagist.org/packages/contributte/webapp-skeleton"><img src="https://badgen.net/packagist/v/contributte/webapp-skeleton"></a>
</p>
<p align=center>
<a href="https://packagist.org/packages/contributte/webapp-skeleton"><img src="https://badgen.net/packagist/php/contributte/webapp-skeleton"></a>
<a href="https://github.com/contributte/webapp-skeleton"><img src="https://badgen.net/github/license/contributte/webapp-skeleton"></a>
<a href="https://bit.ly/ctteg"><img src="https://badgen.net/badge/support/gitter/cyan"></a>
<a href="https://bit.ly/cttfo"><img src="https://badgen.net/badge/support/forum/yellow"></a>
<a href="https://contributte.org/partners.html"><img src="https://badgen.net/badge/sponsor/donations/F96854"></a>
</p>
<p align=center>
Website 🚀 <a href="https://contributte.org">contributte.org</a> | Contact 👨🏻‍💻 <a href="https://f3l1x.io">f3l1x.io</a> | Twitter 🐦 <a href="https://twitter.com/contributte">@contributte</a>
</p>
<p align=center>
<img src="https://api.microlink.io?url=https%3A%2F%2Fexamples.contributte.org%2Fwebapp-skeleton%2F&overlay.browser=light&screenshot=true&meta=false&embed=screenshot.url"></img>
</p>
-----
## Goal
Main goal is to provide best prepared starter-kit project for Nette developers.
Focused on:
- latest PHP 8.0
- `nette/*` packages
- Doctrine ORM via `nettrine/*`
- Symfony components via `contributte/*`
- codestyle checking via **CodeSniffer** and `ninjify/*`
- static analysing via **phpstan**
- unit / integration tests via **Nette Tester** and `ninjify/*`
## Demo
https://examples.contributte.org/webapp-skeleton/
## Installation
To install latest version of `contributte/webapp-skeleton` use [Composer](https://getcomposer.com).
```
composer create-project -s dev contributte/webapp-skeleton acme
```
### Install using [docker](https://github.com/docker/docker/)
1) At first, use composer to install this project.
```
composer create-project -s dev contributte/webapp-skeleton
```
2) After that, you have to setup Postgres >= 10 database. You can start it manually or use docker image `dockette/postgres:12`.
```
docker run -it -p 5432:5432 -e POSTGRES_PASSWORD=webapp -e POSTGRES_USER=webapp dockette/postgres:12
```
Or use make task, `make docker-postgres`.
3) Custom configuration file is located at `config/local.neon`. Edit it if you want.
Default configuration should look like:
```neon
# Host Config
parameters:
# Database
database:
host: localhost
dbname: webapp
user: webapp
password: webapp
```
4) Ok database is now running and application is configured to connect to it. Let's create initial data.
Run `NETTE_DEBUG=1 bin/console migrations:migrate` to create tables. Run `NETTE_DEBUG=1 bin/console doctrine:fixtures:load --append` to create first user(s).
Or via task `make build`.
5) Start your devstack or use PHP local development server.
You can start PHP server by running `php -S localhost:8000 -t www` or use prepared make task `make dev`.
6) Open http://localhost and enjoy!
Take a look at:
- http://localhost:8000.
- http://localhost:8000/admin (admin@admin.cz / admin)
### Install using [docker-compose](https://https://github.com/docker/compose/)
1) At first, use composer to install this project.
```
composer create-project -s dev contributte/webapp-project
```
2) Modify `config/local.neon` and set host to `database`
Default configuration should look like this:
```neon
# Host Config
parameters:
# Database
database:
host: database
dbname: webapp
user: webapp
password: webapp
```
3) Run `docker-compose up`
4) Open http://localhost and enjoy!
Take a look at:
- http://localhost.
- http://localhost/admin (admin@admin.cz / admin)
## Features
Here is a list of all features you can find in this project.
- PHP 8.0+
- :package: Packages
- Nette 3+
- Contributte
- Nettrine
- :deciduous_tree: Structure
- `app`
- `config` - configuration files
- `env` - prod/dev/test environments
- `app` - application configs
- `ext` - extensions configs
- `local.neon` - local runtime config
- `local.neon.dist` - template for local config
- `domain` - business logic and domain specific classes
- `model` - application backbone
- `modules` - Front/Admin module, presenters and components
- `resources` - static content for mails and others
- `ui` - UI components and base classes
- `bootstrap.php` - Nette entrypoint
- `bin` - console entrypoint (`bin/console`)
- `db` - database files
- `fixtures` - PHP fixtures
- `migrations` - migrations files
- `docs` - documentation
- `var`
- `log` - runtime and error logs
- `tmp` - tmp files and cache
- `tests` - test engine and unit/integration tests
- `vendor` - composer's folder
- `www` - public content
- :exclamation: Tracy
- Cool error 500 page
### Notable changes
- `$user` variable in templates [is renamed](https://github.com/contributte/webapp-skeleton/blob/master/app/model/Latte/TemplateFactory.php) to `$_user`
### Composer packages
Take a detailed look :eyes: at each single package.
- [contributte/bootstrap](https://contributte.org/packages/contributte/bootstrap.html)
- [contributte/application](https://contributte.org/packages/contributte/application.html)
- [contributte/di](https://contributte.org/packages/contributte/di.html)
- [contributte/cache](https://contributte.org/packages/contributte/cache.html)
- [contributte/http](https://contributte.org/packages/contributte/http.html)
- [contributte/forms](https://contributte.org/packages/contributte/forms.html)
- [contributte/latte](https://contributte.org/packages/contributte/latte.html)
- [contributte/mail](https://contributte.org/packages/contributte/mail.html)
- [contributte/security](https://contributte.org/packages/contributte/security.html)
- [contributte/utils](https://contributte.org/packages/contributte/utils.html)
- [contributte/tracy](https://contributte.org/packages/contributte/tracy.html)
- [contributte/console](https://contributte.org/packages/contributte/console.html)
- [contributte/webapp-skeleton](https://contributte.org/packages/contributte/webapp-skeleton.html)
- [contributte/event-dispatcher](https://contributte.org/packages/contributte/event-dispatcher.html)
- [contributte/event-dispatcher-extra](https://contributte.org/packages/contributte/event-dispatcher-extra.html)
- [contributte/neonizer](https://contributte.org/packages/contributte/neonizer.html)
- [contributte/mailing](https://contributte.org/packages/contributte/mailing.html)
- [contributte/monolog](https://contributte.org/packages/contributte/monolog.html)
**Nettrine**
- [nettrine/orm](https://contributte.org/packages/nettrine/orm.html)
- [nettrine/dbal](https://contributte.org/packages/nettrine/dbal.html)
- [nettrine/annotations](https://contributte.org/packages/nettrine/annotations.html)
- [nettrine/cache](https://contributte.org/packages/nettrine/cache.html)
- [nettrine/migrations](https://contributte.org/packages/nettrine/migrations.html)
- [nettrine/fixtures](https://contributte.org/packages/nettrine/fixtures.html)
- [nettrine/extensions](https://contributte.org/packages/nettrine/extensions.html)
**Dev**
- [contributte/dev](https://contributte.org/packages/contributte/dev.html)
- [ninjify/qa](https://contributte.org/packages/ninjify/qa.html)
- [ninjify/nunjuck](https://contributte.org/packages/ninjify/nunjuck.html)
- [phpstan/phpstan](https://github.com/phpstan/phpstan)
- [mockery/mockery](https://github.com/mockery/mockery)
- [nelmio/alice](https://github.com/nelmio/alice)
## Screenshots
![](.docs/assets/screenshot1.png)
> admin@admin.cz / admin
![](.docs/assets/screenshot2.png)
![](.docs/assets/screenshot3.png)
![](.docs/assets/screenshot4.png)
## Development
See [how to contribute](https://contributte.org/contributing.html) to this package.
This package is currently maintaining by these authors.
<a href="https://github.com/f3l1x">
<img width="80" height="80" src="https://avatars2.githubusercontent.com/u/538058?v=3&s=80">
</a>
-----
Consider to [support](https://contributte.org/partners.html) **contributte** development team. Also thank you for using this project.

2
app/.htaccess Normal file
View File

@@ -0,0 +1,2 @@
Order Allow,Deny
Deny from all

40
app/bootstrap.php Normal file
View File

@@ -0,0 +1,40 @@
<?php declare(strict_types = 1);
use Contributte\Bootstrap\ExtraConfigurator;
use Nette\DI\Compiler;
use Tracy\Debugger;
require __DIR__ . '/../vendor/autoload.php';
$configurator = new ExtraConfigurator();
$configurator->setTempDirectory(__DIR__ . '/../var/tmp');
$configurator->onCompile[] = function (ExtraConfigurator $configurator, Compiler $compiler): void {
// Add env variables to config structure
$compiler->addConfig(['parameters' => $configurator->getEnvironmentParameters()]);
};
// According to NETTE_DEBUG env
$configurator->setEnvDebugMode();
// Enable tracy and configure it
$configurator->enableTracy(__DIR__ . '/../var/log');
Debugger::$errorTemplate = __DIR__ . '/resources/tracy/500.phtml';
// Provide some parameters
$configurator->addParameters([
'rootDir' => realpath(__DIR__ . '/..'),
'appDir' => __DIR__,
'wwwDir' => realpath(__DIR__ . '/../www'),
]);
// Load development or production config
if (getenv('NETTE_ENV', true) === 'dev') {
$configurator->addConfig(__DIR__ . '/../config/env/dev.neon');
} else {
$configurator->addConfig(__DIR__ . '/../config/env/prod.neon');
}
$configurator->addConfig(__DIR__ . '/../config/local.neon');
return $configurator->createContainer();

View File

@@ -0,0 +1,25 @@
<?php declare(strict_types = 1);
namespace App\Domain\Http;
use Contributte\Events\Extra\Event\Application\RequestEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Tracy\Debugger;
class RequestLoggerSubscriber implements EventSubscriberInterface
{
/**
* @return mixed[]
*/
public static function getSubscribedEvents(): array
{
return [RequestEvent::class => 'onRequest'];
}
public function onRequest(RequestEvent $event): void
{
Debugger::barDump($event->getRequest());
}
}

View File

@@ -0,0 +1,25 @@
<?php declare(strict_types = 1);
namespace App\Domain\Order\Event;
use Symfony\Contracts\EventDispatcher\Event;
final class OrderCreated extends Event
{
public const NAME = 'order.created';
/** @var string */
private $order;
public function __construct(string $order)
{
$this->order = $order;
}
public function getOrder(): string
{
return $this->order;
}
}

View File

@@ -0,0 +1,42 @@
<?php declare(strict_types = 1);
namespace App\Domain\Order;
use App\Domain\Order\Event\OrderCreated;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Tracy\Debugger;
class OrderLogSubscriber implements EventSubscriberInterface
{
/**
* @return mixed[]
*/
public static function getSubscribedEvents(): array
{
return [
OrderCreated::NAME => [
['onOrderCreatedBefore', 100],
['onOrderCreated', 0],
['onOrderCreatedAfter', -100],
],
];
}
public function onOrderCreatedBefore(OrderCreated $event): void
{
Debugger::barDump('BEFORE');
}
public function onOrderCreated(OrderCreated $event): void
{
Debugger::log($event, 'info');
Debugger::barDump($event);
}
public function onOrderCreatedAfter(OrderCreated $event): void
{
Debugger::barDump('AFTER');
}
}

View File

@@ -0,0 +1,48 @@
<?php declare(strict_types = 1);
namespace App\Domain\User;
use App\Model\Database\Entity\User;
use App\Model\Database\EntityManager;
use App\Model\Security\Passwords;
class CreateUserFacade
{
/** @var EntityManager */
private $em;
public function __construct(
EntityManager $em
)
{
$this->em = $em;
}
/**
* @param mixed[] $data
*/
public function createUser(array $data): User
{
// Create User
$user = new User(
$data['name'],
$data['surname'],
$data['email'],
$data['username'],
Passwords::create()->hash($data['password'] ?? md5(microtime()))
);
// Set role
if (isset($data['role'])) {
$user->setRole($data['role']);
}
// Save user
$this->em->persist($user);
$this->em->flush();
return $user;
}
}

14
app/model/App.php Normal file
View File

@@ -0,0 +1,14 @@
<?php declare(strict_types = 1);
namespace App\Model;
final class App
{
public const DESTINATION_FRONT_HOMEPAGE = ':Front:Home:';
public const DESTINATION_ADMIN_HOMEPAGE = ':Admin:Home:';
public const DESTINATION_SIGN_IN = ':Admin:Sign:in';
public const DESTINATION_AFTER_SIGN_IN = self::DESTINATION_ADMIN_HOMEPAGE;
public const DESTINATION_AFTER_SIGN_OUT = self::DESTINATION_FRONT_HOMEPAGE;
}

View File

@@ -0,0 +1,25 @@
<?php declare(strict_types = 1);
namespace App\Model\Console;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class HelloCommand extends Command
{
protected function configure(): void
{
$this->setName('hello');
$this->setDescription('Hello world!');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->write('Hello world!');
return 0;
}
}

View File

@@ -0,0 +1,8 @@
<?php declare(strict_types = 1);
namespace App\Model\Database\Entity;
abstract class AbstractEntity
{
}

View File

@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);
namespace App\Model\Database\Entity\Attributes;
use App\Model\Utils\DateTime;
use Doctrine\ORM\Mapping as ORM;
trait TCreatedAt
{
/**
* @var DateTime
* @ORM\Column(type="datetime", nullable=FALSE)
*/
protected $createdAt;
public function getCreatedAt(): DateTime
{
return $this->createdAt;
}
/**
* Doctrine annotation
*
* @ORM\PrePersist
* @internal
*/
public function setCreatedAt(): void
{
$this->createdAt = new DateTime();
}
}

View File

@@ -0,0 +1,27 @@
<?php declare(strict_types = 1);
namespace App\Model\Database\Entity\Attributes;
trait TId
{
/**
* @var int
* @ORM\Column(type="integer", nullable=FALSE)
* @ORM\Id
* @ORM\GeneratedValue
*/
private $id;
public function getId(): int
{
return $this->id;
}
public function __clone()
{
$this->id = null;
}
}

View File

@@ -0,0 +1,33 @@
<?php declare(strict_types = 1);
namespace App\Model\Database\Entity\Attributes;
use App\Model\Utils\DateTime;
use Doctrine\ORM\Mapping as ORM;
trait TUpdatedAt
{
/**
* @var DateTime|NULL
* @ORM\Column(type="datetime", nullable=TRUE)
*/
protected $updatedAt;
public function getUpdatedAt(): ?DateTime
{
return $this->updatedAt;
}
/**
* Doctrine annotation
*
* @ORM\PreUpdate
* @internal
*/
public function setUpdatedAt(): void
{
$this->updatedAt = new DateTime();
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Model\Database\Entity;
use App\Model\Database\Entity\Attributes\TCreatedAt;
use App\Model\Database\Entity\Attributes\TId;
use App\Model\Database\Entity\Attributes\TUpdatedAt;
use App\Model\Exception\Logic\InvalidArgumentException;
use App\Model\Security\Identity;
use App\Model\Utils\DateTime;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class DictType extends AbstractEntity
{
use TId;
public function __construct($short_name,$full_name)
{
$this->shortName = $short_name;
$this->fullName = $full_name;
}
/**
* @ORM\Column(type="string")
*/
protected $shortName;
public function getShortName()
{
return $this->shortName;
}
/**
* @ORM\Column(type="string")
*/
protected $fullName;
public function getFullName()
{
return $this->fullName;
}
}
?>

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Model\Database\Entity;
use Doctrine\ORM\Mapping as ORM;
use App\Model\Database\Entity\Attributes\TId;
/**
* @ORM\Entity
*/
class Dictionary extends AbstractEntity
{
use Tid;
public function __construct($name,$fullname)
{
$this->name = $name;
$this->fullName = $fullname;
$this->translations = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* @ORM\OneToMany(targetEntity="App\Model\Database\Entity\Translation", mappedBy="dictionary", cascade={"persist"})
* @var Article[]|\Doctrine\Common\Collections\ArrayCollection
*/
protected $translations;
public function getTranslations()
{
return $this->translations;
}
/**
* @ORM\ManyToOne(targetEntity="Language", inversedBy="lang1_dicionaries")
*/
protected $lang1;
/**
* @ORM\ManyToOne(targetEntity="Language", inversedBy="lang2_dicionaries")
*/
protected $lang2;
/**
* @ORM\Column(type="string")
*/
protected $name;
public function getName()
{
return $this->name;
}
/**
* @ORM\Column(type="string")
*/
protected $fullName;
public function setLang1($lang1)
{
$this->lang1 = $lang1;
return $this;
}
public function setLang2($lang2)
{
$this->lang2 = $lang2;
return $this;
}
}
?>

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Model\Database\Entity;
use Doctrine\ORM\Mapping as ORM;
use App\Model\Database\Entity\Attributes\TId;
/**
* @ORM\Entity
*/
class Language extends AbstractEntity
{
use Tid;
public function __construct($shortname,$name)
{
$this->shortName = $shortname;
$this->name = $name;
}
/**
* @ORM\Column(type="string",length=2, unique=true, options={"fixed" = true})
*/
protected $shortName;
public function getShortName()
{
return $this->shortName;
}
/**
* @ORM\Column(type="string",length=32)
*/
protected $name;
public function getName()
{
return $this->name;
}
/*
* @ORM\OneToMany(targetEntity="Dictionary", mappedBy="language")
*/
protected $dictionaries1;
/*
* @ORM\OneToMany(targetEntity="Dictionary", mappedBy="language")
*/
protected $dictionaries2;
}
?>

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Model\Database\Entity;
use Doctrine\ORM\Mapping as ORM;
use App\Model\Database\Entity\Attributes\TId;
/**
* @ORM\Entity
*/
class Pronunciation extends AbstractEntity
{
use Tid;
public function __construct($type,$ipa,$filename=null)
{
$this->type = $type;
$this->ipa = $ipa;
$this->filename = $filename;
$this->terms = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* @ORM\ManyToMany(targetEntity="Term", mappedBy="pronunciations")
*/
private $terms;
/**
* @ORM\ManyToOne(targetEntity="PronunciationType", inversedBy="pronunciations")
*/
protected $type;
/**
* @ORM\Column(type="string",nullable=true)
*/
protected $ipa;
/**
* @ORM\Column(type="string", nullable=true)
*/
protected $filename;
}
?>

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Model\Database\Entity;
use Doctrine\ORM\Mapping as ORM;
use App\Model\Database\Entity\Attributes\Tid;
/**
* @ORM\Entity
*/
class PronunciationType extends AbstractEntity
{
use Tid;
public function __construct($name,$fullName)
{
$this->name = $name;
$this->fullName = $fullName;
}
/**
* @ORM\Column(type="string")
*/
protected $name;
/**
* @ORM\Column(type="string")
*/
protected $fullName;
}
?>

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Model\Database\Entity;
use Doctrine\ORM\Mapping as ORM;
use App\Model\Database\Entity\Attributes\TId;
/**
* @ORM\Entity
*/
class Suffix extends AbstractEntity
{
use Tid;
public function __construct($text)
{
$this->text = $text;
}
/**
* @ORM\Column(type="string")
*/
protected $text;
public function getText()
{
return $this->text;
}
}
?>

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Model\Database\Entity;
use Doctrine\ORM\Mapping as ORM;
use App\Model\Database\Entity\Attributes\TId;
/**
* @ORM\Entity
* @ORM\Table(indexes={@ORM\Index(columns={"string1"}, flags={"fulltext"}),
@ORM\Index(columns={"string2"}, flags={"fulltext"})})
*/
class Term extends AbstractEntity
{
use Tid;
public function __construct(Dictionary $dictionary,$string)
{
$this->dictionary = $dictionary;
$this->string1 = $string;
}
/**
* @ORM\ManyToOne(targetEntity="Dictionary", inversedBy="fullDict",cascade={"persist", "remove" })
*/
protected $dictionary;
/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}
}
?>

View File

@@ -0,0 +1,90 @@
<?php
namespace App\Model\Database\Entity;
use Doctrine\ORM\Mapping as ORM;
use App\Model\Database\Entity\Attributes\TId;
/**
* @ORM\Entity
*/
class TermFlag extends AbstractEntity
{
use Tid;
public function __construct($id,$class,$language,$code,$description)
{
$this->id = $id;
$this->class = $class;
$this->language = $language;
$this->code = $code;
$this->description = $description;
}
/**
* @ORM\ManyToOne(targetEntity="WordClass", inversedBy="flags")
*/
protected $class;
public function getClass()
{
return $this->class;
}
public function setClass($class)
{
$this->class = $class;
return $this;
}
/**
* @ORM\ManyToOne(targetEntity="Language", inversedBy="flags")
*/
protected $language;
public function getLanguage()
{
return $this->language;
}
public function setLanguage($language)
{
$this->language = $language;
return $this;
}
/**
* @ORM\Column(type="string")
*/
protected $code;
public function getCode()
{
return $this->code;
}
public function setCode($code)
{
$this->code = $code;
return $this;
}
/**
* @ORM\Column(type="string")
*/
protected $description;
public function getDescription()
{
return $this->description;
}
public function setDescription($description)
{
$this->description = $description;
return $this;
}
}
?>

View File

@@ -0,0 +1,77 @@
<?php
namespace App\Model\Database\Entity;
use Doctrine\ORM\Mapping as ORM;
use App\Model\Database\Entity\Attributes\TId;
/**
* @ORM\Entity
*/
class Translation extends AbstractEntity
{
use Tid;
public function __construct($dictionary,$lang1,$lang2,$lang_name1,$lang_name2,$direction)
{
$this->dictionary = $dictionary;
$this->lang1 = $lang1;
$this->lang2 = $lang2;
$this->lang_name1 = $lang_name1;
$this->lang_name2 = $lang_name2;
$this->direction = $direction;
}
/**
* @ORM\ManyToOne(targetEntity="Dictionary", inversedBy="translations")
*/
protected $dictionary;
public function getDictionary()
{
return $this->dictionary;
}
/**
* @ORM\ManyToOne(targetEntity="Language", inversedBy="translations1")
*/
protected $lang1;
/**
* @ORM\ManyToOne(targetEntity="Language", inversedBy="translations2")
*/
protected $lang2;
/**
* @ORM\Column(type="string")
*/
protected $lang_name1;
public function getLangName1()
{
return $this->lang_name1;
}
/**
* @ORM\Column(type="string")
*/
protected $lang_name2;
public function getLangName2()
{
return $this->lang_name2;
}
/**
* @ORM\Column(type="smallint")
*/
protected $direction;
public function getDirection()
{
return $this->direction;
}
}
?>

View File

@@ -0,0 +1,205 @@
<?php declare(strict_types = 1);
namespace App\Model\Database\Entity;
use App\Model\Database\Entity\Attributes\TCreatedAt;
use App\Model\Database\Entity\Attributes\TId;
use App\Model\Database\Entity\Attributes\TUpdatedAt;
use App\Model\Exception\Logic\InvalidArgumentException;
use App\Model\Security\Identity;
use App\Model\Utils\DateTime;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="App\Model\Database\Repository\UserRepository")
* @ORM\Table(name="`user`")
* @ORM\HasLifecycleCallbacks
*/
class User extends AbstractEntity
{
public const ROLE_ADMIN = 'admin';
public const ROLE_USER = 'user';
public const STATE_FRESH = 1;
public const STATE_ACTIVATED = 2;
public const STATE_BLOCKED = 3;
public const STATES = [self::STATE_FRESH, self::STATE_BLOCKED, self::STATE_ACTIVATED];
use TId;
use TCreatedAt;
use TUpdatedAt;
/**
* @var string
* @ORM\Column(type="string", length=255, nullable=FALSE, unique=false)
*/
private $name;
/**
* @var string
* @ORM\Column(type="string", length=255, nullable=FALSE, unique=false)
*/
private $surname;
/**
* @var string
* @ORM\Column(type="string", length=255, nullable=FALSE, unique=TRUE)
*/
private $email;
/**
* @var string
* @ORM\Column(type="string", length=255, nullable=FALSE, unique=TRUE)
*/
private $username;
/**
* @var int
* @ORM\Column(type="integer", length=10, nullable=FALSE)
*/
private $state;
/**
* @var string
* @ORM\Column(type="string", length=255, nullable=FALSE)
*/
private $password;
/**
* @var string
* @ORM\Column(type="string", length=255, nullable=FALSE)
*/
private $role;
/**
* @var DateTime|NULL
* @ORM\Column(type="datetime", nullable=TRUE)
*/
private $lastLoggedAt;
public function __construct(string $name, string $surname, string $email, string $username, string $passwordHash)
{
$this->name = $name;
$this->surname = $surname;
$this->email = $email;
$this->username = $username;
$this->password = $passwordHash;
$this->role = self::ROLE_USER;
$this->state = self::STATE_FRESH;
}
public function changeLoggedAt(): void
{
$this->lastLoggedAt = new DateTime();
}
public function getEmail(): string
{
return $this->email;
}
public function getUsername(): string
{
return $this->username;
}
public function changeUsername(string $username): void
{
$this->username = $username;
}
public function getLastLoggedAt(): ?DateTime
{
return $this->lastLoggedAt;
}
public function getRole(): string
{
return $this->role;
}
public function setRole(string $role): void
{
$this->role = $role;
}
public function getPasswordHash(): string
{
return $this->password;
}
public function changePasswordHash(string $password): void
{
$this->password = $password;
}
public function block(): void
{
$this->state = self::STATE_BLOCKED;
}
public function activate(): void
{
$this->state = self::STATE_ACTIVATED;
}
public function isActivated(): bool
{
return $this->state === self::STATE_ACTIVATED;
}
public function getName(): string
{
return $this->name;
}
public function getSurname(): string
{
return $this->surname;
}
public function getFullname(): string
{
return $this->name . ' ' . $this->surname;
}
public function rename(string $name, string $surname): void
{
$this->name = $name;
$this->surname = $surname;
}
public function getState(): int
{
return $this->state;
}
public function setState(int $state): void
{
if (!in_array($state, self::STATES)) {
throw new InvalidArgumentException(sprintf('Unsupported state %s', $state));
}
$this->state = $state;
}
public function getGravatar(): string
{
return 'https://www.gravatar.com/avatar/' . md5($this->email);
}
public function toIdentity(): Identity
{
return new Identity($this->getId(), [$this->role], [
'email' => $this->email,
'name' => $this->name,
'surname' => $this->surname,
'state' => $this->state,
'gravatar' => $this->getGravatar(),
]);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Model\Database\Entity;
use Doctrine\ORM\Mapping as ORM;
use App\Model\Database\Entity\Attributes\TId;
/**
* @ORM\Entity
*/
class WordClass extends AbstractEntity
{
use Tid;
public function __construct($id,$name)
{
$this->id = $id;
$this->name = $name;
}
public function setId($id)
{
$this->id = $id;
return $id;
}
public function getId()
{
return $this->id;
}
/**
* @ORM\Column(type="string")
*/
protected $name;
public function getName()
{
return $this->id;
}
public function setName($name)
{
$this->name = $name;
return $this;
}
}

View File

@@ -0,0 +1,28 @@
<?php declare(strict_types = 1);
namespace App\Model\Database;
use App\Model\Database\Repository\AbstractRepository;
use Doctrine\Persistence\ObjectRepository;
use Nettrine\ORM\EntityManagerDecorator;
class EntityManager extends EntityManagerDecorator
{
use TRepositories;
/**
* @param string $entityName
* @return AbstractRepository<T>|ObjectRepository<T>
* @internal
* @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint
* @phpstan-template T of object
* @phpstan-param class-string<T> $entityName
* @phpstan-return ObjectRepository<T>
*/
public function getRepository($entityName): ObjectRepository
{
return parent::getRepository($entityName);
}
}

View File

@@ -0,0 +1,49 @@
<?php declare(strict_types = 1);
namespace App\Model\Database\Repository;
use Doctrine\ORM\EntityRepository;
/**
* @phpstan-template TEntityClass of object
* @phpstan-extends EntityRepository<TEntityClass>
*/
abstract class AbstractRepository extends EntityRepository
{
/**
* Fetches all records like $key => $value pairs
*
* @param mixed[] $criteria
* @param mixed[] $orderBy
* @return mixed[]
*/
public function findPairs(?string $key, string $value, array $criteria = [], array $orderBy = []): array
{
if ($key === null) {
$key = $this->getClassMetadata()->getSingleIdentifierFieldName();
}
$qb = $this->createQueryBuilder('e')
->select(['e.' . $value, 'e.' . $key])
->resetDQLPart('from')
->from($this->getEntityName(), 'e', 'e.' . $key);
foreach ($criteria as $k => $v) {
if (is_array($v)) {
$qb->andWhere(sprintf('e.%s IN(:%s)', $k, $k))->setParameter($k, array_values($v));
} else {
$qb->andWhere(sprintf('e.%s = :%s', $k, $k))->setParameter($k, $v);
}
}
foreach ($orderBy as $column => $order) {
$qb->addOrderBy($column, $order);
}
return array_map(function ($row) {
return reset($row);
}, $qb->getQuery()->getArrayResult());
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App;
use Kdyby;
use Nette;
class DictTypes extends Nette\Object
{
private $em;
private $dicttypes;
public function __construct(Kdyby\Doctrine\EntityManager $em)
{
$this->em = $em;
$this->dicttypes = $em->getRepository(DictType::class);
}
public function find($value)
{
return $this->dicttypes->find($value);
}
public function findBy($criteria = [],$orderBy = [])
{
return $this->dicttypes->findBy($criteria,$orderBy);
}
public function findPairs($criteria,$value,$orderBy,$key)
{
return $this->dicttypes->findPairs($criteria,$value,$orderBy,$key);
}
public function findAssoc($criteria, $key = NULL)
{
return $this->dicttypes->findAssoc($criteria,$key);
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App;
use Kdyby;
use Nette;
class Dictionaries extends Nette\Object
{
private $em;
private $dictionaries;
public function __construct(Kdyby\Doctrine\EntityManager $em)
{
$this->em = $em;
$this->dictionaries = $em->getRepository(Dictionary::class);
}
public function findAll($criteria = [],$orderBy = [])
{
return $this->dictionaries->findBy($criteria,$orderBy);
}
public function findPairs($criteria,$value,$orderBy,$key)
{
return $this->dictionaries->findPairs($criteria,$value,$orderBy,$key);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App;
use Kdyby;
use Nette;
class Pronunciations extends Nette\Object
{
private $em;
private $pronunciations;
public function __construct(Kdyby\Doctrine\EntityManager $em)
{
$this->em = $em;
$this->pronunciations = $em->getRepository(Pronunciation::class);
}
public function find($value)
{
return $this->pronunciations->find($value);
}
public function findBy($criteria = [],$orderBy = [])
{
return $this->pronunciations->findBy($criteria,$orderBy);
}
public function findPairs($criteria,$value,$orderBy,$key)
{
return $this->pronunciations->findPairs($criteria,$value,$orderBy,$key);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App;
use Kdyby;
use Nette;
class TermFlags extends Nette\Object
{
private $em;
private $termFlags;
public function __construct(Kdyby\Doctrine\EntityManager $em)
{
$this->em = $em;
$this->termFlags = $em->getRepository(TermFlag::class);
}
public function find($value)
{
return $this->termFlags->find($value);
}
public function findBy($criteria = [],$orderBy = [])
{
return $this->termFlags->findBy($criteria,$orderBy);
}
public function findPairs($criteria,$value,$orderBy,$key)
{
return $this->termFlags->findPairs($criteria,$value,$orderBy,$key);
}
public function findAssoc($criteria,$key)
{
return $this->termFlags->findAssoc($criteria,$key);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App;
use Kdyby;
use Nette;
class Terms extends Nette\Object
{
private $em;
private $termsDao;
private $terms;
public function __construct(Kdyby\Doctrine\EntityManager $em)
{
$this->em = $em;
$this->terms = $em->getRepository(Term::class);
$this->termsDao = $em->getDao(Term::class);
}
public function findAll($criteria = [],$orderBy = [])
{
return $this->terms->findBy($criteria,$orderBy);
}
public function findBy($criteria = [],$orderBy = [])
{
return $this->terms->findBy($criteria,$orderBy);
}
public function find($id)
{
return $this->terms->find($id);
}
public function findPairs($criteria,$value,$orderBy,$key)
{
return $this->terms->findPairs($criteria,$value,$orderBy,$key);
}
public function fetch($query)
{
return $this->termsDao->fetch($query);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App;
use Kdyby;
use Nette;
class Translations extends Nette\Object
{
private $em;
private $translations;
public function __construct(Kdyby\Doctrine\EntityManager $em)
{
$this->em = $em;
$this->translations = $em->getRepository(Translation::class);
}
public function find($value)
{
return $this->translations->find($value);
}
public function findBy($criteria = [],$orderBy = [])
{
return $this->translations->findBy($criteria,$orderBy);
}
public function findPairs($criteria,$value,$orderBy,$key)
{
return $this->translations->findPairs($criteria,$value,$orderBy,$key);
}
}

View File

@@ -0,0 +1,22 @@
<?php declare(strict_types = 1);
namespace App\Model\Database\Repository;
use App\Model\Database\Entity\User;
/**
* @method User|NULL find($id, ?int $lockMode = NULL, ?int $lockVersion = NULL)
* @method User|NULL findOneBy(array $criteria, array $orderBy = NULL)
* @method User[] findAll()
* @method User[] findBy(array $criteria, array $orderBy = NULL, ?int $limit = NULL, ?int $offset = NULL)
* @extends AbstractRepository<User>
*/
class UserRepository extends AbstractRepository
{
public function findOneByEmail(string $email): ?User
{
return $this->findOneBy(['email' => $email]);
}
}

View File

@@ -0,0 +1,19 @@
<?php declare(strict_types = 1);
namespace App\Model\Database;
use App\Model\Database\Entity\User;
use App\Model\Database\Repository\UserRepository;
/**
* @mixin EntityManager
*/
trait TRepositories
{
public function getUserRepository(): UserRepository
{
return $this->getRepository(User::class);
}
}

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace App\Model\Exception\Logic;
use App\Model\Exception\LogicException;
final class InvalidArgumentException extends LogicException
{
}

View File

@@ -0,0 +1,8 @@
<?php declare(strict_types = 1);
namespace App\Model\Exception;
class LogicException extends \LogicException
{
}

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace App\Model\Exception\Runtime;
use Nette\Security\AuthenticationException as NetteAuthenticationException;
final class AuthenticationException extends NetteAuthenticationException
{
}

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace App\Model\Exception\Runtime;
use App\Model\Exception\RuntimeException;
final class IOException extends RuntimeException
{
}

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace App\Model\Exception\Runtime;
use App\Model\Exception\RuntimeException;
final class InvalidStateException extends RuntimeException
{
}

View File

@@ -0,0 +1,8 @@
<?php declare(strict_types = 1);
namespace App\Model\Exception;
class RuntimeException extends \RuntimeException
{
}

View File

@@ -0,0 +1,35 @@
<?php declare(strict_types = 1);
namespace App\Model\Latte;
use Latte\Engine;
final class FilterExecutor
{
/** @var Engine */
private $latte;
public function __construct(Engine $latte)
{
$this->latte = $latte;
}
/**
* @param mixed[] $args
* @return mixed
*/
public function __call(string $name, array $args)
{
return $this->latte->invokeFilter($name, $args);
}
/**
* @return mixed
*/
public function __get(string $name)
{
return $this->latte->invokeFilter($name, []);
}
}

View File

@@ -0,0 +1,30 @@
<?php declare(strict_types = 1);
namespace App\Model\Latte;
use Nette\Neon\Neon;
use Nette\StaticClass;
use Nette\Utils\Json;
final class Filters
{
use StaticClass;
/**
* @param mixed $value
*/
public static function neon($value): string
{
return Neon::encode($value, Neon::BLOCK);
}
/**
* @param mixed $value
*/
public static function json($value): string
{
return Json::encode($value);
}
}

View File

@@ -0,0 +1,16 @@
<?php declare(strict_types = 1);
namespace App\Model\Latte;
use Latte\Compiler;
use Latte\Macros\MacroSet;
final class Macros extends MacroSet
{
public static function register(Compiler $compiler): void
{
$compiler = new static($compiler);
}
}

View File

@@ -0,0 +1,51 @@
<?php declare(strict_types = 1);
namespace App\Model\Latte;
use App\Model\Security\SecurityUser;
use Nette\Application\UI\Control;
use Nette\Bridges\ApplicationLatte\LatteFactory;
use Nette\Bridges\ApplicationLatte\Template;
use Nette\Bridges\ApplicationLatte\TemplateFactory as NetteTemplateFactory;
use Nette\Caching\IStorage;
use Nette\Http\IRequest;
final class TemplateFactory extends NetteTemplateFactory
{
/** @var LatteFactory */
private $latteFactory;
/** @var SecurityUser */
private $user;
public function __construct(
LatteFactory $latteFactory,
IRequest $httpRequest,
SecurityUser $user,
IStorage $cacheStorage,
string $templateClass = null
)
{
parent::__construct($latteFactory, $httpRequest, $user, $cacheStorage, $templateClass);
$this->latteFactory = $latteFactory;
$this->user = $user;
}
public function createTemplate(Control $control = null, string $class = null): Template
{
/** @var Template $template */
$template = parent::createTemplate($control);
// Remove default $template->user for prevent misused
unset($template->user);
// Assign new variables
$template->_user = $this->user;
$template->_template = $template;
$template->_filters = new FilterExecutor($this->latteFactory->create());
return $template;
}
}

View File

@@ -0,0 +1,20 @@
<?php declare(strict_types = 1);
namespace App\Model\Latte;
use App\Model\Security\SecurityUser;
use App\Modules\Base\BasePresenter;
use App\UI\Control\BaseControl;
use Nette\Bridges\ApplicationLatte\Template;
/**
* @property-read SecurityUser $_user
* @property-read BasePresenter $presenter
* @property-read BaseControl $control
* @property-read string $baseUri
* @property-read string $basePath
* @property-read array $flashes
*/
final class TemplateProperty extends Template
{
}

View File

@@ -0,0 +1,55 @@
<?php declare(strict_types = 1);
namespace App\Model\Router;
use Nette\Application\Routers\Route;
use Nette\Application\Routers\RouteList;
final class RouterFactory
{
public function create(): RouteList
{
$router = new RouteList();
$this->buildMailing($router);
$this->buildPdf($router);
$this->buildAdmin($router);
$this->buildFront($router);
return $router;
}
protected function buildAdmin(RouteList $router): RouteList
{
$router[] = $list = new RouteList('Admin');
$list[] = new Route('admin/<presenter>/<action>[/<id>]', 'Home:default');
return $router;
}
protected function buildFront(RouteList $router): RouteList
{
$router[] = $list = new RouteList('Front');
$list[] = new Route('<presenter>/<action>[/<id>]', 'Home:default');
return $router;
}
protected function buildMailing(RouteList $router): RouteList
{
$router[] = $list = new RouteList('Mailing');
$list[] = new Route('mailing/<presenter>/<action>[/<id>]', 'Home:default');
return $router;
}
protected function buildPdf(RouteList $router): RouteList
{
$router[] = $list = new RouteList('Pdf');
$list[] = new Route('pdf/<presenter>/<action>[/<id>]', 'Home:default');
return $router;
}
}

View File

@@ -0,0 +1,57 @@
<?php declare(strict_types = 1);
namespace App\Model\Security\Authenticator;
use App\Model\Database\Entity\User;
use App\Model\Database\EntityManager;
use App\Model\Exception\Runtime\AuthenticationException;
use App\Model\Security\Passwords;
use Nette\Security\Authenticator;
use Nette\Security\IIdentity;
final class UserAuthenticator implements Authenticator
{
/** @var EntityManager */
private $em;
/** @var Passwords */
private $passwords;
public function __construct(EntityManager $em, Passwords $passwords)
{
$this->em = $em;
$this->passwords = $passwords;
}
/**
* @param string $username
* @param string $password
* @throws AuthenticationException
*/
public function authenticate(string $username, string $password): IIdentity
{
$user = $this->em->getUserRepository()->findOneBy(['email' => $username]);
if (!$user) {
throw new AuthenticationException('The username is incorrect.', self::IDENTITY_NOT_FOUND);
} elseif (!$user->isActivated()) {
throw new AuthenticationException('The user is not active.', self::INVALID_CREDENTIAL);
} elseif (!$this->passwords->verify($password, $user->getPasswordHash())) {
throw new AuthenticationException('The password is incorrect.', self::INVALID_CREDENTIAL);
}
$user->changeLoggedAt();
$this->em->flush();
return $this->createIdentity($user);
}
protected function createIdentity(User $user): IIdentity
{
$this->em->flush();
return $user->toIdentity();
}
}

View File

@@ -0,0 +1,47 @@
<?php declare(strict_types = 1);
namespace App\Model\Security\Authorizator;
use App\Model\Database\Entity\User;
use Nette\Security\Permission;
final class StaticAuthorizator extends Permission
{
/**
* Create ACL
*/
public function __construct()
{
$this->addRoles();
$this->addResources();
$this->addPermissions();
}
/**
* Setup roles
*/
protected function addRoles(): void
{
$this->addRole(User::ROLE_ADMIN);
}
/**
* Setup resources
*/
protected function addResources(): void
{
$this->addResource('Admin:Home');
}
/**
* Setup ACL
*/
protected function addPermissions(): void
{
$this->allow(User::ROLE_ADMIN, [
'Admin:Home',
]);
}
}

View File

@@ -0,0 +1,15 @@
<?php declare(strict_types = 1);
namespace App\Model\Security;
use Nette\Security\SimpleIdentity as NetteIdentity;
class Identity extends NetteIdentity
{
public function getFullname(): string
{
return sprintf('%s %s', $this->data['name'] ?? '', $this->data['surname'] ?? '');
}
}

View File

@@ -0,0 +1,15 @@
<?php declare(strict_types = 1);
namespace App\Model\Security;
use Nette\Security\Passwords as NettePasswords;
final class Passwords extends NettePasswords
{
public static function create(): Passwords
{
return new Passwords();
}
}

View File

@@ -0,0 +1,19 @@
<?php declare(strict_types = 1);
namespace App\Model\Security;
use App\Model\Database\Entity\User;
use Nette\Security\User as NetteUser;
/**
* @method Identity getIdentity()
*/
final class SecurityUser extends NetteUser
{
public function isAdmin(): bool
{
return $this->isInRole(User::ROLE_ADMIN);
}
}

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace App\Model\Utils;
use Nette\Utils\Arrays as NetteArrays;
final class Arrays extends NetteArrays
{
}

View File

@@ -0,0 +1,13 @@
<?php declare(strict_types = 1);
namespace App\Model\Utils;
use Nette\Utils\DateTime as ContributteDateTime;
/**
* @method DateTime modifyClone(string $modify = '')
*/
final class DateTime extends ContributteDateTime
{
}

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace App\Model\Utils;
use Contributte\Utils\FileSystem as ContributteFileSystem;
final class FileSystem extends ContributteFileSystem
{
}

10
app/model/Utils/Html.php Normal file
View File

@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace App\Model\Utils;
use Nette\Utils\Html as NetteHtml;
final class Html extends NetteHtml
{
}

10
app/model/Utils/Image.php Normal file
View File

@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace App\Model\Utils;
use Nette\Utils\Image as NetteImage;
class Image extends NetteImage
{
}

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace App\Model\Utils;
use Contributte\Utils\Strings as ContributteStrings;
final class Strings extends ContributteStrings
{
}

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace App\Model\Utils;
use Contributte\Utils\Validators as ContributteValidators;
final class Validators extends ContributteValidators
{
}

View File

@@ -0,0 +1,26 @@
<?php declare(strict_types = 1);
namespace App\Modules\Admin;
use App\Model\App;
use App\Modules\Base\SecuredPresenter;
use Nette\Application\UI\ComponentReflection;
abstract class BaseAdminPresenter extends SecuredPresenter
{
/**
* @param ComponentReflection|mixed $element
* @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint
*/
public function checkRequirements($element): void
{
parent::checkRequirements($element);
if (!$this->user->isAllowed('Admin:Home')) {
$this->flashError('You cannot access this with user role');
$this->redirect(App::DESTINATION_FRONT_HOMEPAGE);
}
}
}

View File

@@ -0,0 +1,31 @@
<?php declare(strict_types = 1);
namespace App\Modules\Admin\Home;
use App\Domain\Order\Event\OrderCreated;
use App\Modules\Admin\BaseAdminPresenter;
use Nette\Application\UI\Form;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
final class HomePresenter extends BaseAdminPresenter
{
/** @var EventDispatcherInterface @inject */
public $dispatcher;
protected function createComponentOrderForm(): Form
{
$form = new Form();
$form->addText('order', 'Order name')
->setRequired(true);
$form->addSubmit('send', 'OK');
$form->onSuccess[] = function (Form $form): void {
$this->dispatcher->dispatch(new OrderCreated($form->values->order), OrderCreated::NAME);
};
return $form;
}
}

View File

@@ -0,0 +1,8 @@
{block #content}
<h1>SECRET PAGE!</h1>
<a n:href="Sign:out" class="btn btn-danger">Logout</a>
<hr>
<h2>Orders</h2>
{control orderForm}

View File

@@ -0,0 +1,75 @@
<?php declare(strict_types = 1);
namespace App\Modules\Admin\Sign;
use App\Model\App;
use App\Model\Exception\Runtime\AuthenticationException;
use App\Modules\Admin\BaseAdminPresenter;
use App\UI\Form\BaseForm;
use App\UI\Form\FormFactory;
use Nette\Application\UI\ComponentReflection;
final class SignPresenter extends BaseAdminPresenter
{
/** @var string @persistent */
public $backlink;
/** @var FormFactory @inject */
public $formFactory;
/**
* @param ComponentReflection|mixed $element
* @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint
*/
public function checkRequirements($element): void
{
}
public function actionIn(): void
{
if ($this->user->isLoggedIn()) {
$this->redirect(App::DESTINATION_AFTER_SIGN_IN);
}
}
public function actionOut(): void
{
if ($this->user->isLoggedIn()) {
$this->user->logout();
$this->flashSuccess('_front.sign.out.success');
}
$this->redirect(App::DESTINATION_AFTER_SIGN_OUT);
}
protected function createComponentLoginForm(): BaseForm
{
$form = $this->formFactory->forBackend();
$form->addEmail('email')
->setRequired(true);
$form->addPassword('password')
->setRequired(true);
$form->addCheckbox('remember')
->setDefaultValue(true);
$form->addSubmit('submit');
$form->onSuccess[] = [$this, 'processLoginForm'];
return $form;
}
public function processLoginForm(BaseForm $form): void
{
try {
$this->user->setExpiration($form->values->remember ? '14 days' : '20 minutes');
$this->user->login($form->values->email, $form->values->password);
} catch (AuthenticationException $e) {
$form->addError('Invalid username or password');
return;
}
$this->redirect(App::DESTINATION_AFTER_SIGN_IN);
}
}

View File

@@ -0,0 +1,29 @@
{block #content}
<form n:name="loginForm" class="form-signin">
<div class="text-center mb-4">
<img class="mb-4" src="https://avatars0.githubusercontent.com/u/99965?s=200&v=4" alt="" width="72" height="72">
<h1 class="h3 mb-3 font-weight-normal">Webapp Skeleton Admin</h1>
</div>
<div n:foreach="$form->errors as $error" class="alert alert-danger" role="alert">
{$error}
</div>
<div class="form-label-group">
<input type="email" n:name="email" class="form-control" placeholder="Email address" required autofocus>
<label n:name="email">Email address</label>
</div>
<div class="form-label-group">
<input type="password" n:name="password" class="form-control" placeholder="Password" required>
<label n:name="password">Password</label>
</div>
<div class="checkbox mb-3">
<label>
<input n:name="remember" type="checkbox"> Remember me
</label>
</div>
<button n:name="submit" class="btn btn-lg btn-primary btn-block">Sign in</button>
<p class="mt-5 mb-3 text-muted text-center">&copy; {=date('Y')}</p>
</form>

View File

@@ -0,0 +1,7 @@
{layout '../../Base/templates/@layout.latte'}
{block #head}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.2.1/css/bootstrap.min.css" defer />
<link rel="stylesheet" href="{$basePath}/assets/admin.css" defer />
<script src="{$basePath}/assets/admin.js" defer></script>
{/block}

View File

@@ -0,0 +1,44 @@
<?php declare(strict_types = 1);
namespace App\Modules\Base;
use App\Model\Exception\Runtime\InvalidStateException;
use Nette\Application\BadRequestException;
use Nette\Application\Request;
use Nette\Application\UI\ComponentReflection;
abstract class BaseError4xxPresenter extends SecuredPresenter
{
/**
* Common presenter method
*/
public function startup(): void
{
parent::startup();
if ($this->getRequest() !== null && $this->getRequest()->isMethod(Request::FORWARD)) {
return;
}
$this->error();
}
public function renderDefault(BadRequestException $exception): void
{
$rf1 = new ComponentReflection(static::class);
$fileName = $rf1->getFileName();
// Validate if class is not in PHP core
if ($fileName === false) {
throw new InvalidStateException('Class is defined in the PHP core or in a PHP extension');
}
$dir = dirname($fileName);
// Load template 403.latte or 404.latte or ... 4xx.latte
$file = $dir . '/templates/' . $exception->getCode() . '.latte';
$this->template->setFile(is_file($file) ? $file : $dir . '/templates/4xx.latte');
}
}

View File

@@ -0,0 +1,57 @@
<?php declare(strict_types = 1);
namespace App\Modules\Base;
use Nette\Application\BadRequestException;
use Nette\Application\Helpers;
use Nette\Application\IResponse as AppResponse;
use Nette\Application\Request;
use Nette\Application\Responses\CallbackResponse;
use Nette\Application\Responses\ForwardResponse;
use Nette\Http\IRequest;
use Nette\Http\IResponse;
use Psr\Log\LogLevel;
use Throwable;
use Tracy\Debugger;
use Tracy\ILogger;
abstract class BaseErrorPresenter extends SecuredPresenter
{
/**
* @return ForwardResponse|CallbackResponse
*/
public function run(Request $request): AppResponse
{
$e = $request->getParameter('exception');
if ($e instanceof Throwable) {
$code = $e->getCode();
$level = ($code >= 400 && $code <= 499) ? LogLevel::WARNING : LogLevel::ERROR;
Debugger::log(sprintf(
'Code %s: %s in %s:%s',
$code,
$e->getMessage(),
$e->getFile(),
$e->getLine()
), $level);
Debugger::log($e, ILogger::EXCEPTION);
}
if ($e instanceof BadRequestException) {
[$module, , $sep] = Helpers::splitName($request->getPresenterName());
return new ForwardResponse($request->setPresenterName($module . $sep . 'Error4xx'));
}
return new CallbackResponse(function (IRequest $httpRequest, IResponse $httpResponse): void {
$header = $httpResponse->getHeader('Content-Type');
if ($header !== null && preg_match('#^text/html(?:;|$)#', $header)) {
require __DIR__ . '/templates/500.phtml';
}
});
}
}

View File

@@ -0,0 +1,23 @@
<?php declare(strict_types = 1);
namespace App\Modules\Base;
use App\Model\Latte\TemplateProperty;
use App\Model\Security\SecurityUser;
use App\UI\Control\TFlashMessage;
use App\UI\Control\TModuleUtils;
use Contributte\Application\UI\Presenter\StructuredTemplates;
use Nette\Application\UI\Presenter;
/**
* @property-read TemplateProperty $template
* @property-read SecurityUser $user
*/
abstract class BasePresenter extends Presenter
{
use StructuredTemplates;
use TFlashMessage;
use TModuleUtils;
}

View File

@@ -0,0 +1,30 @@
<?php declare(strict_types = 1);
namespace App\Modules\Base;
use App\Model\App;
use Nette\Application\UI\ComponentReflection;
use Nette\Security\IUserStorage;
abstract class SecuredPresenter extends BasePresenter
{
/**
* @param ComponentReflection|mixed $element
* @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint
*/
public function checkRequirements($element): void
{
if (!$this->user->isLoggedIn()) {
if ($this->user->getLogoutReason() === IUserStorage::INACTIVITY) {
$this->flashInfo('You have been logged out for inactivity');
}
$this->redirect(
App::DESTINATION_SIGN_IN,
['backlink' => $this->storeRequest()]
);
}
}
}

View File

@@ -0,0 +1,8 @@
<?php declare(strict_types = 1);
namespace App\Modules\Base;
abstract class UnsecuredPresenter extends BasePresenter
{
}

View File

@@ -0,0 +1,27 @@
<!DOCTYPE html><!-- "' --></textarea></script></style></pre></xmp></a></audio></button></canvas></datalist></details></dialog></iframe></listing></meter></noembed></noframes></noscript></optgroup></option></progress></rp></select></table></template></title></video>
<meta charset="utf-8">
<meta name="robots" content="noindex">
<title>Server Error</title>
<style>
#nette-error { all: initial; position: absolute; top: 0; left: 0; right: 0; height: 70vh; min-height: 400px; display: flex; align-items: center; justify-content: center; z-index: 1000 }
#nette-error div { all: initial; max-width: 550px; background: white; color: #333; display: block }
#nette-error h1 { all: initial; font: bold 50px/1.1 sans-serif; display: block; margin: 40px }
#nette-error p { all: initial; font: 20px/1.4 sans-serif; margin: 40px; display: block }
#nette-error small { color: gray }
</style>
<div id=nette-error>
<div>
<h1>Server Error</h1>
<p>We're sorry! The server encountered an internal error and
was unable to complete your request. Please try again later.</p>
<p><small>error 500</small></p>
</div>
</div>
<script>
document.body.insertBefore(document.getElementById('nette-error'), document.body.firstChild);
</script>

View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="shortcut icon" href="{$basePath}/favicon.ico">
<!-- Seo -->
<title>{block #title|stripHtml|trim}Webapp Skeleton{/} | Contributte</title>
<!-- Meta -->
<meta name="description" n:ifset="#description" content="{include #description}">
<meta name="keywords" n:ifset="#keywords" content="{include #keywords}">
<meta name="robots" content="index,follow">
<meta name="googlebot" content="snippet,archive">
<meta name="author" content="f3l1x">
{block #head}{/}
</head>
<body>
{block #main}
<div class="container">
{include #content}
</div>
{/}
</body>
</html>

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace App\Modules\Front;
use App\Modules\Base\UnsecuredPresenter;
abstract class BaseFrontPresenter extends UnsecuredPresenter
{
}

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace App\Modules\Front\Error;
use App\Modules\Base\BaseErrorPresenter;
final class ErrorPresenter extends BaseErrorPresenter
{
}

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace App\Modules\Front\Error4xx;
use App\Modules\Base\BaseError4xxPresenter;
final class Error4xxPresenter extends BaseError4xxPresenter
{
}

View File

@@ -0,0 +1,7 @@
{block #content}
<h1 n:block=title>Access Denied</h1>
<p>You do not have permission to view this page. Please try contact the web
site administrator if you believe you should be able to view this page.</p>
<p><small>error 403</small></p>

View File

@@ -0,0 +1,8 @@
{block #content}
<h1 n:block=title>Page Not Found</h1>
<p>The page you requested could not be found. It is possible that the address is
incorrect, or that the page no longer exists. Please use a search engine to find
what you are looking for.</p>
<p><small>error 404</small></p>

View File

@@ -0,0 +1,6 @@
{block #content}
<h1 n:block=title>Method Not Allowed</h1>
<p>The requested method is not allowed for the URL.</p>
<p><small>error 405</small></p>

View File

@@ -0,0 +1,6 @@
{block #content}
<h1 n:block=title>Page Not Found</h1>
<p>The page you requested has been taken off the site. We apologize for the inconvenience.</p>
<p><small>error 410</small></p>

View File

@@ -0,0 +1,4 @@
{block #content}
<h1 n:block=title>Oops...</h1>
<p>Your browser sent a request that this server could not understand or process.</p>

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace App\Modules\Front\Home;
use App\Modules\Front\BaseFrontPresenter;
final class HomePresenter extends BaseFrontPresenter
{
}

View File

@@ -0,0 +1,2 @@
{block #content}
Welcome

View File

@@ -0,0 +1,38 @@
{layout '../../Base/templates/@layout.latte'}
{block #head}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.min.css" defer/>
<link rel="stylesheet" href="{$basePath}/assets/front.css" defer/>
<script src="{$basePath}/assets/front.js" defer></script>
{/}
{block #main}
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
<header class="masthead mb-auto">
<div class="inner">
<h3 class="masthead-brand">Webapp</h3>
<nav class="nav nav-masthead justify-content-center">
<a n:class="$presenter->isLinkCurrent(':Front:Home:') ? active, nav-link" n:href=":Front:Home:">Home</a>
<a n:class="$presenter->isLinkCurrent(':Admin:Home:') ? active, nav-link" n:href=":Admin:Home:">Admin</a>
<a n:class="$presenter->isLinkCurrent(':Pdf:Home:') ? active, nav-link" n:href=":Pdf:Home:">PDF example</a>
</nav>
</div>
</header>
<main role="main" class="inner cover">
<h1 class="cover-heading">Webapp skeleton.</h1>
<p class="lead">This is example project based on <a href="https://nette.org">Nette Framework</a></p>
<p class="lead">And also many <a href="https://contributte.org">Contributte + Nettrine</a> packages.</p>
<div class="lead">
<a href="https://github.com/contributte/webapp-skeleton" class="btn btn-lg btn-secondary">Learn more in docs</a>
</div>
</main>
<footer class="mastfoot mt-auto">
<div class="inner">
<p>Cover template for <a href="https://getbootstrap.com/">Bootstrap</a>, by
<a href="https://twitter.com/mdo">@mdo</a>.</p>
</div>
</footer>
</div>
{/}

View File

@@ -0,0 +1,35 @@
<?php declare(strict_types = 1);
namespace App\Modules\Mailing\Home;
use Contributte\Mailing\IMailBuilderFactory;
use Nette\Application\UI\Presenter;
class HomePresenter extends Presenter
{
/** @var IMailBuilderFactory */
private $mailBuilderFactory;
public function __construct(IMailBuilderFactory $mailBuilderFactory)
{
parent::__construct();
$this->mailBuilderFactory = $mailBuilderFactory;
}
public function actionDefault(): void
{
$mail = $this->mailBuilderFactory->create();
$mail->setSubject('Example');
$mail->addTo('foo@example.com');
$mail->setTemplateFile(__DIR__ . '/templates/Emails/email.latte');
$mail->setParameters([
'title' => 'Title',
'content' => 'Lorem ipsum dolor sit amet',
]);
$mail->send();
}
}

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>{ifset title}{include title|stripHtml} | {/ifset}Nette Web</title>
</head>
<body>
<div n:foreach="$flashes as $flash" n:class="flash, $flash->type">{$flash->message}</div>
{include content}
{block scripts}
<script src="https://nette.github.io/resources/js/netteForms.min.js"></script>
{/block}
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More