This site is built with Viewi itself. It is experimental and still in development. If you see any bugs please do not hesitate and open an issue or DM me on Twitter.

Tour of Heroes

Step 10 - Saving hero changes

What about saving a hero. Let's add an API endpoint to handle that:

Router::register('put', '/api/heroes/{id}', function (int $id) {
    // read the data
    $inputContent = file_get_contents('php://input');
    // parse
    $stdObject = json_decode($inputContent, false);
    // convert type
    $hero = JsonMapper::Instantiate(HeroModel::class, $stdObject);
    $repository = new Repository(HeroModel::class);
    return $repository->Update($id, $hero);
});

Here we read the request's body and encode it into the object. And then convert it to the HeroModel using Viewi\Common\JsonMapper . It simply copies properties from one object to another if they exist. For production, you may need to consider using more advanced tools for data mapping.

Now let's add a save button and a handler in our HeroDetail component:

viewi-app\Components\Views\HeroDetail\HeroDetail.html

<Layout title="{$hero ? $hero->Name : ''} details">
    <div if="$hero">
        <h2>{strtoupper($hero->Name)} Details</h2>
        <div><span>id: </span>{$hero->Id}</div>
        <div>
            <label for="hero-name">Hero name: </label>
            <input id="hero-name" model="$hero->Name" placeholder="name">
        </div>
        <button (click)="GoBack()">Back</button>
        <button (click)="Save()">Save</button>
    </div>
</Layout>

viewi-app\Components\Views\HeroDetail\HeroDetail.php

<?php

namespace Components\Views\HeroDetail;

use Components\Models\HeroModel;
use Components\Services\HeroService;
use Viewi\BaseComponent;
use Viewi\Common\ClientRouter;

class HeroDetail extends BaseComponent
{
    public ?HeroModel $hero = null;
    private ClientRouter $router;
    private HeroService $heroService;

    public function __init(HeroService $heroService, ClientRouter $router, int $id)
    {
        $this->heroService = $heroService;
        $heroService->GetHero($id, function (?HeroModel $hero) {
            $this->hero = $hero;
        });
        $this->router = $router;
    }

    public function GoBack()
    {
        $this->router->navigateBack();
    }

    public function Save()
    {
        $this->heroService->Update($this->hero, function () {
            $this->router->navigateBack();
        });
    }
}

Now we need to add the Update method to our HeroService.

public function Update(HeroModel $hero, callable $callback)
{
    $this->messageService->Add("HeroService: updating hero id={$hero->Id}");
    $this->http->put("/api/heroes/{$hero->Id}", $hero)->then(function () use ($hero, $callback) {
        $this->messageService->Add("HeroService: updated hero id={$hero->Id}");
        $callback();
    }, function ($error) {
        $this->messageService->Add('HeroService: error has occurred. ' . json_encode($error));
    });
}

Now we should be able to save the changes on a server.

Create and Delete

By following the same pattern, let's update our application with Create and Delete functionality:

viewi-app\Components\Services\HeroService.php

<?php

namespace Components\Services;

use Components\Models\HeroModel;
use Viewi\Common\HttpClient;

class HeroService
{
    private HttpClient $http;
    private MessageService $messageService;

    public function __construct(HttpClient $http, MessageService $messageService)
    {
        $this->http = $http;
        $this->messageService = $messageService;
    }

    public function GetHeroes(callable $callback)
    {
        $this->messageService->Add('HeroService: fetching heroes');
        $this->http->get('/api/heroes')->then(function (array $heroes) use ($callback) {
            $this->messageService->Add('HeroService: fetched heroes');
            $callback($heroes);
        }, function ($error) {
            $this->messageService->Add('HeroService: error has occurred. ' . json_encode($error));
        });
    }

    public function GetHero(int $id, callable $callback)
    {
        $this->messageService->Add("HeroService: fetching hero id={$id}");
        $this->http->get("/api/heroes/{$id}")->then(function (?HeroModel $hero) use ($id, $callback) {
            $this->messageService->Add("HeroService: fetched hero id={$id}");
            $callback($hero);
        }, function ($error) {
            $this->messageService->Add('HeroService: error has occurred. ' . json_encode($error));
        });
    }

    public function Update(HeroModel $hero, callable $callback)
    {
        $this->messageService->Add("HeroService: updating hero id={$hero->Id}");
        $this->http->put("/api/heroes/{$hero->Id}", $hero)->then(function () use ($hero, $callback) {
            $this->messageService->Add("HeroService: updated hero id={$hero->Id}");
            $callback();
        }, function ($error) {
            $this->messageService->Add('HeroService: error has occurred. ' . json_encode($error));
        });
    }

    public function Create(HeroModel $hero, callable $callback)
    {
        $this->messageService->Add("HeroService: creating hero");
        $this->http->post("/api/heroes", $hero)->then(function (HeroModel $newHero) use ($callback) {
            $this->messageService->Add("HeroService: created hero id={$newHero->Id}");
            $callback($newHero);
        }, function ($error) {
            $this->messageService->Add('HeroService: error has occurred. ' . json_encode($error));
        });
    }

    public function Delete(int $id, callable $callback)
    {
        $this->messageService->Add("HeroService: deleting hero $id");
        $this->http->delete("/api/heroes/$id")->then(function () use ($id, $callback) {
            $this->messageService->Add("HeroService: deleted hero id={$id}");
            $callback();
        }, function ($error) {
            $this->messageService->Add('HeroService: error has occurred. ' . json_encode($error));
        });
    }
}

backend\endpoints.php

<?php

namespace BackendApp;

use Components\Models\HeroModel;
use Viewi\Common\JsonMapper;
use Viewi\Routing\Router;
use Viewi\WebComponents\Response;

include 'repository.php';

// API
Router::register('get', '/api/heroes', function () {
    $repository = new Repository(HeroModel::class);
    return $repository->Get();
});

Router::register('get', '/api/heroes/{id}', function (int $id) {
    $repository = new Repository(HeroModel::class);
    return $repository->GetById($id);
});

Router::register('post', '/api/heroes', function () {
    // read the data
    $inputContent = file_get_contents('php://input');
    // parse
    $stdObject = json_decode($inputContent, false);
    // convert type
    $hero = JsonMapper::Instantiate(HeroModel::class, $stdObject);
    $repository = new Repository(HeroModel::class);
    return $repository->Create($hero);
});

Router::register('put', '/api/heroes/{id}', function () {
    // read the data
    $inputContent = file_get_contents('php://input');
    // parse
    $stdObject = json_decode($inputContent, false);
    // convert type
    $hero = JsonMapper::Instantiate(HeroModel::class, $stdObject);
    $repository = new Repository(HeroModel::class);
    return $repository->Update($hero);
});

Router::register('delete', '/api/heroes/{id}', function (int $id) {
    $repository = new Repository(HeroModel::class);
    return $repository->Delete($id);
});

// 404 
Router::register('*', '/api/*', function () {
    return Response::Json([
        "message" => "Not Found"
    ])->WithCode(404);
});

viewi-app\Components\Views\Heroes\Heroes.php

<?php

namespace Components\Views\Heroes;

use Components\Models\HeroModel;
use Components\Services\HeroService;
use Components\Services\MessageService;
use Viewi\BaseComponent;

class Heroes extends BaseComponent
{
    /**
     * 
     * @var HeroModel[]
     */
    public array $heroes;
    public string $heroName = '';
    private HeroService $heroService;

    public function __init(HeroService $heroService, MessageService $messageService)
    {
        $this->heroService = $heroService;
        $this->messageService = $messageService;
        $this->ReadHeroes();
    }

    public function ReadHeroes()
    {
        $this->heroService->GetHeroes(function (array $heroes) {
            $this->heroes = $heroes;
        });
    }

    public function Add()
    {
        if (strlen($this->heroName)) {
            $hero = new HeroModel();
            $hero->Name = $this->heroName;
            $this->heroService->Create($hero, function () {
                $this->heroName = '';
                $this->ReadHeroes();
            });
        }
    }

    public function Delete(HeroModel $hero)
    {
        $this->heroService->Delete($hero->Id, function () {
            $this->ReadHeroes();
        });
    }
}

viewi-app\Components\Views\Heroes\Heroes.html

<Layout title="My Heroes">
    <h2>My Heroes</h2>
    <div>
        <label for="new-hero">Hero name: </label>
        <input id="new-hero" model="$heroName" />
        <button class="add-button" (click)="Add()">
            Add hero
        </button>
    </div>
    <ul class="heroes">
        <li foreach="$heroes as $hero">
            <a href="/detail/{$hero->Id}"><span class="badge">{$hero->Id}</span> {$hero->Name}</a>
            <button class="delete" title="delete hero" (click)="Delete($hero)">x</button>
        </li>
    </ul>
</Layout>

After refreshing the page, you can read, update, create, and delete heroes.

I hope you liked this tutorial.

Feel free to contact me if you have any questions or find a bug.