* initial commit
This commit is contained in:
61
src/App/ConfigProvider.php
Normal file
61
src/App/ConfigProvider.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
/**
|
||||
* The configuration provider for the App module
|
||||
*
|
||||
* @see https://docs.zendframework.com/zend-component-installer/
|
||||
*/
|
||||
class ConfigProvider
|
||||
{
|
||||
/**
|
||||
* Returns the configuration array
|
||||
*
|
||||
* To add a bit of a structure, each section is defined in a separate
|
||||
* method which returns an array with its configuration.
|
||||
*
|
||||
*/
|
||||
public function __invoke() : array
|
||||
{
|
||||
return [
|
||||
'dependencies' => $this->getDependencies(),
|
||||
'templates' => $this->getTemplates(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the container dependencies
|
||||
*/
|
||||
public function getDependencies() : array
|
||||
{
|
||||
return [
|
||||
'invokables' => [
|
||||
Handler\PingHandler::class => Handler\PingHandler::class,
|
||||
],
|
||||
'factories' => [
|
||||
Handler\HomePageHandler::class => Handler\HomePageHandlerFactory::class,
|
||||
Handler\TeamHandler::class => Handler\TeamHandlerFactory::class,
|
||||
|
||||
Service\TeamService::class => Service\TeamServiceFactory::class,
|
||||
Service\SlideManager::class => Service\SlideManagerFactory::class,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the templates configuration
|
||||
*/
|
||||
public function getTemplates() : array
|
||||
{
|
||||
return [
|
||||
'paths' => [
|
||||
'app' => ['templates/app'],
|
||||
'error' => ['templates/error'],
|
||||
'layout' => ['templates/layout'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
209
src/App/Entity/Slide.php
Normal file
209
src/App/Entity/Slide.php
Normal file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Gedmo\Mapping\Annotation as Gedmo;
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(name="slides")
|
||||
*/
|
||||
class Slide implements JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="title", type="string", length=200, unique=true)
|
||||
* @var string
|
||||
*/
|
||||
private $title;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Team", inversedBy="slides")
|
||||
* @ORM\JoinColumn(name="team_id", referencedColumnName="id")
|
||||
* @var Team
|
||||
*/
|
||||
private $team;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="slide_data", type="text")
|
||||
* @var string
|
||||
*/
|
||||
private $slideData;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="is_visible", type="boolean")
|
||||
* @var bool
|
||||
*/
|
||||
private $isVisible;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="created_at", type="datetime_immutable", nullable=true)
|
||||
* @Gedmo\Timestampable(on="create")
|
||||
* @var \DateTimeImmutable
|
||||
*/
|
||||
private $createdAt;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="updated_at", type="datetime_immutable", nullable=true)
|
||||
* @Gedmo\Timestampable(on="update")
|
||||
* @var \DateTimeImmutable
|
||||
*/
|
||||
private $updatedAt;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return Slide
|
||||
*/
|
||||
public function setId(int $id): Slide
|
||||
{
|
||||
$this->id = $id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $title
|
||||
* @return Slide
|
||||
*/
|
||||
public function setTitle(string $title)
|
||||
{
|
||||
$this->title = $title;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Team
|
||||
*/
|
||||
public function getTeam(): ?Team
|
||||
{
|
||||
return $this->team;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Team $team
|
||||
* @return Slide
|
||||
*/
|
||||
public function setTeam(Team $team): Slide
|
||||
{
|
||||
$this->team = $team;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSlideData(): ?string
|
||||
{
|
||||
return $this->slideData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $slideData
|
||||
* @return Slide
|
||||
*/
|
||||
public function setSlideData(string $slideData): Slide
|
||||
{
|
||||
$this->slideData = $slideData;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isVisible(): ?bool
|
||||
{
|
||||
return $this->isVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $isVisible
|
||||
* @return Slide
|
||||
*/
|
||||
public function setIsVisible(bool $isVisible): Slide
|
||||
{
|
||||
$this->isVisible = $isVisible;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeImmutable
|
||||
*/
|
||||
public function getCreatedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTimeImmutable $createdAt
|
||||
* @return Slide
|
||||
*/
|
||||
public function setCreatedAt(\DateTimeImmutable $createdAt): Slide
|
||||
{
|
||||
$this->createdAt = $createdAt;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeImmutable
|
||||
*/
|
||||
public function getUpdatedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTimeImmutable $updatedAt
|
||||
* @return Slide
|
||||
*/
|
||||
public function setUpdatedAt(\DateTimeImmutable $updatedAt): Slide
|
||||
{
|
||||
$this->updatedAt = $updatedAt;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return [
|
||||
'id' => $this->getId(),
|
||||
'title' => $this->getTitle(),
|
||||
'team' => $this->getTeam(),
|
||||
'slideData' => $this->getSlideData(),
|
||||
'isVisible' => $this->isVisible(),
|
||||
'createdAt' => $this->getCreatedAt()
|
||||
? $this->getCreatedAt()->format("Y-m-d H:i:s")
|
||||
: null,
|
||||
'updatedAt' => $this->getUpdatedAt()
|
||||
? $this->getUpdatedAt()->format("Y-m-d H:i:s")
|
||||
: null,
|
||||
];
|
||||
}
|
||||
}
|
||||
234
src/App/Entity/Team.php
Normal file
234
src/App/Entity/Team.php
Normal file
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Gedmo\Mapping\Annotation as Gedmo;
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(name="teams")
|
||||
*/
|
||||
class Team implements JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="name", type="string", length=200, unique=true)
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="members", type="json")
|
||||
* @var array
|
||||
*/
|
||||
private $members;
|
||||
|
||||
/**
|
||||
* @ORM\OneToMany(
|
||||
* targetEntity="Slide",
|
||||
* mappedBy="team",
|
||||
* cascade={"persist", "remove"},
|
||||
* orphanRemoval=true
|
||||
* )
|
||||
* @var Slide[]|Collection
|
||||
*/
|
||||
private $slides;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="is_active", type="boolean")
|
||||
* @var bool
|
||||
*/
|
||||
private $isActive;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="created_at", type="datetime_immutable", nullable=true)
|
||||
* @Gedmo\Timestampable(on="create")
|
||||
* @var \DateTimeImmutable
|
||||
*/
|
||||
private $createdAt;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="updated_at", type="datetime_immutable", nullable=true)
|
||||
* @Gedmo\Timestampable(on="update")
|
||||
* @var \DateTimeImmutable
|
||||
*/
|
||||
private $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->slides = new ArrayCollection;
|
||||
$this->members = new \ArrayObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return Team
|
||||
*/
|
||||
public function setId(int $id): Team
|
||||
{
|
||||
$this->id = $id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return Team
|
||||
*/
|
||||
public function setName(string $name): Team
|
||||
{
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getMembers()
|
||||
{
|
||||
return $this->members;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $members
|
||||
* @return Team
|
||||
*/
|
||||
public function setMembers(array $members): Team
|
||||
{
|
||||
$this->members = $members;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Slide $slide
|
||||
* @return Team
|
||||
*/
|
||||
public function addSlides(Slide $slide): Team
|
||||
{
|
||||
if (!$this->slides->contains($slide)) {
|
||||
$this->slides->removeElement($slide);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Slide[]|Collection
|
||||
*/
|
||||
public function getSlides(): ?Collection
|
||||
{
|
||||
return $this->slides;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Slide $slide
|
||||
* @return Team
|
||||
*/
|
||||
public function removeSlide(Slide $slide): Team
|
||||
{
|
||||
if ($this->slides->contains($slide)) {
|
||||
$this->slides->removeElement($slide);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isActive(): ?bool
|
||||
{
|
||||
return $this->isActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $isActive
|
||||
* @return Team
|
||||
*/
|
||||
public function setIsActive(bool $isActive): Team
|
||||
{
|
||||
$this->isActive = $isActive;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeImmutable
|
||||
*/
|
||||
public function getCreatedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTimeImmutable $createdAt
|
||||
* @return Team
|
||||
*/
|
||||
public function setCreatedAt(\DateTimeImmutable $createdAt): Team
|
||||
{
|
||||
$this->createdAt = $createdAt;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeImmutable
|
||||
*/
|
||||
public function getUpdatedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTimeImmutable $updatedAt
|
||||
* @return Team
|
||||
*/
|
||||
public function setUpdatedAt(\DateTimeImmutable $updatedAt): Team
|
||||
{
|
||||
$this->updatedAt = $updatedAt;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return [
|
||||
'id' => $this->getId(),
|
||||
'name' => $this->getName(),
|
||||
'members' => $this->getMembers(),
|
||||
'isActive' => $this->isActive(),
|
||||
'createdAt' => $this->getCreatedAt()
|
||||
? $this->getCreatedAt()->format("Y-m-d H:i:s")
|
||||
: null,
|
||||
'updatedAt' => $this->getUpdatedAt()
|
||||
? $this->getUpdatedAt()->format("Y-m-d H:i:s")
|
||||
: null,
|
||||
];
|
||||
}
|
||||
}
|
||||
73
src/App/Form/Slide.php
Normal file
73
src/App/Form/Slide.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use Zend\Form\Annotation;
|
||||
|
||||
/**
|
||||
* @Annotation\Name("slide")
|
||||
* @Annotation\Hydrator("doctrine.hydrator")
|
||||
*/
|
||||
class Slide
|
||||
{
|
||||
/**
|
||||
* @Annotation\Type("Zend\Form\Element\Hidden")
|
||||
* @Annotation\Required(false)
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @Annotation\Type("Zend\Form\Element\Text")
|
||||
* @Annotation\Required(true)
|
||||
* @Annotation\InputFilter("Zend\Filter\StringTrim")
|
||||
* @Annotation\Options({
|
||||
* "label": "Slide title"
|
||||
* })
|
||||
* @var string
|
||||
*/
|
||||
private $title;
|
||||
|
||||
/**
|
||||
* @Annotation\Type("Zend\Form\Element\Text")
|
||||
* @Annotation\Required(true)
|
||||
* @Annotation\Options({
|
||||
* "label": "Team",
|
||||
* "target_class": "App\Entity\Team",
|
||||
* "display_empty_item": false,
|
||||
* "empty_item_label": "",
|
||||
* "is_method": true,
|
||||
* "find_method": {
|
||||
* "name": "findBy",
|
||||
* "params": {
|
||||
* "criteria": {"isActive": true},
|
||||
* "orderBy": {"name": "ASC"}
|
||||
* }
|
||||
* }
|
||||
* })
|
||||
* @var
|
||||
*/
|
||||
private $team;
|
||||
|
||||
/**
|
||||
* @Annotation\Type("Zend\Form\Element\Text")
|
||||
* @Annotation\Required(true)
|
||||
* @Annotation\InputFilter("Zend\Filter\StringTrim")
|
||||
* @Annotation\Options({
|
||||
* "label": "Slide contents"
|
||||
* })
|
||||
* @var
|
||||
*/
|
||||
private $slideData;
|
||||
|
||||
/**
|
||||
* @Annotation\Type("Zend\Form\Element\Checkbox")
|
||||
* @Annotation\Options({
|
||||
* "label": "Visible"
|
||||
* })
|
||||
* @var bool
|
||||
*/
|
||||
private $isVisible;
|
||||
}
|
||||
52
src/App/Form/Team.php
Normal file
52
src/App/Form/Team.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use Zend\Form\Annotation;
|
||||
|
||||
/**
|
||||
* @Annotation\Name("team")
|
||||
* @Annotation\Hydrator("doctrine.hydrator")
|
||||
*/
|
||||
class Team
|
||||
{
|
||||
/**
|
||||
* @Annotation\Type("Zend\Form\Element\Hidden")
|
||||
* @Annotation\Required(false)
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @Annotation\Type("Zend\Form\Element\Text")
|
||||
* @Annotation\Required(true)
|
||||
* @Annotation\InputFilter("Zend\Filter\StringTrim")
|
||||
* @Annotation\Options({
|
||||
* "label": "Team name"
|
||||
* })
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* This is a dummy field, not a text actually. Only used to filter the input
|
||||
* @Annotation\Type("Zend\Form\Element\Text")
|
||||
* @Annotation\Required(true)
|
||||
* @Annotation\Options({
|
||||
* "label": "Members"
|
||||
* })
|
||||
* @var array
|
||||
*/
|
||||
private $members;
|
||||
|
||||
/**
|
||||
* @Annotation\Type("Zend\Form\Element\Checkbox")
|
||||
* @Annotation\Options({
|
||||
* "label": "Active"
|
||||
* })
|
||||
* @var bool
|
||||
*/
|
||||
private $isActive;
|
||||
}
|
||||
161
src/App/Handler/AbstractCrudHandler.php
Normal file
161
src/App/Handler/AbstractCrudHandler.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
use Zend\Json\Json;
|
||||
|
||||
abstract class AbstractCrudHandler implements RequestHandlerInterface
|
||||
{
|
||||
const IDENTIFIER_NAME = 'id';
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$requestMethod = strtoupper($request->getMethod());
|
||||
$id = $request->getAttribute(static::IDENTIFIER_NAME);
|
||||
|
||||
switch ($requestMethod) {
|
||||
case 'GET':
|
||||
return isset($id)
|
||||
? $this->get($request)
|
||||
: $this->getList($request);
|
||||
case 'POST':
|
||||
return $this->create($request);
|
||||
case 'PUT':
|
||||
return $this->update($request);
|
||||
case 'DELETE':
|
||||
return isset($id)
|
||||
? $this->delete($request)
|
||||
: $this->deleteList($request);
|
||||
case 'HEAD':
|
||||
return $this->head($request);
|
||||
case 'OPTIONS':
|
||||
return $this->options($request);
|
||||
case 'PATCH':
|
||||
return $this->patch($request);
|
||||
default:
|
||||
return $this->createResponse(['content' => 'Method not allowed'], 405);
|
||||
}
|
||||
}
|
||||
|
||||
public function get(ServerRequestInterface $request)
|
||||
{
|
||||
return $this->createResponse(['content' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
public function getList(ServerRequestInterface $request)
|
||||
{
|
||||
return $this->createResponse(['content' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
public function create(ServerRequestInterface $request)
|
||||
{
|
||||
return $this->createResponse(['content' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
public function update(ServerRequestInterface $request)
|
||||
{
|
||||
return $this->createResponse(['content' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
public function delete(ServerRequestInterface $request)
|
||||
{
|
||||
return $this->createResponse(['content' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
public function deleteList(ServerRequestInterface $request)
|
||||
{
|
||||
return $this->createResponse(['content' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
public function head(ServerRequestInterface $request)
|
||||
{
|
||||
return $this->createResponse(['content' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
public function options(ServerRequestInterface $request)
|
||||
{
|
||||
return new EmptyResponse(200);
|
||||
}
|
||||
|
||||
public function patch(ServerRequestInterface $request)
|
||||
{
|
||||
return $this->createResponse(['content' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
final protected function createResponse($data, $status = 200)
|
||||
{
|
||||
return new JsonResponse($data, $status);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return array|object
|
||||
*/
|
||||
public function getRequestData(ServerRequestInterface $request)
|
||||
{
|
||||
$body = $request->getParsedBody();
|
||||
|
||||
if (!empty($body)) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
return $this->parseRequestData(
|
||||
$request->getBody()->getContents(),
|
||||
$request->getHeaderLine('content-type')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $input
|
||||
* @param string $contentType
|
||||
* @return mixed
|
||||
*/
|
||||
private function parseRequestData($input, $contentType)
|
||||
{
|
||||
$contentTypeParts = preg_split('/\s*[;,]\s*/', $contentType);
|
||||
$parser = $this->returnParserContentType($contentTypeParts[0]);
|
||||
|
||||
return $parser($input);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string $contentType
|
||||
* @return callable
|
||||
*/
|
||||
private function returnParserContentType(string $contentType): callable
|
||||
{
|
||||
if ($contentType === 'application/x-www-form-urlencoded') {
|
||||
return function ($input) {
|
||||
parse_str($input, $data);
|
||||
return $data;
|
||||
};
|
||||
} elseif ($contentType === 'application/json') {
|
||||
return function ($input) {
|
||||
$jsonDecoder = new Json();
|
||||
try {
|
||||
return $jsonDecoder->decode($input, Json::TYPE_ARRAY);
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
} elseif ($contentType === 'multipart/form-data') {
|
||||
return function ($input) {
|
||||
return $input;
|
||||
};
|
||||
}
|
||||
|
||||
return function ($input) {
|
||||
return $input;
|
||||
};
|
||||
}
|
||||
}
|
||||
94
src/App/Handler/HomePageHandler.php
Normal file
94
src/App/Handler/HomePageHandler.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Zend\Diactoros\Response\HtmlResponse;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
use Zend\Expressive\Plates\PlatesRenderer;
|
||||
use Zend\Expressive\Router;
|
||||
use Zend\Expressive\Template;
|
||||
use Zend\Expressive\Twig\TwigRenderer;
|
||||
use Zend\Expressive\ZendView\ZendViewRenderer;
|
||||
|
||||
class HomePageHandler implements RequestHandlerInterface
|
||||
{
|
||||
private $containerName;
|
||||
|
||||
private $router;
|
||||
|
||||
private $template;
|
||||
|
||||
public function __construct(
|
||||
Router\RouterInterface $router,
|
||||
Template\TemplateRendererInterface $template = null,
|
||||
string $containerName
|
||||
) {
|
||||
$this->router = $router;
|
||||
$this->template = $template;
|
||||
$this->containerName = $containerName;
|
||||
}
|
||||
|
||||
public function handle(ServerRequestInterface $request) : ResponseInterface
|
||||
{
|
||||
if (! $this->template) {
|
||||
return new JsonResponse([
|
||||
'welcome' => 'Congratulations! You have installed the zend-expressive skeleton application.',
|
||||
'docsUrl' => 'https://docs.zendframework.com/zend-expressive/',
|
||||
]);
|
||||
}
|
||||
|
||||
$data = [];
|
||||
|
||||
switch ($this->containerName) {
|
||||
case 'Aura\Di\Container':
|
||||
$data['containerName'] = 'Aura.Di';
|
||||
$data['containerDocs'] = 'http://auraphp.com/packages/2.x/Di.html';
|
||||
break;
|
||||
case 'Pimple\Container':
|
||||
$data['containerName'] = 'Pimple';
|
||||
$data['containerDocs'] = 'https://pimple.symfony.com/';
|
||||
break;
|
||||
case 'Zend\ServiceManager\ServiceManager':
|
||||
$data['containerName'] = 'Zend Servicemanager';
|
||||
$data['containerDocs'] = 'https://docs.zendframework.com/zend-servicemanager/';
|
||||
break;
|
||||
case 'Auryn\Injector':
|
||||
$data['containerName'] = 'Auryn';
|
||||
$data['containerDocs'] = 'https://github.com/rdlowrey/Auryn';
|
||||
break;
|
||||
case 'Symfony\Component\DependencyInjection\ContainerBuilder':
|
||||
$data['containerName'] = 'Symfony DI Container';
|
||||
$data['containerDocs'] = 'https://symfony.com/doc/current/service_container.html';
|
||||
break;
|
||||
}
|
||||
|
||||
if ($this->router instanceof Router\AuraRouter) {
|
||||
$data['routerName'] = 'Aura.Router';
|
||||
$data['routerDocs'] = 'http://auraphp.com/packages/2.x/Router.html';
|
||||
} elseif ($this->router instanceof Router\FastRouteRouter) {
|
||||
$data['routerName'] = 'FastRoute';
|
||||
$data['routerDocs'] = 'https://github.com/nikic/FastRoute';
|
||||
} elseif ($this->router instanceof Router\ZendRouter) {
|
||||
$data['routerName'] = 'Zend Router';
|
||||
$data['routerDocs'] = 'https://docs.zendframework.com/zend-router/';
|
||||
}
|
||||
|
||||
if ($this->template instanceof PlatesRenderer) {
|
||||
$data['templateName'] = 'Plates';
|
||||
$data['templateDocs'] = 'http://platesphp.com/';
|
||||
} elseif ($this->template instanceof TwigRenderer) {
|
||||
$data['templateName'] = 'Twig';
|
||||
$data['templateDocs'] = 'http://twig.sensiolabs.org/documentation';
|
||||
} elseif ($this->template instanceof ZendViewRenderer) {
|
||||
$data['templateName'] = 'Zend View';
|
||||
$data['templateDocs'] = 'https://docs.zendframework.com/zend-view/';
|
||||
}
|
||||
|
||||
return new HtmlResponse($this->template->render('app::home-page', $data));
|
||||
}
|
||||
}
|
||||
23
src/App/Handler/HomePageHandlerFactory.php
Normal file
23
src/App/Handler/HomePageHandlerFactory.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Zend\Expressive\Router\RouterInterface;
|
||||
use Zend\Expressive\Template\TemplateRendererInterface;
|
||||
|
||||
class HomePageHandlerFactory
|
||||
{
|
||||
public function __invoke(ContainerInterface $container) : RequestHandlerInterface
|
||||
{
|
||||
$router = $container->get(RouterInterface::class);
|
||||
$template = $container->has(TemplateRendererInterface::class)
|
||||
? $container->get(TemplateRendererInterface::class)
|
||||
: null;
|
||||
|
||||
return new HomePageHandler($router, $template, get_class($container));
|
||||
}
|
||||
}
|
||||
18
src/App/Handler/PingHandler.php
Normal file
18
src/App/Handler/PingHandler.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
class PingHandler implements RequestHandlerInterface
|
||||
{
|
||||
public function handle(ServerRequestInterface $request) : ResponseInterface
|
||||
{
|
||||
return new JsonResponse(['ack' => time()]);
|
||||
}
|
||||
}
|
||||
73
src/App/Handler/SlideHandler.php
Normal file
73
src/App/Handler/SlideHandler.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler;
|
||||
|
||||
use App\Service\SlideManager;
|
||||
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
class SlideHandler extends AbstractCrudHandler
|
||||
{
|
||||
/** @var SlideManager */
|
||||
private $slideManager;
|
||||
|
||||
public function __construct(SlideManager $teamService) {
|
||||
$this->slideManager = $teamService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getList(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
return new JsonResponse($this->slideManager->listSlides());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function get(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$id = $request->getAttribute('id');
|
||||
return new JsonResponse($this->slideManager->getSlide((int)$id));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @return JsonResponse
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function create(ServerRequestInterface $request)
|
||||
{
|
||||
$data = $this->getRequestData($request);
|
||||
try {
|
||||
return new JsonResponse($this->slideManager->addSlide($data));
|
||||
} catch (UniqueConstraintViolationException $e) {
|
||||
return new JsonResponse([
|
||||
'message' => 'The field `name` must be unique',
|
||||
], 500);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return new JsonResponse([
|
||||
'message' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @return JsonResponse
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
*/
|
||||
public function delete(ServerRequestInterface $request)
|
||||
{
|
||||
$id = $request->getAttribute('id');
|
||||
return new JsonResponse($this->slideManager->removeSlide($id));
|
||||
}
|
||||
}
|
||||
22
src/App/Handler/SlideHandlerFactory.php
Normal file
22
src/App/Handler/SlideHandlerFactory.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler;
|
||||
|
||||
use App\Service\SlideManager;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
class SlideHandlerFactory
|
||||
{
|
||||
/**
|
||||
* @param ContainerInterface $container
|
||||
* @return RequestHandlerInterface
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container) : RequestHandlerInterface
|
||||
{
|
||||
$slideManager = $container->get(SlideManager::class);
|
||||
return new SlideHandler($slideManager);
|
||||
}
|
||||
}
|
||||
73
src/App/Handler/TeamHandler.php
Normal file
73
src/App/Handler/TeamHandler.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler;
|
||||
|
||||
use App\Service\TeamService;
|
||||
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
class TeamHandler extends AbstractCrudHandler
|
||||
{
|
||||
/** @var TeamService */
|
||||
private $teamService;
|
||||
|
||||
public function __construct(TeamService $teamService) {
|
||||
$this->teamService = $teamService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getList(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
return new JsonResponse($this->teamService->listTeams());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function get(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$id = $request->getAttribute('id');
|
||||
return new JsonResponse($this->teamService->getTeam((int)$id));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @return JsonResponse
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function create(ServerRequestInterface $request)
|
||||
{
|
||||
$data = $this->getRequestData($request);
|
||||
try {
|
||||
return new JsonResponse($this->teamService->addTeam($data));
|
||||
} catch (UniqueConstraintViolationException $e) {
|
||||
return new JsonResponse([
|
||||
'message' => 'The field `name` must be unique',
|
||||
], 500);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return new JsonResponse([
|
||||
'message' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @return JsonResponse
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
*/
|
||||
public function delete(ServerRequestInterface $request)
|
||||
{
|
||||
$id = $request->getAttribute('id');
|
||||
return new JsonResponse($this->teamService->removeTeam($id));
|
||||
}
|
||||
}
|
||||
22
src/App/Handler/TeamHandlerFactory.php
Normal file
22
src/App/Handler/TeamHandlerFactory.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Handler;
|
||||
|
||||
use App\Service\TeamService;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
class TeamHandlerFactory
|
||||
{
|
||||
/**
|
||||
* @param ContainerInterface $container
|
||||
* @return RequestHandlerInterface
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container) : RequestHandlerInterface
|
||||
{
|
||||
$teamService = $container->get(TeamService::class);
|
||||
return new TeamHandler($teamService);
|
||||
}
|
||||
}
|
||||
124
src/App/Service/SlideManager.php
Normal file
124
src/App/Service/SlideManager.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\Slide;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Zend\Form\Form;
|
||||
|
||||
class SlideManager
|
||||
{
|
||||
const ENTITY_NAME = Slide::class;
|
||||
|
||||
/** @var EntityManager */
|
||||
private $em;
|
||||
|
||||
/** @var Form */
|
||||
private $form;
|
||||
|
||||
public function __construct(EntityManager $em, Form $form)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->form = $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function listSlides(): array
|
||||
{
|
||||
return $this->em->getRepository(self::ENTITY_NAME)->findBy([
|
||||
'isActive' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return Slide
|
||||
*/
|
||||
public function getSlide(int $id): Slide
|
||||
{
|
||||
/** @var Slide $entity */
|
||||
$entity = $this->em->getRepository(self::ENTITY_NAME)->find($id);
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return Slide
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
* @throws \Doctrine\DBAL\Exception\UniqueConstraintViolationException
|
||||
*/
|
||||
public function addSlide(array $data)
|
||||
{
|
||||
$entity = new Slide();
|
||||
$this->form
|
||||
->bind($entity)
|
||||
->setData($data);
|
||||
|
||||
if ($this->form->isValid()) {
|
||||
$this->em->persist($entity);
|
||||
$this->em->flush();
|
||||
return $entity;
|
||||
} else {
|
||||
$messages = $this->form->getMessages();
|
||||
$fields = array_keys($messages);
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
"The following fields are invalid: (%s)",
|
||||
implode(", ", $fields)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @param array $data
|
||||
* @return bool
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function changeSlide(int $id, array $data): bool
|
||||
{
|
||||
if (null === ($entity = $this->getSlide($id))) {
|
||||
return false;
|
||||
}
|
||||
$this->form
|
||||
->bind($entity)
|
||||
->setData($data);
|
||||
|
||||
if ($this->form->isValid()) {
|
||||
$this->em->flush();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return bool
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
*/
|
||||
public function removeSlide(int $id): bool
|
||||
{
|
||||
if (null !== ($entity = $this->getSlide($id))) {
|
||||
$this->em->remove($entity);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @param bool $isVisible
|
||||
* @return bool
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function setSlideVisible(int $id, bool $isVisible): bool
|
||||
{
|
||||
return $this->changeSlide($id, ['isVisible' => $isVisible]);
|
||||
}
|
||||
}
|
||||
23
src/App/Service/SlideManagerFactory.php
Normal file
23
src/App/Service/SlideManagerFactory.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Form\Team;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Zend\Form\Annotation\AnnotationBuilder;
|
||||
use Zend\Form\Form;
|
||||
|
||||
class SlideManagerFactory
|
||||
{
|
||||
public function __invoke(ContainerInterface $container) : SlideManager
|
||||
{
|
||||
$em = $container->get(EntityManager::class);
|
||||
$formBuilder = $container->get(AnnotationBuilder::class);
|
||||
/** @var Form $form */
|
||||
$form = $formBuilder->createForm(Team::class);
|
||||
return new SlideManager($em, $form);
|
||||
}
|
||||
}
|
||||
124
src/App/Service/TeamService.php
Normal file
124
src/App/Service/TeamService.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\Team;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Zend\Form\Form;
|
||||
|
||||
class TeamService
|
||||
{
|
||||
const ENTITY_NAME = Team::class;
|
||||
|
||||
/** @var EntityManager */
|
||||
private $em;
|
||||
|
||||
/** @var Form */
|
||||
private $form;
|
||||
|
||||
public function __construct(EntityManager $em, Form $form)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->form = $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function listTeams(): array
|
||||
{
|
||||
return $this->em->getRepository(self::ENTITY_NAME)->findBy([
|
||||
'isActive' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return Team
|
||||
*/
|
||||
public function getTeam(int $id): ?Team
|
||||
{
|
||||
/** @var Team $entity */
|
||||
$entity = $this->em->getRepository(self::ENTITY_NAME)->find($id);
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return Team
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
* @throws \Doctrine\DBAL\Exception\UniqueConstraintViolationException
|
||||
*/
|
||||
public function addTeam(array $data): Team
|
||||
{
|
||||
$entity = new Team();
|
||||
$this->form
|
||||
->bind($entity)
|
||||
->setData($data);
|
||||
|
||||
if ($this->form->isValid()) {
|
||||
$this->em->persist($entity);
|
||||
$this->em->flush();
|
||||
return $entity;
|
||||
} else {
|
||||
$messages = $this->form->getMessages();
|
||||
$fields = array_keys($messages);
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
"The following fields are invalid: (%s)",
|
||||
implode(", ", $fields)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @param array $data
|
||||
* @return bool
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function changeTeam(int $id, array $data): bool
|
||||
{
|
||||
if (null === ($entity = $this->getTeam($id))) {
|
||||
return false;
|
||||
}
|
||||
$this->form
|
||||
->bind($entity)
|
||||
->setData($data);
|
||||
|
||||
if ($this->form->isValid()) {
|
||||
$this->em->flush();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return bool
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
*/
|
||||
public function removeTeam(int $id): bool
|
||||
{
|
||||
if (null !== ($entity = $this->getTeam($id))) {
|
||||
$this->em->remove($entity);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @param bool $isVisible
|
||||
* @return bool
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function setTeamActive(int $id, bool $isVisible): bool
|
||||
{
|
||||
return $this->changeTeam($id, ['isVisible' => $isVisible]);
|
||||
}
|
||||
}
|
||||
23
src/App/Service/TeamServiceFactory.php
Normal file
23
src/App/Service/TeamServiceFactory.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Form\Team;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Zend\Form\Annotation\AnnotationBuilder;
|
||||
use Zend\Form\Form;
|
||||
|
||||
class TeamServiceFactory
|
||||
{
|
||||
public function __invoke(ContainerInterface $container) : TeamService
|
||||
{
|
||||
$em = $container->get(EntityManager::class);
|
||||
$formBuilder = $container->get(AnnotationBuilder::class);
|
||||
/** @var Form $form */
|
||||
$form = $formBuilder->createForm(Team::class);
|
||||
return new TeamService($em, $form);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user