implement Tests\Server, ServerV2 and groupcompactsyntax namespaces

pull/685/head
fadrian06 20 hours ago
parent 08904df138
commit ce753b7cd2

@ -2,14 +2,14 @@
declare(strict_types=1); declare(strict_types=1);
namespace tests\groupcompactsyntax;
use Flight;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use tests\groupcompactsyntax\PostsController; use tests\groupcompactsyntax\PostsController;
use tests\groupcompactsyntax\TodosController; use tests\groupcompactsyntax\TodosController;
use tests\groupcompactsyntax\UsersController; use tests\groupcompactsyntax\UsersController;
require_once __DIR__ . '/UsersController.php';
require_once __DIR__ . '/PostsController.php';
final class FlightRouteCompactSyntaxTest extends TestCase final class FlightRouteCompactSyntaxTest extends TestCase
{ {
public function setUp(): void public function setUp(): void

@ -9,60 +9,82 @@ declare(strict_types=1);
* @author Kristaps Muižnieks https://github.com/krmu * @author Kristaps Muižnieks https://github.com/krmu
*/ */
require file_exists(__DIR__ . '/../../vendor/autoload.php') ? __DIR__ . '/../../vendor/autoload.php' : __DIR__ . '/../../flight/autoload.php'; namespace Tests\ServerV2 {
class AuthCheck
Flight::set('flight.content_length', false); {
Flight::set('flight.views.path', './'); public function before(): void
Flight::set('flight.views.extension', '.phtml'); {
// This enables the old functionality of Flight output buffering if (!isset($_COOKIE['user'])) {
Flight::set('flight.v2.output_buffering', true); echo '<span id="infotext">Middleware text:</span> You are not authorized to access this route!';
}
// Test 1: Root route }
Flight::route('/', function () {
echo '<span id="infotext">Route text:</span> Root route works!';
if (Flight::request()->query->redirected) {
echo '<br>Redirected from /redirect route successfully!';
} }
}); }
Flight::route('/querytestpath', function () {
echo '<span id="infotext">Route text:</span> This ir query route<br>'; namespace {
echo "I got such query parameters:<pre>";
print_r(Flight::request()->query); use Tests\ServerV2\AuthCheck;
echo "</pre>";
}, false, "querytestpath"); require_once __DIR__ . '/../phpunit_autoload.php';
// Test 2: Simple route Flight::set('flight.content_length', false);
Flight::route('/test', function () { Flight::set('flight.views.path', './');
echo '<span id="infotext">Route text:</span> Test route works!'; Flight::set('flight.views.extension', '.phtml');
}); // This enables the old functionality of Flight output buffering
Flight::set('flight.v2.output_buffering', true);
// Test 3: Route with parameter
Flight::route('/user/@name', function ($name) { // Test 1: Root route
echo "<span id='infotext'>Route text:</span> Hello, $name!"; Flight::route('/', function () {
}); echo '<span id="infotext">Route text:</span> Root route works!';
Flight::route('POST /postpage', function () {
echo '<span id="infotext">Route text:</span> THIS IS POST METHOD PAGE'; if (Flight::request()->query['redirected']) {
}, false, "postpage"); echo '<br>Redirected from /redirect route successfully!';
}
// Test 4: Grouped routes });
Flight::group('/group', function () {
Flight::route('/querytestpath', function () {
echo '<span id="infotext">Route text:</span> This ir query route<br>';
echo 'I got such query parameters:<pre>';
print_r(Flight::request()->query);
echo '</pre>';
}, false, 'querytestpath');
// Test 2: Simple route
Flight::route('/test', function () { Flight::route('/test', function () {
echo '<span id="infotext">Route text:</span> Group test route works!'; echo '<span id="infotext">Route text:</span> Test route works!';
}); });
// Test 3: Route with parameter
Flight::route('/user/@name', function ($name) { Flight::route('/user/@name', function ($name) {
echo "<span id='infotext'>Route text:</span> There is variable called name and it is $name"; echo "<span id='infotext'>Route text:</span> Hello, $name!";
}); });
Flight::group('/group1', function () {
Flight::group('/group2', function () { Flight::route('POST /postpage', function () {
Flight::group('/group3', function () { echo '<span id="infotext">Route text:</span> THIS IS POST METHOD PAGE';
Flight::group('/group4', function () { }, false, 'postpage');
Flight::group('/group5', function () {
Flight::group('/group6', function () { // Test 4: Grouped routes
Flight::group('/group7', function () { Flight::group('/group', function () {
Flight::group('/group8', function () { Flight::route('/test', function () {
Flight::route('/final_group', function () { echo '<span id="infotext">Route text:</span> Group test route works!';
echo 'Mega Group test route works!'; });
}, false, "final_group");
Flight::route('/user/@name', function ($name) {
echo "<span id='infotext'>Route text:</span> There is variable called name and it is $name";
});
Flight::group('/group1', function () {
Flight::group('/group2', function () {
Flight::group('/group3', function () {
Flight::group('/group4', function () {
Flight::group('/group5', function () {
Flight::group('/group6', function () {
Flight::group('/group7', function () {
Flight::group('/group8', function () {
Flight::route('/final_group', function () {
echo 'Mega Group test route works!';
}, false, 'final_group');
});
}); });
}); });
}); });
@ -71,152 +93,151 @@ Flight::group('/group', function () {
}); });
}); });
}); });
});
// Test 5: Route alias
// Test 5: Route alias Flight::route('/alias', function () {
Flight::route('/alias', function () { echo '<span id="infotext">Route text:</span> Alias route works!';
echo '<span id="infotext">Route text:</span> Alias route works!'; }, false, 'aliasroute');
}, false, 'aliasroute');
class AuthCheck $middle = new AuthCheck();
{
public function before(): void // Test 6: Route with middleware
{ Flight::route('/protected', function () {
if (!isset($_COOKIE['user'])) { echo '<span id="infotext">Route text:</span> Protected route works!';
echo '<span id="infotext">Middleware text:</span> You are not authorized to access this route!'; })->addMiddleware([$middle]);
// Test 7: Route with template
Flight::route('/template/@name', function ($name) {
Flight::render('template.phtml', ['name' => $name]);
});
// Test 8: Throw an error
Flight::route('/error', function () {
trigger_error('This is a successful error');
});
// Test 9: JSON output (should not output any other html)
Flight::route('/json', function () {
echo "\n\n\n\n\n";
Flight::json(['message' => 'JSON renders successfully!']);
echo "\n\n\n\n\n";
});
// Test 13: JSONP output (should not output any other html)
Flight::route('/jsonp', function () {
echo "\n\n\n\n\n";
Flight::jsonp(['message' => 'JSONP renders successfully!'], 'jsonp');
echo "\n\n\n\n\n";
});
Flight::route('/json-halt', function () {
Flight::jsonHalt(['message' => 'JSON rendered and halted successfully with no other body content!']);
});
// Test 10: Halt
Flight::route('/halt', function () {
Flight::halt(400, 'Halt worked successfully');
});
// Test 11: Redirect
Flight::route('/redirect', function () {
Flight::redirect('/?redirected=1');
});
Flight::set('flight.views.path', './');
Flight::map('error', function (Throwable $error) {
echo "<h1> An error occurred, mapped error method worked, error below </h1>";
echo '<pre style="border: 2px solid red; padding: 21px; background: lightgray; font-weight: bold;">';
echo str_replace(getenv('PWD'), "***CLASSIFIED*****", $error->getTraceAsString());
echo "</pre>";
echo "<a href='/'>Go back</a>";
});
Flight::map('notFound', function () {
echo '<span id="infotext">Route text:</span> The requested URL was not found';
echo "<a href='/'>Go back</a>";
});
echo '
<style>
ul {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
background-color: #333;
} }
}
}
$middle = new AuthCheck();
// Test 6: Route with middleware
Flight::route('/protected', function () {
echo '<span id="infotext">Route text:</span> Protected route works!';
})->addMiddleware([$middle]);
// Test 7: Route with template
Flight::route('/template/@name', function ($name) {
Flight::render('template.phtml', ['name' => $name]);
});
// Test 8: Throw an error
Flight::route('/error', function () {
trigger_error('This is a successful error');
});
// Test 9: JSON output (should not output any other html)
Flight::route('/json', function () {
echo "\n\n\n\n\n";
Flight::json(['message' => 'JSON renders successfully!']);
echo "\n\n\n\n\n";
});
// Test 13: JSONP output (should not output any other html)
Flight::route('/jsonp', function () {
echo "\n\n\n\n\n";
Flight::jsonp(['message' => 'JSONP renders successfully!'], 'jsonp');
echo "\n\n\n\n\n";
});
Flight::route('/json-halt', function () {
Flight::jsonHalt(['message' => 'JSON rendered and halted successfully with no other body content!']);
});
// Test 10: Halt
Flight::route('/halt', function () {
Flight::halt(400, 'Halt worked successfully');
});
// Test 11: Redirect
Flight::route('/redirect', function () {
Flight::redirect('/?redirected=1');
});
Flight::set('flight.views.path', './');
Flight::map('error', function (Throwable $error) {
echo "<h1> An error occurred, mapped error method worked, error below </h1>";
echo '<pre style="border: 2px solid red; padding: 21px; background: lightgray; font-weight: bold;">';
echo str_replace(getenv('PWD'), "***CLASSIFIED*****", $error->getTraceAsString());
echo "</pre>";
echo "<a href='/'>Go back</a>";
});
Flight::map('notFound', function () {
echo '<span id="infotext">Route text:</span> The requested URL was not found';
echo "<a href='/'>Go back</a>";
});
echo '
<style>
ul {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
background-color: #333;
}
li { li {
float: left; float: left;
} }
#infotext { #infotext {
font-weight: bold; font-weight: bold;
color: blueviolet; color: blueviolet;
}
li a {
display: block;
color: white;
text-align: center;
padding: 14px 16px;
text-decoration: none;
} }
li a {
display: block;
color: white;
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
li a:hover { li a:hover {
background-color: #111; background-color: #111;
} }
#container { #container {
color: #333; color: #333;
font-size: 16px; font-size: 16px;
line-height: 1.5; line-height: 1.5;
margin: 20px 0; margin: 20px 0;
padding: 10px; padding: 10px;
border: 1px solid #ddd; border: 1px solid #ddd;
background-color: #f9f9f9; background-color: #f9f9f9;
} }
#debugrequest { #debugrequest {
color: #333; color: #333;
font-size: 16px; font-size: 16px;
line-height: 1.5; line-height: 1.5;
margin: 20px 0; margin: 20px 0;
padding: 10px; padding: 10px;
border: 1px solid #ddd; border: 1px solid #ddd;
background-color: #f9f9f9; background-color: #f9f9f9;
} }
</style> </style>
<ul> <ul>
<li><a href="/">Root Route</a></li> <li><a href="/">Root Route</a></li>
<li><a href="/test">Test Route</a></li> <li><a href="/test">Test Route</a></li>
<li><a href="/user/John">User Route with Parameter (John)</a></li> <li><a href="/user/John">User Route with Parameter (John)</a></li>
<li><a href="/group/test">Grouped Test Route</a></li> <li><a href="/group/test">Grouped Test Route</a></li>
<li><a href="/group/user/Jane">Grouped User Route with Parameter (Jane)</a></li> <li><a href="/group/user/Jane">Grouped User Route with Parameter (Jane)</a></li>
<li><a href="/alias">Alias Route</a></li> <li><a href="/alias">Alias Route</a></li>
<li><a href="/protected">Protected path</a></li> <li><a href="/protected">Protected path</a></li>
<li><a href="/template/templatevariable">Template path</a></li> <li><a href="/template/templatevariable">Template path</a></li>
<li><a href="/querytestpath?test=1&variable2=uuid&variable3=tester">Query path</a></li> <li><a href="/querytestpath?test=1&variable2=uuid&variable3=tester">Query path</a></li>
<li><a href="/postpage">Post method test page - should be 404</a></li> <li><a href="/postpage">Post method test page - should be 404</a></li>
<li><a href="' . Flight::getUrl('final_group') . '">Mega group</a></li> <li><a href="' . Flight::getUrl('final_group') . '">Mega group</a></li>
<li><a href="/error">Error</a></li> <li><a href="/error">Error</a></li>
<li><a href="/json">JSON</a></li> <li><a href="/json">JSON</a></li>
<li><a href="/jsonp?jsonp=myjson">JSONP</a></li> <li><a href="/jsonp?jsonp=myjson">JSONP</a></li>
<li><a href="/json-halt">JSON Halt</a></li> <li><a href="/json-halt">JSON Halt</a></li>
<li><a href="/halt">Halt</a></li> <li><a href="/halt">Halt</a></li>
<li><a href="/redirect">Redirect</a></li> <li><a href="/redirect">Redirect</a></li>
</ul>'; </ul>';
Flight::before('start', function ($params) {
echo '<div id="container">'; Flight::before('start', function () {
}); echo '<div id="container">';
Flight::after('start', function ($params) { });
echo '</div>';
echo '<div id="debugrequest">'; Flight::after('start', function () {
echo "Request information<pre>"; echo '</div>';
print_r(Flight::request()); echo '<div id="debugrequest">';
echo "</pre>"; echo "Request information<pre>";
echo "</div>"; print_r(Flight::request());
}); echo "</pre>";
Flight::start(); echo "</div>";
});
Flight::start();
}

@ -2,6 +2,8 @@
declare(strict_types=1); declare(strict_types=1);
namespace Tests\Server;
class AuthCheck class AuthCheck
{ {
public function before(): void public function before(): void

@ -2,6 +2,10 @@
declare(strict_types=1); declare(strict_types=1);
namespace Tests\Server;
use Flight;
class LayoutMiddleware class LayoutMiddleware
{ {
public function before(): void public function before(): void

@ -2,11 +2,20 @@
declare(strict_types=1); declare(strict_types=1);
namespace Tests\Server;
use Flight;
class OverwriteBodyMiddleware class OverwriteBodyMiddleware
{ {
public function after(): void public function after(): void
{ {
$response = Flight::response(); $response = Flight::response();
$response->write(str_replace('<span style="color:red; font-weight: bold;">failed</span>', '<span style="color:green; font-weight: bold;">successfully works!</span>', $response->getBody()), true);
$response->write(str_replace(
'<span style="color:red; font-weight: bold;">failed</span>',
'<span style="color:green; font-weight: bold;">successfully works!</span>',
$response->getBody()
), true);
} }
} }

@ -2,6 +2,8 @@
declare(strict_types=1); declare(strict_types=1);
namespace Tests\Server;
class Pascal_Snake_Case // phpcs:ignore class Pascal_Snake_Case // phpcs:ignore
{ {
public function doILoad() // phpcs:ignore public function doILoad() // phpcs:ignore

@ -2,10 +2,15 @@
declare(strict_types=1); declare(strict_types=1);
use Dice\Dice;
use flight\core\Loader; use flight\core\Loader;
use flight\database\PdoWrapper; use flight\database\PdoWrapper;
use tests\classes\Container; use tests\classes\Container;
use tests\classes\ContainerDefault; use tests\classes\ContainerDefault;
use Tests\Server\AuthCheck;
use Tests\Server\LayoutMiddleware;
use Tests\Server\OverwriteBodyMiddleware;
use Tests\Server\Pascal_Snake_Case;
/* /*
* This is the test file where we can open up a quick test server and make * This is the test file where we can open up a quick test server and make
@ -14,7 +19,7 @@ use tests\classes\ContainerDefault;
* @author Kristaps Muižnieks https://github.com/krmu * @author Kristaps Muižnieks https://github.com/krmu
*/ */
require file_exists(__DIR__ . '/../../vendor/autoload.php') ? __DIR__ . '/../../vendor/autoload.php' : __DIR__ . '/../../flight/autoload.php'; require_once __DIR__ . '/../phpunit_autoload.php';
Flight::set('flight.content_length', false); Flight::set('flight.content_length', false);
Flight::set('flight.views.path', './'); Flight::set('flight.views.path', './');
@ -23,20 +28,20 @@ Loader::setV2ClassLoading(false);
Flight::path(__DIR__); Flight::path(__DIR__);
Flight::group('', function () { Flight::group('', function () {
// Test 1: Root route // Test 1: Root route
Flight::route('/', function () { Flight::route('/', function () {
echo '<span id="infotext">Route text:</span> Root route works!'; echo '<span id="infotext">Route text:</span> Root route works!';
if (Flight::request()->query->redirected) { if (Flight::request()->query['redirected']) {
echo '<br>Redirected from /redirect route successfully!'; echo '<br>Redirected from /redirect route successfully!';
} }
}); });
Flight::route('/querytestpath', function () { Flight::route('/querytestpath', function () {
echo '<span id="infotext">Route text:</span> This is query route<br>'; echo '<span id="infotext">Route text:</span> This is query route<br>';
echo "Query parameters:<pre>"; echo 'Query parameters:<pre>';
print_r(Flight::request()->query); print_r(Flight::request()->query);
echo "</pre>"; echo '</pre>';
}, false, "querytestpath"); }, false, 'querytestpath');
// Test 2: Simple route // Test 2: Simple route
Flight::route('/test', function () { Flight::route('/test', function () {
@ -47,18 +52,21 @@ Flight::group('', function () {
Flight::route('/user/@name', function ($name) { Flight::route('/user/@name', function ($name) {
echo "<span id='infotext'>Route text:</span> Hello, $name!"; echo "<span id='infotext'>Route text:</span> Hello, $name!";
}); });
Flight::route('POST /postpage', function () { Flight::route('POST /postpage', function () {
echo '<span id="infotext">Route text:</span> THIS IS POST METHOD PAGE'; echo '<span id="infotext">Route text:</span> THIS IS POST METHOD PAGE';
}, false, "postpage"); }, false, 'postpage');
// Test 4: Grouped routes // Test 4: Grouped routes
Flight::group('/group', function () { Flight::group('/group', function () {
Flight::route('/test', function () { Flight::route('/test', function () {
echo '<span id="infotext">Route text:</span> Group test route works!'; echo '<span id="infotext">Route text:</span> Group test route works!';
}); });
Flight::route('/user/@name', function ($name) { Flight::route('/user/@name', function ($name) {
echo "<span id='infotext'>Route text:</span> There is variable called name and it is $name"; echo "<span id='infotext'>Route text:</span> There is variable called name and it is $name";
}); });
Flight::group('/group1', function () { Flight::group('/group1', function () {
Flight::group('/group2', function () { Flight::group('/group2', function () {
Flight::group('/group3', function () { Flight::group('/group3', function () {
@ -69,7 +77,7 @@ Flight::group('', function () {
Flight::group('/group8', function () { Flight::group('/group8', function () {
Flight::route('/final_group', function () { Flight::route('/final_group', function () {
echo 'Mega Group test route works!'; echo 'Mega Group test route works!';
}, false, "final_group"); }, false, 'final_group');
}); });
}); });
}); });
@ -86,8 +94,8 @@ Flight::group('', function () {
}, false, 'aliasroute'); }, false, 'aliasroute');
/** Middleware test */ /** Middleware test */
include_once 'AuthCheck.php';
$middle = new AuthCheck(); $middle = new AuthCheck();
// Test 6: Route with middleware // Test 6: Route with middleware
Flight::route('/protected', function () { Flight::route('/protected', function () {
echo '<span id="infotext">Route text:</span> Protected route works!'; echo '<span id="infotext">Route text:</span> Protected route works!';
@ -119,44 +127,68 @@ Flight::group('', function () {
// Test 12: Redirect with status code // Test 12: Redirect with status code
Flight::route('/streamResponse', function () { Flight::route('/streamResponse', function () {
echo "Streaming a response"; echo 'Streaming a response';
for ($i = 1; $i <= 50; $i++) { for ($i = 1; $i <= 50; $i++) {
echo "."; echo ".";
usleep(50000); usleep(50000);
ob_flush(); ob_flush();
} }
echo "is successful!!";
echo 'is successful!!';
})->stream(); })->stream();
// Test 12: Redirect with status code // Test 12: Redirect with status code
Flight::route('/streamWithHeaders', function () { Flight::route('/streamWithHeaders', function () {
echo "Streaming a response"; echo 'Streaming a response';
for ($i = 1; $i <= 50; $i++) { for ($i = 1; $i <= 50; $i++) {
echo "."; echo ".";
usleep(50000); usleep(50000);
ob_flush(); ob_flush();
} }
echo "is successful!!";
echo 'is successful!!';
})->streamWithHeaders(['Content-Type' => 'text/html', 'status' => 200]); })->streamWithHeaders(['Content-Type' => 'text/html', 'status' => 200]);
// Test 14: Overwrite the body with a middleware // Test 14: Overwrite the body with a middleware
Flight::route('/overwrite', function () { Flight::route('/overwrite', function () {
echo '<span id="infotext">Route text:</span> This route status is that it <span style="color:red; font-weight: bold;">failed</span>'; echo <<<'html'
<span id="infotext">Route text:</span>
This route status is that it
<span style="color:red; font-weight: bold;">failed</span>
html;
})->addMiddleware([new OverwriteBodyMiddleware()]); })->addMiddleware([new OverwriteBodyMiddleware()]);
// Test 15: UTF8 Chars in url // Test 15: UTF8 Chars in url
Flight::route('/わたしはひとです', function () { Flight::route('/わたしはひとです', function () {
echo '<span id="infotext">Route text:</span> This route status is that it <span style="color:green; font-weight: bold;">succeeded はい!!!</span>'; echo <<<'html'
<span id="infotext">Route text:</span>
This route status is that it
<span style="color:green; font-weight: bold;">succeeded はい!!!</span>
html;
}); });
// Test 16: UTF8 Chars in url with utf8 params // Test 16: UTF8 Chars in url with utf8 params
Flight::route('/わたしはひとです/@name', function ($name) { Flight::route('/わたしはひとです/@name', function ($name) {
echo '<span id="infotext">Route text:</span> This route status is that it <span style="color:' . ($name === 'ええ' ? 'green' : 'red') . '; font-weight: bold;">' . ($name === 'ええ' ? 'succeeded' : 'failed') . ' URL Param: ' . $name . '</span>'; echo '<span id="infotext">Route text:</span> This route status is that it <span style="color:'
. ($name === 'ええ' ? 'green' : 'red')
. '; font-weight: bold;">'
. ($name === 'ええ' ? 'succeeded' : 'failed')
. ' URL Param: '
. $name
. '</span>';
}); });
// Test 17: Slash in param // Test 17: Slash in param
Flight::route('/redirect/@id', function ($id) { Flight::route('/redirect/@id', function ($id) {
echo '<span id="infotext">Route text:</span> This route status is that it <span style="color:' . ($id === 'before/after' ? 'green' : 'red') . '; font-weight: bold;">' . ($id === 'before/after' ? 'succeeded' : 'failed') . ' URL Param: ' . $id . '</span>'; echo '<span id="infotext">Route text:</span> This route status is that it <span style="color:'
. ($id === 'before/after' ? 'green' : 'red')
. '; font-weight: bold;">'
. ($id === 'before/after' ? 'succeeded' : 'failed')
. ' URL Param: '
. $id
. '</span>';
}); });
Flight::set('test_me_out', 'You got it boss!'); // used in /no-container route Flight::set('test_me_out', 'You got it boss!'); // used in /no-container route
@ -195,23 +227,25 @@ Flight::map('error', function (Throwable $e) {
$e->getCode(), $e->getCode(),
str_replace(getenv('PWD'), '***CONFIDENTIAL***', $e->getTraceAsString()) str_replace(getenv('PWD'), '***CONFIDENTIAL***', $e->getTraceAsString())
); );
echo "<br><a href='/'>Go back</a>"; echo "<br><a href='/'>Go back</a>";
}); });
Flight::map('notFound', function () { Flight::map('notFound', function () {
echo '<span id="infotext">Route text:</span> The requested URL was not found<br>'; echo '<span id="infotext">Route text:</span> The requested URL was not found<br>';
echo "<a href='/'>Go back</a>"; echo "<a href='/'>Go back</a>";
}); });
Flight::map('start', function () { Flight::map('start', function () {
if (Flight::request()->url === '/dice') { if (Flight::request()->url === '/dice') {
$dice = new \Dice\Dice(); $dice = new Dice();
$dice = $dice->addRules([ $dice = $dice->addRules([
PdoWrapper::class => [ PdoWrapper::class => [
'shared' => true, 'shared' => true,
'constructParams' => ['sqlite::memory:'] 'constructParams' => ['sqlite::memory:']
] ]
]); ]);
Flight::registerContainerHandler(function ($class, $params) use ($dice) { Flight::registerContainerHandler(function ($class, $params) use ($dice) {
return $dice->create($class, $params); return $dice->create($class, $params);
}); });

Loading…
Cancel
Save