PHP code refresh (!8)

Reviewed-on: #8
This commit is contained in:
Alexey Skobkin 2022-07-10 16:51:26 +03:00
parent e88ca59e06
commit 8e5e3cceb9
54 changed files with 309 additions and 541 deletions

View File

@ -30,7 +30,7 @@ doctrine:
mappings: mappings:
App: App:
is_bundle: false is_bundle: false
type: annotation type: attribute
dir: '%kernel.project_dir%/src/Entity' dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity' prefix: 'App\Entity'
alias: App alias: App
@ -39,7 +39,7 @@ doctrine:
mappings: mappings:
Magnetico: Magnetico:
is_bundle: false is_bundle: false
type: annotation type: attribute
dir: '%kernel.project_dir%/src/Magnetico/Entity' dir: '%kernel.project_dir%/src/Magnetico/Entity'
prefix: 'App\Magnetico' prefix: 'App\Magnetico'
alias: Magnetico alias: Magnetico

View File

@ -6,10 +6,9 @@ security:
class: App\Entity\User class: App\Entity\User
property: username property: username
manager_name: default manager_name: default
encoders: password_hashers:
App\Entity\User: App\Entity\User:
# https://symfony.com/blog/new-in-symfony-4-3-native-password-encoder algorithm: sodium
algorithm: 'auto'
firewalls: firewalls:
dev: dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/ pattern: ^/(_(profiler|wdt)|css|images|js)/

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Api\V1\Controller; namespace App\Api\V1\Controller;

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Api\V1\Controller; namespace App\Api\V1\Controller;

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Api\V1\Controller; namespace App\Api\V1\Controller;

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Api\V1\Controller; namespace App\Api\V1\Controller;

View File

@ -1,9 +1,10 @@
<?php <?php
declare(strict_types=1);
namespace App\Api\V1\DTO; namespace App\Api\V1\DTO;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Annotation\{Groups, MaxDepth}; use Symfony\Component\Serializer\Annotation\Groups;
class ApiResponse class ApiResponse
{ {
@ -12,33 +13,20 @@ class ApiResponse
public const STATUS_FAIL = 'fail'; public const STATUS_FAIL = 'fail';
public const STATUS_UNKNOWN = 'unknown'; public const STATUS_UNKNOWN = 'unknown';
/** #[Groups(['api'])]
* @var int HTTP response status code private int $code;
*
* @Groups({"api"})
*/
private $code;
/** /** Status text: 'success' (1xx-3xx), 'error' (4xx), 'fail' (5xx) or 'unknown' */
* @var string Status text: 'success' (1xx-3xx), 'error' (4xx), 'fail' (5xx) or 'unknown' #[Groups(['api'])]
* private string $status;
* @Groups({"api"})
*/
private $status;
/** /** Used for 'fail' and 'error') */
* @var string|null Used for 'fail' and 'error' #[Groups(['api'])]
* private ?string $message;
* @Groups({"api"})
*/
private $message;
/** /** @Response body. In case of 'error' or 'fail' contains cause or exception name. */
* @var string|\object|array|null Response body. In case of 'error' or 'fail' contains cause or exception name. #[Groups(['api'])]
* private string|object|array|null $data;
* @Groups({"api"})
*/
private $data;
public function __construct($data = null, int $code = Response::HTTP_OK, string $message = null, string $status = '') public function __construct($data = null, int $code = Response::HTTP_OK, string $message = null, string $status = '')
{ {

View File

@ -1,46 +1,27 @@
<?php <?php
declare(strict_types=1);
namespace App\Api\V1\DTO; namespace App\Api\V1\DTO;
use Pagerfanta\Pagerfanta; use Pagerfanta\Pagerfanta;
use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Serializer\Annotation\Groups;
class ListPage class ListPage
{ {
/** #[Groups(['api'])]
* @var int private int $numberOfPages;
*
* @Serializer\Groups({"api"})
*/
private $numberOfPages;
/** #[Groups(['api'])]
* @var int private int $currentPage;
*
* @Serializer\Groups({"api"})
*/
private $currentPage;
/** #[Groups(['api'])]
* @var int private int $numberOfResults;
*
* @Serializer\Groups({"api"})
*/
private $numberOfResults;
/** #[Groups(['api'])]
* @var int private int $maxPerPage;
*
* @Serializer\Groups({"api"})
*/
private $maxPerPage;
/** #[Groups(['api'])]
* @var \Traversable protected \Traversable $items;
*
* @Serializer\Groups({"api"})
*/
protected $items;
public function __construct(\Traversable $items, int $numberOfResults, int $numberOfPages, int $currentPage, int $maxPerPage) public function __construct(\Traversable $items, int $numberOfResults, int $numberOfPages, int $currentPage, int $maxPerPage)
{ {

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Command; namespace App\Command;
@ -11,22 +12,12 @@ use Symfony\Component\Console\Output\OutputInterface;
class AddInvitesCommand extends Command class AddInvitesCommand extends Command
{ {
/** @var EntityManagerInterface */ public function __construct(
private $em; private readonly EntityManagerInterface $em,
private readonly UserRepository $userRepo,
/** @var UserRepository */ private readonly InviteManager $inviteManager
private $userRepo; ) {
/** @var InviteManager */
private $inviteManager;
public function __construct(EntityManagerInterface $em, UserRepository $userRepo, InviteManager $inviteManager)
{
parent::__construct(); parent::__construct();
$this->em = $em;
$this->userRepo = $userRepo;
$this->inviteManager = $inviteManager;
} }
protected function configure() protected function configure()
@ -38,15 +29,15 @@ class AddInvitesCommand extends Command
; ;
} }
protected function execute(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output): int
{ {
$username = $input->getArgument('username'); $username = $input->getArgument('username');
$number = $input->getArgument('number'); $number = (int) $input->getArgument('number');
if (null === $user = $this->userRepo->findOneBy(['username' => $username])) { if (null === $user = $this->userRepo->findOneBy(['username' => $username])) {
$output->writeln('<error>User not found.</error>'); $output->writeln('<error>User not found.</error>');
return 1; return Command::FAILURE;
} }
$this->inviteManager->createInvitesForUser($user, $number); $this->inviteManager->createInvitesForUser($user, $number);
@ -55,6 +46,6 @@ class AddInvitesCommand extends Command
$output->writeln(sprintf('<info>%d invites added to \'%s\'.</info>', $number, $user->getUsername())); $output->writeln(sprintf('<info>%d invites added to \'%s\'.</info>', $number, $user->getUsername()));
return 0; return Command::SUCCESS;
} }
} }

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Command; namespace App\Command;
@ -13,26 +14,13 @@ use Symfony\Component\Console\Question\Question;
class AddUserCommand extends Command class AddUserCommand extends Command
{ {
/** @var EntityManagerInterface */ public function __construct(
private $em; private readonly EntityManagerInterface $em,
private readonly UserManager $userManager,
/** @var UserManager */ private readonly UserRepository $userRepo,
private $userManager; private readonly InviteManager $inviteManager,
) {
/** @var UserRepository */
private $userRepo;
/** @var InviteManager */
private $inviteManager;
public function __construct(EntityManagerInterface $em, UserManager $userManager, UserRepository $userRepo, InviteManager $inviteManager)
{
parent::__construct(); parent::__construct();
$this->em = $em;
$this->userManager = $userManager;
$this->userRepo = $userRepo;
$this->inviteManager = $inviteManager;
} }
protected function configure() protected function configure()
@ -47,7 +35,7 @@ class AddUserCommand extends Command
; ;
} }
protected function execute(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output): int
{ {
$username = $input->getArgument('username'); $username = $input->getArgument('username');
$email = $input->getArgument('email'); $email = $input->getArgument('email');
@ -68,7 +56,7 @@ class AddUserCommand extends Command
if (!$password) { if (!$password) {
$output->writeln('<error>User password cannot be empty.</error>'); $output->writeln('<error>User password cannot be empty.</error>');
return 1; return Command::FAILURE;
} }
if ($roles) { if ($roles) {
@ -88,7 +76,7 @@ class AddUserCommand extends Command
$output->writeln(sprintf('<info>User \'%s\' registered, %d invites added.</info>', $user->getUsername(), $invites)); $output->writeln(sprintf('<info>User \'%s\' registered, %d invites added.</info>', $user->getUsername(), $invites));
return 0; return Command::SUCCESS;
} }
} }

View File

@ -1,11 +1,10 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller; namespace App\Controller;
use App\Entity\ApiToken; use App\Entity\{ApiToken, User};
use App\Entity\User; use App\Repository\{ApiTokenRepository, InviteRepository};
use App\Repository\ApiTokenRepository;
use App\Repository\InviteRepository;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller; namespace App\Controller;

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller; namespace App\Controller;

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller; namespace App\Controller;

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller; namespace App\Controller;

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Controller; namespace App\Controller;

View File

@ -1,5 +1,4 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace App\Doctrine\ORM\AST; namespace App\Doctrine\ORM\AST;
@ -14,14 +13,13 @@ use Doctrine\ORM\Query\{AST\Functions\FunctionNode, AST\Node, Lexer, Parser, Sql
*/ */
abstract class BaseFunction extends FunctionNode abstract class BaseFunction extends FunctionNode
{ {
/** @var string */ protected string $functionPrototype;
protected $functionPrototype;
/** @var string[] */ /** @var string[] */
protected $nodesMapping = []; protected array $nodesMapping = [];
/** @var Node[] */ /** @var Node[] */
protected $nodes = []; protected array $nodes = [];
abstract protected function customiseFunction(): void; abstract protected function customiseFunction(): void;
@ -70,4 +68,4 @@ abstract class BaseFunction extends FunctionNode
return \vsprintf($this->functionPrototype, $dispatched); return \vsprintf($this->functionPrototype, $dispatched);
} }
} }

View File

@ -1,5 +1,4 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace App\Doctrine\ORM\AST; namespace App\Doctrine\ORM\AST;

View File

@ -1,40 +1,27 @@
<?php <?php
declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use App\Repository\ApiTokenRepository;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Serializer\Annotation as Serializer;
/** #[ORM\Table(name: 'api_tokens', schema: 'users')]
* @ORM\Table(name="api_tokens", schema="users") #[ORM\Entity(repositoryClass: ApiTokenRepository::class, readOnly: true)]
* @ORM\Entity(repositoryClass="App\Repository\ApiTokenRepository", readOnly=true)
*/
class ApiToken class ApiToken
{ {
/** #[ORM\ManyToOne(targetEntity: User::class)]
* @var User #[ORM\JoinColumn(name: 'user_id', nullable: false)]
* private User $user;
* @ORM\ManyToOne(targetEntity="App\Entity\User")
* @ORM\JoinColumn(name="user_id", nullable=false)
*/
private $user;
/** #[Serializer\Groups(['api', 'api_v1_login'])]
* @var string #[ORM\Id]
* #[ORM\Column(name: 'key', type: 'string', length: 32)]
* @Serializer\Groups({"api", "api_v1_login"}) private string $key;
*
* @ORM\Id()
* @ORM\Column(name="key", type="string", length=32)
*/
private $key;
/** #[ORM\Column(name: 'created_at', type: 'datetime')]
* @var \DateTime private \DateTime $createdAt;
*
* @ORM\Column(name="created_at", type="datetime")
*/
private $createdAt;
public function __construct(User $user) public function __construct(User $user)
{ {
@ -57,4 +44,4 @@ class ApiToken
{ {
return $this->createdAt; return $this->createdAt;
} }
} }

View File

@ -1,51 +1,35 @@
<?php <?php
declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use App\Repository\InviteRepository;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
/** #[ORM\Table(name: 'invites', schema: 'users')]
* @ORM\Table(name="invites", schema="users") #[ORM\Entity(repositoryClass: InviteRepository::class)]
* @ORM\Entity(repositoryClass="App\Repository\InviteRepository")
*/
class Invite class Invite
{ {
/** #[ORM\Id]
* @var int #[ORM\GeneratedValue(strategy: 'AUTO')]
* #[ORM\Column(name: 'id', type: 'integer')]
* @ORM\Id() private int $id;
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(name="id", type="integer")
*/
private $id;
/** #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'invites')]
* @var User #[ORM\JoinColumn(name: 'user_id', nullable: false)]
* private User $user;
* @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="invites")
* @ORM\JoinColumn(name="user_id", nullable=false)
*/
private $user;
/** #[ORM\Column(name: 'code', type: 'string', length: 32, unique: true)]
* @var string private string $code;
*
* @ORM\Column(name="code", type="string", length=32, unique=true)
*/
private $code;
/** #[ORM\ManyToOne(targetEntity: User::class)]
* @var User|null #[ORM\JoinColumn(name: 'used_by_id', nullable: true)]
* private ?User $usedBy;
* @ORM\ManyToOne(targetEntity="App\Entity\User")
* @ORM\JoinColumn(name="used_by_id", nullable=true)
*/
private $usedBy;
public function __construct(User $forUser) public function __construct(User $forUser)
{ {
$this->user = $forUser; $this->user = $forUser;
$this->code = md5(random_bytes(100)); $this->code = md5(\random_bytes(100));
} }
public function getId(): int public function getId(): int
@ -81,4 +65,4 @@ class Invite
$this->usedBy = $user; $this->usedBy = $user;
} }
} }

View File

@ -1,38 +1,24 @@
<?php <?php
declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
/** #[ORM\Table(schema: 'users', name: 'password_reset_tokens')]
* @ORM\Table(schema="users", name="password_reset_tokens") #[ORM\Entity(readOnly: true)]
* @ORM\Entity(readOnly=true)
*/
class PasswordResetToken class PasswordResetToken
{ {
/** #[ORM\ManyToOne(targetEntity: User::class, fetch: 'EAGER')]
* @var User #[ORM\JoinColumn(name: 'user_id', nullable: false, onDelete: 'CASCADE')]
* private User $user;
* @ORM\ManyToOne(targetEntity="User", fetch="EAGER")
* @ORM\JoinColumn(name="user_id", nullable=false, onDelete="CASCADE")
*/
private $user;
/** #[ORM\Id]
* @var string #[ORM\Column(name: 'code', type: 'text', nullable: false)]
* private string $code;
* @ORM\Id()
* @ORM\Column(name="code", type="text", nullable=false)
*/
private $code;
/** #[ORM\Column(name: 'valid_until', type: 'datetime', nullable: false)]
* @var \DateTime private \DateTime $validUntil;
*
* @ORM\Column(name="valid_until", type="datetime", nullable=false)
*/
private $validUntil;
public function __construct(User $user, \DateInterval $validFor = null) public function __construct(User $user, \DateInterval $validFor = null)
{ {

View File

@ -1,73 +1,46 @@
<?php <?php
declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; use Symfony\Component\PasswordHasher\PasswordHasherInterface;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\{PasswordAuthenticatedUserInterface, UserInterface};
/** #[ORM\Table(name: 'users', schema: 'users')]
* @ORM\Table(name="users", schema="users") #[ORM\Entity(repositoryClass: UserRepository::class)]
* @ORM\Entity(repositoryClass="App\Repository\UserRepository") class User implements UserInterface, PasswordAuthenticatedUserInterface
*/
class User implements UserInterface, \Serializable
{ {
/** #[ORM\Id]
* @var int #[ORM\Column(name: 'id', type: 'integer')]
* #[ORM\GeneratedValue(strategy: 'AUTO')]
* @ORM\Id private int $id;
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/** #[ORM\Column(name: 'username', type: 'string', length: 25, unique: true)]
* @var string private string $username;
*
* @ORM\Column(name="username", type="string", length=25, unique=true)
*/
private $username;
/** #[ORM\Column(name: 'password', type: 'text')]
* @var string private string $password;
*
* @ORM\Column(name="password", type="text")
*/
private $password;
/** #[ORM\Column(name: 'email', type: 'string', length: 254, unique: true)]
* @var string private string $email;
*
* @ORM\Column(name="email", type="string", length=254, unique=true)
*/
private $email;
/** #[ORM\Column(name: 'roles', type: 'json')]
* @var string[] private array $roles = [];
*
* @ORM\Column(name="roles", type="json")
*/
private $roles = [];
/** #[ORM\Column(name: 'created_at', type: 'datetime')]
* @var \DateTime private \DateTime $createdAt;
*
* @ORM\Column(name="created_at", type="datetime")
*/
private $createdAt;
/** /** @var Invite[]|ArrayCollection */
* @var Invite[]|ArrayCollection #[ORM\OneToMany(targetEntity: Invite::class, mappedBy: 'user', fetch: 'EXTRA_LAZY')]
*
* @ORM\OneToMany(targetEntity="App\Entity\Invite", mappedBy="user", fetch="EXTRA_LAZY")
*/
private $invites; private $invites;
public function __construct(string $username, PasswordEncoderInterface $encoder, string $rawPassword, string $email, array $roles = []) public function __construct(string $username, PasswordHasherInterface $hasher, string $rawPassword, string $email, array $roles = [])
{ {
$this->username = $username; $this->username = $username;
$this->password = $encoder->encodePassword($rawPassword, null); $this->password = $hasher->hash($rawPassword);
$this->email = $email; $this->email = $email;
$this->roles = $roles ?: ['ROLE_USER']; $this->roles = $roles ?: ['ROLE_USER'];
$this->createdAt = new \DateTime(); $this->createdAt = new \DateTime();
@ -78,22 +51,29 @@ class User implements UserInterface, \Serializable
return $this->id; return $this->id;
} }
public function getUsername() public function getUserIdentifier(): string
{ {
return $this->username; return $this->username;
} }
public function getPassword() /** @deprecated since Symfony 5.3, use getUserIdentifier() instead */
public function getUsername(): string
{
return $this->username;
}
public function getPassword(): string
{ {
return $this->password; return $this->password;
} }
public function changePassword(PasswordEncoderInterface $encoder, string $rawPassword): void public function changePassword(PasswordHasherInterface $hasher, string $rawPassword): void
{ {
$this->password = $encoder->encodePassword($rawPassword, null); $this->password = $hasher->hash($rawPassword);
} }
public function getSalt() /** @deprecated since Symfony 5.3 */
public function getSalt(): ?string
{ {
// Salt is not needed when using Argon2i // Salt is not needed when using Argon2i
// @see https://symfony.com/doc/current/reference/configuration/security.html#using-the-argon2i-password-encoder // @see https://symfony.com/doc/current/reference/configuration/security.html#using-the-argon2i-password-encoder
@ -132,7 +112,7 @@ class User implements UserInterface, \Serializable
} }
/** @see \Serializable::serialize() */ /** @see \Serializable::serialize() */
public function serialize() public function serialize(): string
{ {
return serialize([ return serialize([
$this->id, $this->id,
@ -142,7 +122,7 @@ class User implements UserInterface, \Serializable
} }
/** @see \Serializable::unserialize() */ /** @see \Serializable::unserialize() */
public function unserialize($serialized) public function unserialize($serialized): void
{ {
[ [
$this->id, $this->id,

View File

@ -1,7 +1,6 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace App\Feed; namespace App\Feed;
use App\Magnet\MagnetGenerator; use App\Magnet\MagnetGenerator;
@ -21,15 +20,12 @@ class RssGenerator
private const PER_PAGE = 1000; private const PER_PAGE = 1000;
private const MIME_TYPE = 'application/x-bittorrent'; private const MIME_TYPE = 'application/x-bittorrent';
private TorrentRepository $repo; public function __construct(
private RouterInterface $router; private readonly TorrentRepository $repo,
private MagnetGenerator $magnetGenerator; private readonly RouterInterface $router,
private readonly MagnetGenerator $magnetGenerator
) {
public function __construct(TorrentRepository $repo, RouterInterface $router, MagnetGenerator $magnetGenerator)
{
$this->repo = $repo;
$this->router = $router;
$this->magnetGenerator = $magnetGenerator;
} }
public function generateLast(int $page): string public function generateLast(int $page): string
@ -43,8 +39,6 @@ class RssGenerator
/** /**
* @param Torrent[] $torrents * @param Torrent[] $torrents
*
* @return Feed
*/ */
private function createFeedFromTorrents(array $torrents): Feed private function createFeedFromTorrents(array $torrents): Feed
{ {

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Form\Data; namespace App\Form\Data;
@ -6,12 +7,8 @@ use Symfony\Component\Validator\Constraints as Assert;
class PasswordResetData class PasswordResetData
{ {
/** #[Assert\NotBlank]
* @var string #[Assert\Length(min: 8, max: 4096)]
* #[Assert\NotCompromisedPassword(skipOnError: true)]
* @Assert\NotBlank public string $password;
* @Assert\Length(min="8", max="4096")
* @Assert\NotCompromisedPassword(skipOnError=true)
*/
public $password;
} }

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Form\Data; namespace App\Form\Data;
@ -7,18 +8,10 @@ use Symfony\Component\Validator\Constraints as Assert;
class PasswordResetRequestData class PasswordResetRequestData
{ {
/** #[Assert\Email]
* @var string #[Assert\NotBlank]
* public string $email;
* @Assert\Email()
* @Assert\NotBlank()
*/
public $email;
/** #[ReCaptcha\IsTrueV3]
* @var string public string $recaptcha;
*
* @ReCaptcha\IsTrueV3()
*/
public $recaptcha;
} }

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Form\Data; namespace App\Form\Data;
@ -10,37 +11,21 @@ use App\Validator\Constraints as AppAssert;
*/ */
class RegisterData class RegisterData
{ {
/** #[Assert\NotBlank]
* @var string #[Assert\Length(min: 2, max: 25)]
* public string $username;
* @Assert\NotBlank()
* @Assert\Length(min="2", max="25")
*/
public $username;
/** #[Assert\NotBlank]
* @var string #[Assert\Length(min: 8, max: 4096)]
* public string $password;
* @Assert\NotBlank()
* @Assert\Length(min="8", max="4096")
*/
public $password;
/** #[Assert\Email]
* @var string public string $email;
*
* @Assert\Email()
*/
public $email;
/** #[Assert\NotBlank]
* @var string #[Assert\Length(min: 32, max: 32)]
* #[AppAssert\ValidInvite()]
* @Assert\NotBlank() public string $inviteCode;
* @Assert\Length(min="32", max="32")
* @AppAssert\ValidInvite()
*/
public $inviteCode;
public function __construct(string $inviteCode = null) public function __construct(string $inviteCode = null)
{ {

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Form; namespace App\Form;
@ -16,9 +17,9 @@ class LoginType extends AbstractType
; ;
} }
public function getBlockPrefix() public function getBlockPrefix(): string
{ {
// Empty prefix for default UsernamePasswordFrormAuthenticationListener // Empty prefix for default UsernamePasswordFormAuthenticationListener
return ''; return '';
} }
} }

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Form; namespace App\Form;

View File

@ -1,10 +1,11 @@
<?php <?php
declare(strict_types=1);
namespace App\Form; namespace App\Form;
use App\Form\Data\PasswordResetData; use App\Form\Data\PasswordResetData;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\{HiddenType, PasswordType, RepeatedType}; use Symfony\Component\Form\Extension\Core\Type\{PasswordType, RepeatedType};
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Form; namespace App\Form;

View File

@ -1,11 +1,12 @@
<?php <?php
declare(strict_types=1);
namespace App\Helper; namespace App\Helper;
use App\Magnetico\Entity\Torrent; use App\Magnetico\Entity\Torrent;
use App\View\Torrent\FileTreeNode; use App\View\Torrent\FileTreeNode;
class BstreeviewFileTreeBuilder class BsTreeviewFileTreeBuilder
{ {
private const DEFAULT_FILE_ICON = 'fas fa-file'; private const DEFAULT_FILE_ICON = 'fas fa-file';
private const DEFAULT_DIR_ICON = 'fas fa-folder'; private const DEFAULT_DIR_ICON = 'fas fa-folder';
@ -64,4 +65,4 @@ class BstreeviewFileTreeBuilder
return $data; return $data;
} }
} }

View File

@ -30,11 +30,7 @@ class FileSizeHumanizer
$maxSuffixIndex = count(self::SIZE_SUFFIXES) - 1; $maxSuffixIndex = count(self::SIZE_SUFFIXES) - 1;
if ($maxSuffixIndex >= $factor) { $suffixIndex = min($maxSuffixIndex, $factor);
$suffixIndex = $factor;
} else {
$suffixIndex = $maxSuffixIndex;
}
$suffix = self::SIZE_SUFFIXES[$suffixIndex]; $suffix = self::SIZE_SUFFIXES[$suffixIndex];

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App; namespace App;

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Magnet; namespace App\Magnet;
@ -33,4 +34,4 @@ class MagnetGenerator
return $url; return $url;
} }
} }

View File

@ -1,51 +1,32 @@
<?php <?php
declare(strict_types=1);
namespace App\Magnetico\Entity; namespace App\Magnetico\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Serializer\Annotation as Serializer;
/** #[ORM\Entity(readOnly: true)]
* @ORM\Table(schema="magneticod", name="files", indexes={ #[ORM\Table(schema: 'magneticod', name: 'files')]
* @ORM\Index(name="file_info_hash_index", columns={"torrent_id"}) #[ORM\Index(name: 'file_info_hash_index', columns: ['torrent_id'])]
* })
* @ORM\Entity(readOnly=true)
*/
class File class File
{ {
/** #[ORM\Id]
* @var int #[ORM\Column(name: 'id', type: 'integer')]
* private int $id;
* @ORM\Column(name="id", type="integer")
* @ORM\Id
*/
private $id;
/** #[ORM\ManyToOne(targetEntity: Torrent::class, inversedBy: 'files')]
* @var Torrent #[ORM\JoinColumn(name: 'torrent_id')]
* private Torrent $torrent;
* @ORM\ManyToOne(targetEntity="App\Magnetico\Entity\Torrent", inversedBy="files")
* @ORM\JoinColumn(name="torrent_id")
*/
private $torrent;
/** /** File size in bytes */
* @var int File size in bytes #[Serializer\Groups(['api_v1_show'])]
* #[ORM\Column(name: 'size', type: 'bigint', nullable: false)]
* @Serializer\Groups({"api_v1_show"}) private int $size;
*
* @ORM\Column(name="size", type="bigint", nullable=false)
*/
private $size;
/** #[Serializer\Groups(['api_v1_show'])]
* @var string #[ORM\Column(name: 'path', type: 'text', nullable: false)]
* private string $path;
* @Serializer\Groups({"api_v1_show"})
*
* @ORM\Column(name="path", type="text", nullable=false)
*/
private $path;
public function getId(): int public function getId(): int
{ {

View File

@ -1,78 +1,49 @@
<?php <?php
declare(strict_types=1);
namespace App\Magnetico\Entity; namespace App\Magnetico\Entity;
use App\Magnetico\Repository\TorrentRepository;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Serializer\Annotation as Serializer;
/** #[ORM\Entity(repositoryClass: TorrentRepository::class, readOnly: true)]
* @ORM\Table(schema="magneticod", name="torrents", indexes={ #[ORM\Table(schema: 'magneticod', name: 'torrents')]
* @ORM\Index(name="discovered_on_index", columns={"discovered_on"}), #[ORM\Index(name: 'discovered_on_index', columns: ['discovered_on'])]
* @ORM\Index(name="info_hash_index", columns={"info_hash"}) #[ORM\Index(name: 'info_hash_index', columns: ['info_hash'])]
* })
* @ORM\Entity(readOnly=true, repositoryClass="App\Magnetico\Repository\TorrentRepository")
*/
class Torrent class Torrent
{ {
/** #[Serializer\Groups(['api_v1_search', 'api_v1_show'])]
* @var int #[ORM\Id]
* #[ORM\Column(name: 'id', type: 'integer')]
* @Serializer\Groups({"api_v1_search", "api_v1_show"}) private int $id;
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
*/
private $id;
/** /** @var resource Resource pointing to info-hash BLOB */
* @var resource Resource pointing to info-hash BLOB #[Serializer\Groups(['api_v1_search', 'api_v1_show'])]
* #[ORM\Column(name: 'info_hash', type: 'blob', nullable: false)]
* @Serializer\Groups({"api_v1_search", "api_v1_show"})
*
* @ORM\Column(name="info_hash", type="blob", nullable=false)
*/
private $infoHash; private $infoHash;
/** /** Cached value of self::infoHash in HEX string */
* @var string Cached value of self::infoHash in HEX string private ?string $infoHashHexCache = null;
*/
private $infoHashHexCache;
/** #[Serializer\Groups(['api_v1_search', 'api_v1_show'])]
* @var string Torrent name #[ORM\Column(name: 'name', type: 'text', nullable: false)]
* private string $name;
* @Serializer\Groups({"api_v1_search", "api_v1_show"})
*
* @ORM\Column(name="name", type="text", nullable=false)
*/
private $name;
/** /** Torrent files total size in bytes */
* @var int Torrent files total size in bytes #[Serializer\Groups(['api_v1_search', 'api_v1_show'])]
* #[ORM\Column(name: 'total_size', type: 'bigint', nullable: false)]
* @Serializer\Groups({"api_v1_search", "api_v1_show"}) private int $totalSize;
*
* @ORM\Column(name="total_size", type="bigint", nullable=false)
*/
private $totalSize;
/** /** Torrent discovery timestamp */
* @var int Torrent discovery timestamp #[Serializer\Groups(['api_v1_search', 'api_v1_show'])]
* #[ORM\Column(name: 'discovered_on', type: 'integer', nullable: false)]
* @Serializer\Groups({"api_v1_search", "api_v1_show"})
*
* @ORM\Column(name="discovered_on", type="integer", nullable=false)
*/
private $discoveredOn; private $discoveredOn;
/** /** @var File[]|ArrayCollection */
* @var File[]|ArrayCollection #[Serializer\Groups(['api_v1_show'])]
* #[ORM\OneToMany(targetEntity: File::class, fetch: 'EXTRA_LAZY', mappedBy: 'torrent')]
* @Serializer\Groups({"api_v1_show"})
*
* @ORM\OneToMany(targetEntity="App\Magnetico\Entity\File", fetch="EXTRA_LAZY", mappedBy="torrent")
*/
private $files; private $files;
public function getId(): int public function getId(): int

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Magnetico\Repository; namespace App\Magnetico\Repository;
@ -25,4 +26,4 @@ class TorrentRepository extends ServiceEntityRepository
return 0; return 0;
} }
} }
} }

View File

@ -1,13 +1,11 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace App\Pager\View; namespace App\Pager\View;
use Pagerfanta\Pagerfanta; use Pagerfanta\Pagerfanta;
use Pagerfanta\PagerfantaInterface; use Pagerfanta\PagerfantaInterface;
use Pagerfanta\View\Template\TemplateInterface; use Pagerfanta\View\Template\{TemplateInterface, TwitterBootstrap4Template};
use Pagerfanta\View\Template\TwitterBootstrap4Template;
use Pagerfanta\View\ViewInterface; use Pagerfanta\View\ViewInterface;
/** /**
@ -48,31 +46,31 @@ class TwitterBootstrap4PagelessView implements ViewInterface
return new TwitterBootstrap4Template(); return new TwitterBootstrap4Template();
} }
private function initializePagerfanta(PagerfantaInterface $pagerfanta) private function initializePagerfanta(PagerfantaInterface $pagerfanta): void
{ {
$this->pagerfanta = $pagerfanta; $this->pagerfanta = $pagerfanta;
$this->currentPage = $pagerfanta->getCurrentPage(); $this->currentPage = $pagerfanta->getCurrentPage();
} }
private function configureTemplate($routeGenerator, $options) private function configureTemplate($routeGenerator, $options): void
{ {
$this->template->setRouteGenerator($routeGenerator); $this->template->setRouteGenerator($routeGenerator);
$this->template->setOptions($options); $this->template->setOptions($options);
} }
private function generate() private function generate(): array|string
{ {
$pages = $this->generatePages(); $pages = $this->generatePages();
return $this->generateContainer($pages); return $this->generateContainer($pages);
} }
private function generateContainer($pages) private function generateContainer($pages): array|string
{ {
return str_replace('%pages%', $pages, $this->template->container()); return str_replace('%pages%', $pages, $this->template->container());
} }
private function generatePages() private function generatePages(): string
{ {
return $this->previous().$this->currentPage().$this->next(); return $this->previous().$this->currentPage().$this->next();
} }

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Repository; namespace App\Repository;
@ -32,4 +33,4 @@ class ApiTokenRepository extends ServiceEntityRepository
return $qb->getQuery()->getOneOrNullResult(); return $qb->getQuery()->getOneOrNullResult();
} }
} }

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Repository; namespace App\Repository;
@ -32,4 +33,4 @@ class InviteRepository extends ServiceEntityRepository
return $qb->getQuery()->getResult(); return $qb->getQuery()->getResult();
} }
} }

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Repository; namespace App\Repository;

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Repository; namespace App\Repository;
@ -17,4 +18,4 @@ class UserRepository extends ServiceEntityRepository
{ {
$this->getEntityManager()->persist($user); $this->getEntityManager()->persist($user);
} }
} }

View File

@ -13,16 +13,11 @@ class TorrentSearcher
private const ORDER_DISABLED_FIELDS = ['infoHash']; private const ORDER_DISABLED_FIELDS = ['infoHash'];
/** @var TorrentRepository */ public function __construct(
private $torrentRepo; private readonly TorrentRepository $torrentRepo,
private readonly ClassMetadata $classMetadata
) {
/** @var ClassMetadata */
private $classMetadata;
public function __construct(TorrentRepository $torrentRepo, ClassMetadata $classMetadata)
{
$this->torrentRepo = $torrentRepo;
$this->classMetadata = $classMetadata;
} }
public function createSearchQueryBuilder(string $query, string $orderBy = null, string $order = 'asc'): QueryBuilder public function createSearchQueryBuilder(string $query, string $orderBy = null, string $order = 'asc'): QueryBuilder

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Twig; namespace App\Twig;
@ -21,4 +22,4 @@ class FileSizeHumanizerExtension extends AbstractExtension
new TwigFilter('humanize_size', [$this->humanizer, 'humanize']), new TwigFilter('humanize_size', [$this->humanizer, 'humanize']),
]; ];
} }
} }

View File

@ -1,21 +1,22 @@
<?php <?php
declare(strict_types=1);
namespace App\Twig; namespace App\Twig;
use App\Helper\BstreeviewFileTreeBuilder; use App\Helper\BsTreeviewFileTreeBuilder;
use Twig\Extension\AbstractExtension; use Twig\Extension\AbstractExtension;
use Twig\{TwigFilter}; use Twig\{TwigFilter};
class FileTreeExtension extends AbstractExtension class FileTreeExtension extends AbstractExtension
{ {
private BstreeviewFileTreeBuilder $builder; private BsTreeviewFileTreeBuilder $builder;
public function __construct(BstreeviewFileTreeBuilder $builder) public function __construct(BsTreeviewFileTreeBuilder $builder)
{ {
$this->builder = $builder; $this->builder = $builder;
} }
public function getFilters() public function getFilters(): array
{ {
return [ return [
new TwigFilter('file_tree', [$this->builder, 'buildFileTreeDataArray']), new TwigFilter('file_tree', [$this->builder, 'buildFileTreeDataArray']),

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Twig; namespace App\Twig;
@ -8,12 +9,10 @@ use Twig\{TwigFilter, TwigFunction};
class MagnetExtension extends AbstractExtension class MagnetExtension extends AbstractExtension
{ {
/** @var MagnetGenerator */ public function __construct(
private $magnetGenerator; private readonly MagnetGenerator $magnetGenerator
) {
public function __construct(MagnetGenerator $magnetGenerator)
{
$this->magnetGenerator = $magnetGenerator;
} }
public function getFunctions(): array public function getFunctions(): array
@ -23,10 +22,10 @@ class MagnetExtension extends AbstractExtension
]; ];
} }
public function getFilters() public function getFilters(): array
{ {
return [ return [
new TwigFilter('magnet', [$this->magnetGenerator, 'generate']), new TwigFilter('magnet', [$this->magnetGenerator, 'generate']),
]; ];
} }
} }

View File

@ -1,8 +1,9 @@
<?php <?php
declare(strict_types=1);
namespace App\User\Exception; namespace App\User\Exception;
class InvalidInviteException extends \InvalidArgumentException class InvalidInviteException extends \InvalidArgumentException
{ {
protected $message = 'Invalid invite'; protected $message = 'Invalid invite';
} }

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\User\Exception; namespace App\User\Exception;

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\User; namespace App\User;
@ -7,16 +8,11 @@ use App\Repository\InviteRepository;
class InviteManager class InviteManager
{ {
/** @var InviteRepository */ public function __construct(
private $inviteRepo; private readonly InviteRepository $inviteRepo,
private readonly int $newUserInvites = 0
) {
/** @var int Which amount of invites we need to give to the new user */
private $newUserInvites;
public function __construct(InviteRepository $inviteRepo, int $newUserInvites = 0)
{
$this->inviteRepo = $inviteRepo;
$this->newUserInvites = $newUserInvites;
} }
/** /**
@ -28,7 +24,7 @@ class InviteManager
return []; return [];
} }
$amount = (null !== $forceAmount) ? $forceAmount : $this->newUserInvites; $amount = $forceAmount ?? $this->newUserInvites;
$invites = []; $invites = [];
@ -40,4 +36,4 @@ class InviteManager
return $invites; return $invites;
} }
} }

View File

@ -1,5 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\User; namespace App\User;
@ -14,38 +14,15 @@ use Symfony\Component\Routing\{Generator\UrlGeneratorInterface, RouterInterface}
class PasswordResetManager class PasswordResetManager
{ {
/** @var UserRepository */
private $userRepo;
/** @var PasswordResetTokenRepository */
private $tokenRepo;
/** @var EntityManagerInterface */
private $em;
/** @var MailerInterface */
private $mailer;
/** @var RouterInterface */
private $router;
/** @var string */
private $fromAddress;
public function __construct( public function __construct(
UserRepository $userRepo, private readonly UserRepository $userRepo,
PasswordResetTokenRepository $tokenRepo, private readonly PasswordResetTokenRepository $tokenRepo,
EntityManagerInterface $em, private readonly EntityManagerInterface $em,
MailerInterface $mailer, private readonly MailerInterface $mailer,
RouterInterface $router, private readonly RouterInterface $router,
string $fromAddress private readonly string $fromAddress
) { ) {
$this->userRepo = $userRepo;
$this->tokenRepo = $tokenRepo;
$this->em = $em;
$this->mailer = $mailer;
$this->router = $router;
$this->fromAddress = $fromAddress;
} }
public function sendResetLink(string $address): void public function sendResetLink(string $address): void

View File

@ -1,37 +1,29 @@
<?php <?php
declare(strict_types=1);
namespace App\User; namespace App\User;
use App\Entity\{Invite, User}; use App\Entity\{Invite, User};
use App\Repository\{InviteRepository, UserRepository}; use App\Repository\UserRepository;
use App\User\Exception\InvalidInviteException; use App\User\Exception\InvalidInviteException;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
class UserManager class UserManager
{ {
private const DEFAULT_ROLES = ['ROLE_USER']; private const DEFAULT_ROLES = ['ROLE_USER'];
/** @var UserRepository */ public function __construct(
private $userRepo; private readonly PasswordHasherFactoryInterface $hasherFactory,
private readonly UserRepository $userRepo,
) {
/** @var InviteRepository */
private $inviteRepo;
/** @var EncoderFactoryInterface */
private $encoderFactory;
public function __construct(EncoderFactoryInterface $encoderFactory, UserRepository $userRepo, InviteRepository $inviteRepo)
{
$this->userRepo = $userRepo;
$this->inviteRepo = $inviteRepo;
$this->encoderFactory = $encoderFactory;
} }
public function createUser(string $username, string $password, string $email, array $roles = self::DEFAULT_ROLES): User public function createUser(string $username, string $password, string $email, array $roles = self::DEFAULT_ROLES): User
{ {
$user = new User( $user = new User(
$username, $username,
$this->encoderFactory->getEncoder(User::class), $this->hasherFactory->getPasswordHasher(User::class),
$password, $password,
$email, $email,
$roles $roles
@ -44,10 +36,7 @@ class UserManager
public function changePassword(User $user, string $rawPassword): void public function changePassword(User $user, string $rawPassword): void
{ {
$user->changePassword( $user->changePassword($this->hasherFactory->getPasswordHasher(User::class), $rawPassword);
$this->encoderFactory->getEncoder(User::class),
$rawPassword
);
} }
public function createUserByInvite(string $username, string $password, string $email, Invite $invite, array $roles = self::DEFAULT_ROLES): User public function createUserByInvite(string $username, string $password, string $email, Invite $invite, array $roles = self::DEFAULT_ROLES): User

View File

@ -1,12 +1,11 @@
<?php <?php
declare(strict_types=1);
namespace App\Validator\Constraints; namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
/** #[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD)]
* @Annotation
*/
class ValidInvite extends Constraint class ValidInvite extends Constraint
{ {
public $notFoundMessage = 'Invite {{ code }} not found.'; public $notFoundMessage = 'Invite {{ code }} not found.';
@ -14,6 +13,6 @@ class ValidInvite extends Constraint
public function validatedBy(): string public function validatedBy(): string
{ {
return get_class($this).'Validator'; return static::class.'Validator';
} }
} }

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace App\Validator\Constraints; namespace App\Validator\Constraints;
@ -8,19 +9,16 @@ use Symfony\Component\Validator\{Constraint, ConstraintValidator};
class ValidInviteValidator extends ConstraintValidator class ValidInviteValidator extends ConstraintValidator
{ {
/** @var InviteRepository */ public function __construct(
private $inviteRepo; private readonly InviteRepository $inviteRepo
) {
public function __construct(InviteRepository $inviteRepo)
{
$this->inviteRepo = $inviteRepo;
} }
/** /**
* @param mixed $value
* @param ValidInvite $constraint * @param ValidInvite $constraint
*/ */
public function validate($value, Constraint $constraint) public function validate(mixed $value, Constraint $constraint)
{ {
/** @var Invite $invite */ /** @var Invite $invite */
if (null === $invite = $this->inviteRepo->findOneBy(['code' => $value])) { if (null === $invite = $this->inviteRepo->findOneBy(['code' => $value])) {
@ -39,4 +37,4 @@ class ValidInviteValidator extends ConstraintValidator
; ;
} }
} }
} }

View File

@ -1,9 +1,9 @@
<?php <?php
declare(strict_types=1);
namespace App\View\Torrent; namespace App\View\Torrent;
use App\Magnetico\Entity\File; use App\Magnetico\Entity\{File, Torrent};
use App\Magnetico\Entity\Torrent;
class FileTreeNode class FileTreeNode
{ {
@ -52,7 +52,7 @@ class FileTreeNode
// If we have file only file and not a tree. // If we have file only file and not a tree.
if (1 === count($pathParts)) { if (1 === count($pathParts)) {
$this->addChild($path, FileTreeNode::createFromFile($path, $file, $this)); $this->addChild($path, self::createFromFile($path, $file, $this));
return; return;
} }
@ -89,12 +89,7 @@ class FileTreeNode
return array_key_exists($name, $this->children); return array_key_exists($name, $this->children);
} }
/** public function getChild(string $name): File|FileTreeNode
* @param string $name
*
* @return FileTreeNode|File
*/
public function getChild(string $name)
{ {
if (!array_key_exists($name, $this->children)) { if (!array_key_exists($name, $this->children)) {
throw new \InvalidArgumentException(sprintf( throw new \InvalidArgumentException(sprintf(
@ -119,7 +114,7 @@ class FileTreeNode
$files = []; $files = [];
foreach ($this->children as $name => $child) { foreach ($this->children as $name => $child) {
if ($child instanceof FileTreeNode) { if ($child instanceof self) {
$dirs[$name] = $child; $dirs[$name] = $child;
} elseif ($child instanceof File) { } elseif ($child instanceof File) {
$files[] = $child; $files[] = $child;