Difference between #6 and #27 of
Yii3 - How to start

Changes

Title unchanged

Yii3 - How to start

Category unchanged

Tutorials

Yii version unchanged

3.0

Tags unchanged

Content changed

(draft)
 
 
In Yii3 it is not as easy to start as it was with Yii2. You have to install and configure basic things on your own. Yii3 uses the modern approach based on independent packages and dependency injection, but it makes it harder for newcomers. I am here to show them ...
 
 
Note:
 
- Instead of installing local WAMP- or XAMPP-server I will be using Docker.
 
- Do not forget about a modern IDE like PhpStorm, which comes budled with all you will ever need.
 
 
# Yii3 - How to start
 
 
Yii3 offers more basic applications: Web, Console, API. I will be using the API application:
 
- https://github.com/yiisoft/app-api
 
- Other apps are linked on the page
 
 
Clone it like this:
 
- git clone https://github.com/yiisoft/app-api.git yii3api
 
 
.. and follow the **docker** instructions in the documentation.
 
 
If you don't have Docker, I recommend installing the **latest** version of Docker Desktop:
 
- https://docs.docker.com/get-started/introduction/get-docker-desktop/
 
 
## Running the demo application
 
 
You may be surprised that docker-compose.yml is missing in the root. Instead the "make" commands are prepared.
 
If you run both basic commands as mentioned in the documentation:
 
 
- make composer update
 
- make up
 
 
... then the web will be available on URL
 
- http://localhost:80
 
 
If you check the returned data you will see a <xml> inside the browser. In order to obtain JSON-response, paste the URL into Postman. (so called "content negotiation" does this auto-decision)
 
 
If you want to modify the data that was returned by the endpoint, just open the action-class (src/Api/IndexAction.php) and add one more element to the returned array.
 
 
## Adding DB into your project
 
 
Your project now does not contain any DB. Let's add MariaDB and Adminer (DB browser) into file docker/dev/compose.yml:
 
 
In my case the resulting file looks like this:
 
 
services:
 
  app:
 
    container_name: yii3api_php
 
    build:
 
      dockerfile: docker/Dockerfile
 
      context: ..
 
      target: dev
 
      args:
 
        USER_ID: ${UID}
 
        GROUP_ID: ${GID}
 
    env_file:
 
      - path: ./dev/.env
 
      - path: ./dev/override.env
 
        required: false
 
    restart: unless-stopped
 
    depends_on:
 
      - db
 
    ports:
 
      - "${DEV_PORT:-80}:80"
 
    volumes:
 
      - ../:/app
 
      - ../runtime:/app/runtime
 
      - caddy_data:/data
 
      - caddy_config:/config
 
    tty: true
 
  db:
 
    image: mariadb:12.0.2-noble
 
    container_name: yii3api_db
 
    environment:
 
      MARIADB_ROOT_PASSWORD: root
 
      MARIADB_DATABASE: db
 
      MARIADB_USER: db
 
      MARIADB_PASSWORD: db
 
  adminer:
 
    image: adminer:latest
 
    container_name: yii3api_adminer
 
    environment:
 
      ADMINER_DEFAULT_SERVER: db
 
    ports:
 
      - ${DEV_ADMINER_PORT}:8080
 
    depends_on:
 
      - db
 
volumes:
 
  mariadb_data:
 
 
 
Plus add/modify these variables in file docker/.env
 
- DEV_PORT=9080
 
- DEV_ADMINER_PORT=9081
 
 
Then run following commands:
 
- make down
 
- make build
 
- make up
 
 
Now you should see a DB browser on URL http://localhost:9081/?server=db&username=db&db=db
 
 
Login, server and pwd is defined in the snippet above.
 
 
If you type "docker ps" into your host console, you should see 3 running containers: yii3api_php, yii3api_adminer, yii3api_db.
 
 
The web will be, from now on, available on URL http://localhost:9080 which is more handy than just ":80" I think.
 
 
## Enabling MariaDB (MySQL) and migrations
 
 
Now when your project contains MariaDB, you may wanna use it in the code ...
 
 
### **Installing composer packages**
 
 
After some time of searching you will discover you need to install these composer packages:
 
- https://github.com/yiisoft/db-mysql
 
- https://github.com/yiisoft/cache
 
- https://github.com/yiisoft/db-migration
 
 
So you need to run following commands:
 
- composer require yiisoft/db-mysql
 
- composer require yiisoft/cache
 
- composer require yiisoft/db-migration --dev
 
 
To run composer (or any other command inside your dockerized yii3 application) you have 4 options:
 
- Make:
 
The best solution is to prepend the composer commands with "make".
 
 
Other solutions:
 
> - Locally:
 
> If you have Composer running locally, you can call these commands directly on your computer. (I do not recommend)
 
> - Docker:
 
> You can SSH into your docker container and call it there as Composer is installed inside.
 
> Find the name of the PHP container by typing "docker ps" and call:
 
> docker exec -it {containerName} /bin/bash
 
> Now you are in the console of your php server and you can run composer.
 
> - PhpStorm:
 
> If you are using PhpStorm, find the small icon "Services" in the left lower corner (looks ca like a cog wheel), find item "Docker-compose: app-api", inside click the "app" service, then "yii3api_php" container and then hit the button "terminal" on the righthand side.
 
 
### **Setting up composer packages**
 
 
Follow their documentations. Quick links:
 
- https://github.com/yiisoft/db/blob/master/docs/guide/en/connection/mysql.md (I did not need the snippet with "new DsnSocket()")
 
- https://github.com/yiisoft/db-migration/blob/master/docs/guide/en/README.md (I recommend "Yii Console" installation)
 
 
> The documentations want you to create 2 files:
 
- config/common/di/db-mysql.php
 
- config/common/db.php
 
... you actually need only one. I recommend **db-mysql.php**
 
 
> Note: If you want to create a file using commandline, you can use command "touch". For example "touch config/common/di/db-mysql.php"
 
n
 
 
> Note: In the documentation the PHP snippets do not contain tag and declaration. Prepend it:
 
 
```php
 
<?php
 
declare(strict_types=1);
 
``` 
 
 
### **Create folder for migrations**
 
- src/Migration
 
 
When this is done, call "composer du" or "make composer du" and then try "make yii list". You should see the migration commands.
 
 
## Creating a migration
 
 
Run the command to create a migration:
 
- make yii migrate:create user
 
 
Open the file and paste following content to the up() method:
 
 
```php
 
$b->createTable('user', [
 
'id' => $b->primaryKey(),
 
'name' => $b->string()->notNull(),
 
'surname' => $b->string()->notNull(),
 
'username' => $b->string(),
 
'email' => $b->string()->notNull()->unique(),
 
'phone' => $b->string(),
 
'admin_enabled' => $b->boolean()->notNull()->defaultValue(false)->comment('Can user access the administration?'),
 
'vuejs_enabled' => $b->boolean()->notNull()->defaultValue(false)->comment('Can user access the mobile application?'),
 
'auth_key' => $b->string(32)->notNull()->unique(),
 
'access_token' => $b->string(32)->unique()->comment('For API purposes'),
 
'password_hash' => $b->string(),
 
'password_default' => $b->string(),
 
'password_vuejs_default' => $b->string(),
 
'password_vuejs_hash' => $b->string(),
 
'password_reset_token' => $b->string()->unique(),
 
'verification_token' => $b->string()->unique(),
 
'verified_at' => $b->dateTime(),
 
'status' => $b->smallInteger()->notNull()->defaultValue(100),
 
'created_by' => $b->integer(),
 
'updated_by' => $b->integer(),
 
'deleted_by' => $b->integer(),
 
'created_at' => $b->dateTime()->notNull()->defaultExpression('CURRENT_TIMESTAMP'),
 
'updated_at' => $b->dateTime(),
 
'deleted_at' => $b->dateTime(),
 
]);
 
```
 
 
The down() method should contain this:
 
 
```php
 
$b->dropTable('user');
 
```
 
 
## Running the migrations
 
Try to run "make yii migrate:up" and you will see error "could not find driver", because file "docker/Dockerfile" does not install the "pdo_mysql" extention. Add it to the where "install-php-extensions" is called.
 
 
Then call:
 
- make down
 
- make build
 
- make up
 
 
Now you will see error "Connection refused"
 
It means you have to update dns, user and password in file "config/common/params.php" based on what is written in "docker/dev/compose.yml".
 
 
If you run "make yii migrate:up" it should work now and your DB should contain the first table. Check it via adminer:
 
http://localhost:9081/?server=db&username=db&db=db
 
 
 - all will be retested later)
 
 
In Yii3 it is not as easy to start as it was with Yii2. You have to install and configure basic things on your own. Yii3 uses the modern approach based on independent packages and dependency injection, but it makes it harder for newcomers. I am here to show them ...
 
 
Note:
 
- Instead of installing local WAMP- or XAMPP-server I will be using Docker.
 
- Do not forget about a modern IDE like PhpStorm, which comes bunled with all you will ever need.
 
 
# Yii3 - How to start
 
 
Yii3 offers more basic applications: Web, Console, API. I will be using the API application:
 
- https://github.com/yiisoft/app-api
 
- Other apps are linked on the page
 
 
Clone it like this:
 
- git clone https://github.com/yiisoft/app-api.git yii3api
 
 
.. and follow the **docker** instructions in the documentation.
 
 
If you don't have Docker, I recommend installing the **latest** version of Docker Desktop:
 
- https://docs.docker.com/get-started/introduction/get-docker-desktop/
 
 
## Running the demo application
 
 
You may be surprised that docker-compose.yml is missing in the root. Instead the "make" commands are prepared.
 
If you run both basic commands as mentioned in the documentation:
 
 
- make composer update
 
- make up
 
 
... then the web will be available on URL
 
- http://localhost:80
 
 
If you check the returned data you will see a `<xml>` inside the browser. In order to obtain JSON-response, paste the URL into Postman. (so called "content negotiation" does this auto-decision)
 
 
If you want to modify the data that was returned by the endpoint, just open the action-class `src/Api/IndexAction.php` and add one more element to the returned array.
 
 
## Adding DB into your project
 
 
Your project now does not contain any DB. Let's add MariaDB and Adminer (DB browser) into file docker/dev/compose.yml:
 
 
In my case the resulting file looks like this:
 
 
```yaml
 
services:
 
  app:
 
    container_name: yii3api_php
 
    build:
 
      dockerfile: docker/Dockerfile
 
      context: ..
 
      target: dev
 
      args:
 
        USER_ID: ${UID}
 
        GROUP_ID: ${GID}
 
    env_file:
 
      - path: ./dev/.env
 
      - path: ./dev/override.env
 
        required: false
 
    restart: unless-stopped
 
    depends_on:
 
      - db
 
    ports:
 
      - "${DEV_PORT:-80}:80"
 
    volumes:
 
      - ../:/app
 
      - ../runtime:/app/runtime
 
      - caddy_data:/data
 
      - caddy_config:/config
 
    tty: true
 
  db:
 
    image: mariadb:12.0.2-noble
 
    container_name: yii3api_db
 
    environment:
 
      MARIADB_ROOT_PASSWORD: root
 
      MARIADB_DATABASE: db
 
      MARIADB_USER: db
 
      MARIADB_PASSWORD: db
 
  adminer:
 
    image: adminer:latest
 
    container_name: yii3api_adminer
 
    environment:
 
      ADMINER_DEFAULT_SERVER: db
 
    ports:
 
      - ${DEV_ADMINER_PORT}:8080
 
    depends_on:
 
      - db
 
volumes:
 
  mariadb_data:
 
```
 
 
Plus add/modify these variables in file `docker/.env`
 
- DEV_PORT=9080
 
- DEV_ADMINER_PORT=9081
 
 
Then run following commands:
 
- make down
 
- make build
 
- make up
 
 
Now you should see a DB browser on URL http://localhost:9081/?server=db&username=db&db=db
 
 
Login, server and pwd is defined in the snippet above.
 
 
If you type "docker ps" into your host console, you should see 3 running containers: yii3api_php, yii3api_adminer, yii3api_db.
 
 
The web will be, from now on, available on URL http://localhost:9080 which is more handy than just ":80" I think. (Later you may run 4 different projects at the same time and all cannot run on port 80)
 
 
## Enabling MariaDB (MySQL) and migrations
 
 
Now when your project contains MariaDB, you may wanna use it in the code ...
 
 
### **Installing composer packages**
 
 
After some time of searching you will discover you need to install these composer packages:
 
- https://github.com/yiisoft/db-mysql
 
- https://github.com/yiisoft/cache
 
- https://github.com/yiisoft/db-migration
 
 
So you need to run following commands:
 
 
```sh
 
composer require yiisoft/db-mysql
 
composer require yiisoft/cache
 
composer require yiisoft/db-migration --dev
 
```
 
 
To run composer (or any other command inside your dockerized yii3 application) you have 4 options:
 
- Make:
 
The best solution is to prepend the composer commands with "make".
 
 
 
> Other solutions:
 

 
> - If you have Composer running locally, you can call these commands directly on your computer. (I do not recommend)
 

 
> - You can SSH into your docker container and call it there as Composer is installed inside.
 
> In that case:
 
>   - Find the name of the PHP container by typing "docker ps"
 
>   - Call "docker exec -it {containerName} /bin/bash"
 
>   - Now you are in the console of your php server and you can run composer.
 

 
> - If you are using PhpStorm, find the small icon "Services" in the left lower corner (looks ca like a cog wheel), find item "Docker-compose: app-api", inside click the "app" service, then "yii3api_php" container and then hit the button "terminal" on the righthand side.
 
 
### **Setting up composer packages**
 
 
Follow their documentations. Quick links:
 
- https://github.com/yiisoft/db/blob/master/docs/guide/en/connection/mysql.md (I did not need the snippet with "new DsnSocket()")
 
- https://github.com/yiisoft/db-migration/blob/master/docs/guide/en/README.md (I recommend "Yii Console" installation)
 
 
> The documentations want you to create 2 files:
 
> - config/common/di/db-mysql.php
 
> - config/common/db.php
 
> - But you actually need only one. I recommend **db-mysql.php**
 
 
> Note: If you want to create a file using commandline, you can use command "touch". For example "touch config/common/di/db-mysql.php"
 
 
> Note: In the documentation the PHP snippets do not contain tag and declaration. Prepend it:
 
 
```php
 
<?php
 
declare(strict_types=1);
 
``` 
 
 
### **Create folder for migrations**
 
- src/Migration
 
 
When this is done, call "composer du" or "make composer du" and then try "make yii list". You should see the migration commands.
 
 
## Creating a migration
 
 
Run the command to create a migration:
 
- make yii migrate:create user
 
 
Open the file and paste following content to the up() method:
 
 
```php
 
$b->createTable('user', [
 
'id' => $b->primaryKey(),
 
'name' => $b->string()->notNull(),
 
'surname' => $b->string()->notNull(),
 
'username' => $b->string(),
 
'email' => $b->string()->notNull()->unique(),
 
'phone' => $b->string(),
 
'admin_enabled' => $b->boolean()->notNull()->defaultValue(false)->comment('Can user access the administration?'),
 
'vuejs_enabled' => $b->boolean()->notNull()->defaultValue(false)->comment('Can user access the mobile application?'),
 
'auth_key' => $b->string(32)->notNull()->unique(),
 
'access_token' => $b->string(32)->unique()->comment('For API purposes'),
 
'password_hash' => $b->string(),
 
'password_default' => $b->string(),
 
'password_vuejs_default' => $b->string(),
 
'password_vuejs_hash' => $b->string(),
 
'password_reset_token' => $b->string()->unique(),
 
'verification_token' => $b->string()->unique(),
 
'verified_at' => $b->dateTime(),
 
'status' => $b->smallInteger()->notNull()->defaultValue(100),
 
'created_by' => $b->integer(),
 
'updated_by' => $b->integer(),
 
'deleted_by' => $b->integer(),
 
'created_at' => $b->dateTime()->notNull()->defaultExpression('CURRENT_TIMESTAMP'),
 
'updated_at' => $b->dateTime(),
 
'deleted_at' => $b->dateTime(),
 
]);
 
```
 
 
The down() method should contain this:
 
 
```php
 
$b->dropTable('user');
 
```
 
 
## Running the migrations
 
Try to run "make yii migrate:up" and you will see error "could not find driver", because file "docker/Dockerfile" does not install the "pdo_mysql" extention. Add it to the place where "install-php-extensions" is called.
 
 
Then call:
 
- make down
 
- make build
 
- make up
 
 
Now you will see error "Connection refused"
 
It means you have to update dns, user and password in file "config/common/params.php" based on what is written in "docker/dev/compose.yml".
 
 
If you run "make yii migrate:up" it should work now and your DB should contain the first table. Check it via adminer:
 
http://localhost:9081/?server=db&username=db&db=db
 
 
## Reading data from DB
 
 
In Yii we were always using ActiveRecord and its models, but in Yii3 the package is not ready yet. The solution is to use existing class `Yiisoft\Db\Query\Query`.
 
 
Open class `src/Api/IndexAction.php` and modify it a little to return all users via your REST API. You have more options:
 
 
You can manually instantiate the Query object, but you need to provide the DB connection manually:
 
 
```php
 
declare(strict_types=1);
 
namespace App\Api;
 
use App\Api\Shared\ResponseFactory;
 
use App\Shared\ApplicationParams;
 
use Psr\Http\Message\ResponseInterface;
 
use Yiisoft\Db\Connection\ConnectionInterface;
 
use Yiisoft\Db\Query\Query;
 
 
final class IndexAction
 
{
 
    public function __invoke(
 
        ResponseFactory     $responseFactory,
 
        ApplicationParams   $applicationParams,
 
        ConnectionInterface $db,
 
    ): ResponseInterface
 
    {
 
        $query = (new Query($db))
 
            ->select('*')
 
            ->from('user');
 
        return $responseFactory->success($query->all());
 
    }
 
}
 
```
 
 
Or you can use the DI container to provide you with the instance. I like this better as I can omit input parameters:
 
 
 
```php
 
declare(strict_types=1);
 
namespace App\Api;
 
use App\Api\Shared\ResponseFactory;
 
use App\Shared\ApplicationParams;
 
use Psr\Container\ContainerInterface;
 
use Psr\Http\Message\ResponseInterface;
 
use Yiisoft\Db\Query\Query;
 
 
final class IndexAction
 
{
 
    public function __invoke(
 
        ResponseFactory    $responseFactory,
 
        ApplicationParams  $applicationParams,
 
        ContainerInterface $container,
 
    ): ResponseInterface
 
    {
 
        $query = $container->get(Query::class)
 
            ->select('*')
 
            ->from('user');
 
        return $responseFactory->success($query->all());
 
    }
 
}
 
```
 
 
Now you can call the URL and see all the users. (If you entered some)
 
http://localhost:9080
 
 
> Note:
 
> You can also use Injector (and method `$injector->make()`) instead of ContainerInterface (and method `$container->get()`). Injector seems to allow you to pass input arguments if needed.
 
 
> PS: The input parameter of `new Query(ConnectionInterface $db)` is automatically provided as it is defined in DI. See the file you created earlier above: `config/common/di/db-mysql.php`
 
 
## Seeding the database 
 
 
Seeding = inserting fake data.
 
 
You can technically create a migration or a command and insert random data manually. But you can also use the Faker. In that case I needed following dependencies:
 
 
```sh
 
composer require fakerphp/faker
 
composer require yiisoft/security (not only for generating random strings)
 
```
 
 
Now find the class `HelloCommand.php`, copy and rename it to `SeedCommand.php`
 
 
Inside you will need the instance of `ConnectionInterface`. It can be automatically provided by the DI (because you defined it in `config/common/di/db-mysql.php`), you only need to create a new constructor and then use the instance in method execute():
 
 
```php
 
namespace App\Console;
 
 
use Faker\Factory;
 
use Symfony\Component\Console\Attribute\AsCommand;
 
use Symfony\Component\Console\Command\Command;
 
use Symfony\Component\Console\Input\InputInterface;
 
use Symfony\Component\Console\Output\OutputInterface;
 
use Yiisoft\Db\Connection\ConnectionInterface;
 
use Yiisoft\Security\Random;
 
use Yiisoft\Yii\Console\ExitCode;
 
 
#[AsCommand(
 
    name: 'seed',
 
    description: 'Run to seed the DB',
 
)]
 
final class SeedCommand extends Command
 
{
 
    public function __construct(
 
        private readonly ConnectionInterface $db
 
    )
 
    {
 
        parent::__construct();
 
    }
 
 
    protected function execute(
 
        InputInterface  $input,
 
        OutputInterface $output
 
    ): int
 
    {
 
 
        $faker = Factory::create();
 
 
        for ($i = 0; $i < 10; $i++) {
 
            $this->db->createCommand()
 
                ->insert('user', [
 
                    'name' => $faker->firstName(),
 
                    'surname' => $faker->lastName(),
 
                    'username' => $faker->userName(),
 
                    'email' => $faker->email(),
 
                    'auth_key' => Random::string(32),
 
                ])
 
                ->execute();
 
        }
 
 
        $output->writeln('Seeding DONE.');
 
 
        return ExitCode::OK;
 
    }
 
}
 
```
 
 
Register the new command in file `config/console/commands.php`.
 
 
> You can also obtain the ConnectionInterface in the same way as you did it in `IndexAction` with the `Query` object. Just use `ContainerInterface $container` in the constructor instead of `ConnectionInterface $db`. Then you can call `$db = $this->container->get(ConnectionInterface::class);`.
 
 
## Using Repository and the Model class
 
 
Each entity should have its Model class and Repository class if you are storing it in DB. Have a look at the demo application "blog-api": https://github.com/yiisoft/demo
 
 
In my case the User model (file `src/Entity/User.php`) will only contain private attributes, setters and getters. `UserRepository` (placed in the same folder) may look like this to enable CRUD (compressed code):
 
 
```php
 
<?php
 
declare(strict_types=1);
 
namespace App\Entity;
 
use DateTimeImmutable;
 
use Yiisoft\Db\Connection\ConnectionInterface;
 
use Yiisoft\Db\Exception\Exception;
 
use Yiisoft\Db\Exception\InvalidConfigException;
 
use Yiisoft\Db\Query\Query;
 
final class UserRepository
 
{
 
    public const TABLE_NAME = 'user';
 
    public function __construct(private readonly ConnectionInterface $db){}
 
    public function findAll(array $orderBy = [], $asArray = false): array
 
    {
 
        $query = (new Query($this->db))->select('*')->from(self::TABLE_NAME)->orderBy($orderBy ?: ['created_at' => SORT_DESC]);
 
        if ($asArray) {
 
            return $query->all();
 
        }
 
        return array_map(
 
            fn(array $row) => $this->hydrate($row),
 
            $query->all()
 
        );
 
    }
 
    public function findBy(string $attr, mixed $value): ?User
 
    {
 
        $row = (new Query($this->db))->select('*')->from(self::TABLE_NAME)->where([$attr => $value])->one();
 
        return $row ? $this->hydrate($row) : null;
 
    }
 
    public function findByUsername(string $username): ?User
 
    {
 
        return $this->findBy('username', $username);
 
    }
 
    public function save(User $user): void
 
    {
 
        $data = ['name' => $user->getName(), 'surname' => $user->getSurname(), 'username' => $user->getUsername(), 'email' => $user->getEmail(), 'auth_key' => $user->getAuthKey()];
 
        if ($user->getId() === null) {
 
            $data['created_at'] = (new DateTimeImmutable())->format('Y-m-d H:i:s');
 
            $this->db->createCommand()->insert(self::TABLE_NAME, $data)->execute();
 
        } else {
 
            $this->db->createCommand()->update(self::TABLE_NAME, $data, ['id' => $user->getId()])->execute();
 
        }
 
    }
 
    public function delete(int $id): bool
 
    {
 
        try {
 
            $this->db->createCommand()->delete(self::TABLE_NAME, ['id' => $id])->execute();
 
        } catch (\Throwable $e) {
 
            return false;
 
        }
 
        return true;
 
    }
 
    private function hydrate(array $row): User
 
    {
 
        $user = new User();
 
        $reflection = new \ReflectionClass($user);
 
        $this->hydrateAttribute($reflection, $user, 'id', (int) $row['id']);
 
        $this->hydrateAttribute($reflection, $user, 'name', ($row['name']));
 
        $this->hydrateAttribute($reflection, $user, 'surname', $row['surname']);
 
        $this->hydrateAttribute($reflection, $user, 'username', $row['username']);
 
        $this->hydrateAttribute($reflection, $user, 'email', $row['email']);
 
        $this->hydrateAttribute($reflection, $user, 'created_at', new DateTimeImmutable($row['created_at']));
 
        $this->hydrateAttribute($reflection, $user, 'updated_at', new DateTimeImmutable($row['updated_at'] ?? ''));
 
        return $user;
 
    }
 
    private function hydrateAttribute(\ReflectionClass $reflection, object $obj, string $attribute, mixed $value)
 
    {
 
        $idProperty = $reflection->getProperty($attribute);
 
        $idProperty->setAccessible(true);
 
        $idProperty->setValue($obj, $value);
 
    }
 
}
 
```
 
 
Now you can modify `IndexAction` to contain this: (read above to understand details)
 
 
```php
 
// use App\Entity\UserRepository;
 
$userRepository = $container->get(UserRepository::class);
 
return $responseFactory->success($userRepository->findAll([], true));
 
```
 
 
## API login + access token
 
 
Once user logs in you want to create an access-token. Why? Because in APIs the PHP session is not used, so users would have to send their login in every request, which would be a potential risk. So random strings with limited lifetime are generated and users send them in their requests intstead of the login. After a few minutes or hours the access token expires and a new one must be created. Each user can have more tokens for different situations. Details here:
 
https://goteleport.com/learn/authentication-and-authorization/simple-random-tokens-secure-authentication/
 
 
Below I am indicating how to implement "Random Token Authentication". Other options would be:
 
- JWT (JSON Web Token) .. I see some disadvatages
 
- OAuth, OAuth2 - too complex for a simple API
 
 
Before you start, install dependency:
 
 
```sh
 
composer require yiisoft/security
 
```
 
 
Let's create a migration for storing the access tokens:
 
 
```php
 
// method up():
 
$b->createTable('user_token', [
 
    'id' => $b->primaryKey(),
 
    'id_user' => $b->integer()->notNull(),
 
    'token' => $b->string()->notNull()->unique(),
 
    'expires_at' => $b->dateTime()->notNull(),
 
    'created_at' => $b->dateTime()->notNull()->defaultExpression('CURRENT_TIMESTAMP'),
 
    'updated_at' => $b->dateTime(),
 
    'deleted_at' => $b->dateTime(),
 
]);
 
```
 
 
Then create a model `App\Entity\UserToken`. It again contains only private properties, getters and setters. Plus I added __construct() and toArray():
 
 
```php
 
// Uglified code:
 
#[Column(type: 'primary')]
 
private int $id;
 
#[Column(type: 'integer')]
 
private int $id_user;
 
#[Column(type: 'string(255)', default: '')]
 
private string $token = '';
 
#[Column(type: 'datetime')]
 
private DateTimeImmutable $expires_at;
 
#[Column(type: 'datetime', nullable: true)]
 
private ?DateTimeImmutable $created_at;
 
#[Column(type: 'datetime', nullable: true)]
 
private ?DateTimeImmutable $updated_at;
 
#[Column(type: 'datetime', nullable: true)]
 
private ?DateTimeImmutable $deleted_at;
 
public function __construct(int $userId, string $token, DateTimeImmutable $expiresAt = null)
 
{
 
    $this->id_user = $userId;
 
    $this->token = $token;
 
    $this->expires_at = $expiresAt;
 
}
 
public function toArray(): array
 
{
 
    return [
 
        'id' => $this->id,
 
        'id_user' => $this->id_user,
 
        'token' => $this->token,
 
        'expires_at' => $this->expires_at->format('Y-m-d H:i:s'),
 
    ];
 
}
 
```
 
 
Then you will also need class `App\Entity\UserTokenRepository` for DB manipulation. Copy and modify the UserRepository. These methods will be handy:
 
 
```php
 
public function findByToken(string $token): ?UserToken
 
{
 
    $tokenEntity = $this->findBy('token', $token);
 
    if (!$tokenEntity) {
 
        return null;
 
    }
 
    if ($tokenEntity->getExpiresAt() < new DateTimeImmutable()) {
 
        // Optionally delete expired token
 
        $this->delete($tokenEntity->getId());
 
        return null;
 
    }
 
    return $tokenEntity;
 
}
 
public function create(int $userId, ?string $token = null, ?DateTimeImmutable $expiresAt = null, $lifespan = '+2 hours'): UserToken
 
{
 
    if (!$token) {
 
        $token = bin2hex(Random::string(32));
 
        // Example: 654367506342505647634a6f4c6945784d793447355048734b364a4e62483743
 
    }
 
    if (!$expiresAt) {
 
        $expiresAt = (new DateTimeImmutable())->modify($lifespan);
 
    }
 
    $entity = new UserToken($userId, $token, $expiresAt);
 
    $this->db->createCommand()
 
        ->insert(self::TABLE_NAME, $entity->toArray())
 
        ->execute();
 
    return $entity;
 
}
 
```
 
 
The User model will need one more method:
 
 
```php
 
// use Yiisoft\Security\PasswordHasher;
 
public function validatePassword(string $password): bool
 
{
 
    return (new PasswordHasher())->validate($password, $this->password_vuejs_hash);
 
}
 
```
 
 
In the end you can create the login action. Register it again in `config/common/routes.php`.
 
 
```php
 
<?php
 
declare(strict_types=1);
 
namespace App\Api;
 
use App\Api\Shared\ResponseFactory;
 
use App\Entity\UserRepository;
 
use App\Entity\UserTokenRepository;
 
use App\Shared\ApplicationParams;
 
use DateTimeImmutable;
 
use Psr\Container\ContainerInterface;
 
use Psr\Http\Message\ResponseInterface;
 
use Psr\Http\Message\ServerRequestInterface;
 
use Yiisoft\DataResponse\DataResponse;
 
use Yiisoft\Http\Status;
 
final class LoginAction
 
{
 
    public function __construct(
 
        private UserRepository      $userRepository,
 
        private UserTokenRepository $userTokenRepository,
 
    ){}
 
    public function __invoke(
 
        ResponseFactory        $responseFactory,
 
        ApplicationParams      $applicationParams,
 
        ContainerInterface     $container,
 
        ServerRequestInterface $request
 
    ): ResponseInterface
 
    {
 
        $data = json_decode((string) $request->getBody(), true);
 
        $username = $data['username'] ?? '';
 
        $password = $data['password'] ?? '';
 
        $user = $this->userRepository->findByUsername($username);
 
        if (!$user || !$user->validatePassword($password)) {
 
            return new DataResponse(['error' => 'Invalid credentials'], Status::UNAUTHORIZED);
 
        }
 
        $this->userTokenRepository->deleteByUserId($user->getId());
 
        $userToken = $this->userTokenRepository->create($user->getId());
 
        return $responseFactory->success([
 
            'token' => $userToken->getToken(),
 
            'expires_at' => $userToken->getExpiresAt()->format(DateTimeImmutable::ATOM),
 
        ]);
 
    }
 
}
 
 
```
 
 
Next we also need an algorithm that will enforce these tokens in each request, will validate and refresh them and will restrict access only to endpoints that the user can use. This is a bigger topic for later. It may be covered by the package https://github.com/yiisoft/auth/ which offers "HTTP bearer authentication".
 
 
## JS client - Installable Vuejs3 PWA
 
 
If you create a REST API you may be interested in a JS frontend that will communicate with it using Ajax. Below you can peek into my very simple VueJS3 attempt. It is an installable PWA application that works in offline mode (=1 data transfer per day, not on every mouse click) and is meant for situations when customer does not have wifi everywhere. See my [Gitlab](https://gitlab.com/radin.cerny/vuejs3-pwa-demo-plus).
1 0
2 followers
Viewed: 621 times
Version: 3.0
Category: Tutorials
Tags:
Written by: rackycz rackycz
Last updated by: rackycz rackycz
Created on: Oct 8, 2025
Last updated: 2 days ago
Update Article

Revisions

View all history