tobento/app-card

1.0.0 2025-04-11 17:11 UTC

This package is auto-updated.

Last update: 2025-04-11 17:15:43 UTC


README

The card app provides interfaces to create cards to be displayed on a dashboard page for instance. It comes with a default implementation and basic cards.

Table of Contents

Getting Started

Add the latest version of the app card project running this command.

composer require tobento/app-card

Requirements

  • PHP 8.0 or greater

Documentation

App

Check out the App Skeleton if you are using the skeleton.

You may also check out the App to learn more about the app in general.

Card Boot

The card boot does the following:

  • migrates card config, view and asset files
  • implements cards interfaces based on the card config file
use Tobento\App\AppFactory;
use Tobento\App\Card\CardsInterface;
use Tobento\App\Card\FilterInputInterface;

// Create the app
$app = (new AppFactory())->createApp();

// Add directories:
$app->dirs()
    ->dir(realpath(__DIR__.'/../'), 'root')
    ->dir(realpath(__DIR__.'/../app/'), 'app')
    ->dir($app->dir('app').'config', 'config', group: 'config')
    ->dir($app->dir('root').'public', 'public')
    ->dir($app->dir('root').'vendor', 'vendor');

// Adding boots:
$app->boot(\Tobento\App\Card\Boot\Card::class);
$app->booting();

// Implemented interfaces:
$cards = $app->get(CardsInterface::class);
$filterInput = $app->get(FilterInputInterface::class);

// Run the app
$app->run();

Card Config

The configuration for the card is located in the app/config/card.php file at the default App Skeleton config location where you can configure the implemented card interfaces for your application.

Cards

Adding Cards

You may use the implemented CardsInterface::class adding cards to display them later in your view files or you may create your own cards specific to a resource.

Furthermore, it is recommended to use the App on method to add cards only if requested.

use Tobento\App\Card\Card;
use Tobento\App\Card\CardInterface;
use Tobento\App\Card\CardsInterface;
use Tobento\App\Card\Factory;
use Tobento\Service\View\ViewInterface;

$app->on(
    CardsInterface::class,
    static function(CardsInterface $cards): void {
        // Using a card factory:
        $cards->add(name: 'foo', card: new Factory\Table(
            rows: [['foo']],
        ));
        
        // Using a callable being autowired:
        $cards->add(
            name: 'foo',
            card: static function (string $name, ViewInterface $view): CardInterface {
                return new Card\Table(
                    view: $view,
                    rows: [['foo']],
                );
            }
        );
        
        // Using a card instance:
        $cards->add(name: 'foo', card: new SomeCard());
        
        // Using a card class string being autowired:
        $cards->add(name: 'foo', card: SomeCard::class);
    }
);

Check out the Available Cards or Available Card Factories.

Cards Methods

Filter Methods

You may filter added cards using the following methods returning a new instance:

use Tobento\App\Card\CardInterface;

// Returns a new instance with the filtered cards.
$cards = $cards->filter(fn(CardInterface $c): bool => $c->priority() > 1);

// Returns a new instance with the specified group filtered.
$cards = $cards->group(name: 'name');

// Returns a new instance with only the card(s) specified.
$cards = $cards->only('foo', 'bar');

// Returns a new instance except the specified card(s).
$cards = $cards->except('foo', 'bar');

Retrieving Methods

// Returns true if card exists, otherwise false.
$cards->has(name: 'foo');

// Returns a card by name or null if not exists.
$card = $cards->get(name: 'foo');

// Returns all card names.
$cardNames = $cards->names();

// Returns the number of cards (int).
$numberOfCards = $cards->count();

// Returns all cards.
$cards = $cards->all(); // array<string, CardInterface>

// Iterating cards.
foreach($cards as $card) {}

Displaying Cards In Views

In your view file, use the render method to display the cards:

<!DOCTYPE html>
<html lang="<?= $view->esc($view->get('htmlLang', 'en')) ?>">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Demo Cards</title>
        <?= $view->render('inc/head') ?>
        <?= $view->assets()->render() ?>
        <?php
        // you may add the card css, otherwise you need to write your custom css for your cards.
        $view->asset('assets/card/card.css');
        
        // add if you have or support filterable cards:
        $view->asset('assets/card/card.js')->attr('type', 'module');
        ?>
    </head>
    <body<?= $view->tagAttributes('body')->add('class', 'page')->render() ?>>
        <?= $view->render('inc/header') ?>
        <?= $view->render('inc/nav') ?>
        <main class="page-main">
            <?= $view->render('inc.breadcrumb') ?>
            <?= $view->render('inc.messages') ?>
            <h1 class="text-xl">Demo Cards</h1>
            
            <div class="cards">
                <?php
                foreach($cards as $card) {
                    echo $card->render();
                }
                ?>
            </div>
        </main>
        <?= $view->render('inc/footer') ?>
    </body>
</html>

Check out the App View to learn more about it.

Creating Specific Cards

Instead of using the implemented CardsInterface::class to add cards, you may create cards specific to a resource by simply extend the Cards:::class:

use Tobento\App\Card\Cards;

class DashboardCards extends Cards
{
    //
}

Adding cards

use Tobento\App\Card\Factory;

$app->on(
    DashboardCards::class,
    static function(DashboardCards $cards): void {
        // Adding cards:
        $cards->add(name: 'foo', card: new Factory\Table(
            rows: ['foo', 'bar'],
        ));
    }
);

Available Cards

Group Card

The Group card may be used to group cards being displayed in smaller sizes:

use Psr\Container\ContainerInterface;
use Tobento\App\Card\Card;
use Tobento\App\Card\CardFactoryInterface;
use Tobento\App\Card\CardInterface;
use Tobento\Service\View\ViewInterface;

$card = new Card\Group(
    container: $container, // ContainerInterface
    view: $view, // ViewInterface
    
    cards: [], // array<array-key, CardInterface|CardFactoryInterface>
    
    // You may set a title:
    title: 'A title',
    
    // You may set a group to be later filtered:
    group: 'main',
    
    // You may set a priority:
    priority: 100,
);

You may use the Group Card Factory to add the card.

Html Card

The Html card may be used to add HTML:

use Tobento\App\Card\Card;
use Tobento\Service\View\ViewInterface;

$card = new Card\Html(
    view: $view, // ViewInterface
    
    // The html Must be escaped.
    html: 'html', // string|\Stringable
    
    // You may set a title:
    title: 'A title',
    
    // You may set a group to be later filtered:
    group: 'main',
    
    // You may set a priority:
    priority: 100,
);

You may use the Html Card Factory to add the card.

KeyedList Card

The KeyedList card may be used to add items being displayed as a keyed list:

use Tobento\App\Card\Card;
use Tobento\Service\View\ViewInterface;

$card = new Card\KeyedList(
    view: $view, // ViewInterface
    
    items: [
        'key' => 'value',
    ],
    
    // You may set a title:
    title: 'A title',
    
    // You may set a group to be later filtered:
    group: 'main',
    
    // You may set a priority:
    priority: 100,
);

You may use the KeyedList Card Factory to add the card.

Table Card

The Table card may be used to add items being displayed in a table:

use Tobento\App\Card\Card;
use Tobento\App\Card\Renderable\Link;
use Tobento\Service\View\ViewInterface;

$card = new Card\Table(
    view: $view, // ViewInterface
    
    // You may add table headers:
    headers: ['Description', 'Action'],
    
    // Add table rows:
    rows: [
        ['Desc', new Link(url: 'Url', label: 'Label', attributes: ['class' => 'button'])],
    ],
    
    // You may set a title:
    title: 'A title',
    
    // You may set a group to be later filtered:
    group: 'main',
    
    // You may set a priority:
    priority: 100,
);

You may use the Table Card Factory to add the card.

Available Card Factories

Group Card Factory

The Group card factory creates a Group Card:

use Tobento\App\Card\CardFactoryInterface;
use Tobento\App\Card\CardInterface;
use Tobento\App\Card\Factory;

$factory = new Factory\Group(
    cards: [], // array<array-key, CardInterface|CardFactoryInterface>
    
    // You may set a title:
    title: 'A title',
    
    // You may set a group to be later filtered:
    group: 'main',
    
    // You may set a priority:
    priority: 100,
);

Html Card Factory

The Html card factory creates a Html Card:

use Tobento\App\Card\Factory;

$factory = new Factory\Html(
    // The html Must be escaped.
    html: 'html', // string|\Stringable
    
    // You may set a title:
    title: 'A title',
    
    // You may set a group to be later filtered:
    group: 'main',
    
    // You may set a priority:
    priority: 100,
);

KeyedList Card Factory

The KeyedList card factory creates a KeyedList Card:

use Tobento\App\Card\Factory;

$factory = new Factory\KeyedList(
    items: [
        'key' => 'value',
    ],
    
    // You may set a title:
    title: 'A title',
    
    // You may set a group to be later filtered:
    group: 'main',
    
    // You may set a priority:
    priority: 100,
);

Table Card Factory

The Table card factory creates a Table Card:

use Tobento\App\Card\Factory;
use Tobento\App\Card\Renderable\Link;

$factory = new Factory\Table(
    // You may add table headers:
    headers: ['Description', 'Action'],
    
    // Add table rows:
    rows: [
        ['Desc', new Link(url: 'Url', label: 'Label', attributes: ['class' => 'button'])],
    ],
    
    // You may set a title:
    title: 'A title',
    
    // You may set a group to be later filtered:
    group: 'main',
    
    // You may set a priority:
    priority: 100,
);

Filterable Cards

You may create filterable cards by using the implemented FilterInputInterface storing filtered data in cookies (default) using the App Http Cookies Boot. You may consider encrypting cookies in addition.

use Tobento\App\Card\CardInterface;
use Tobento\App\Card\FilterInputInterface;
use Tobento\Service\View\ViewInterface;

final class OrdersCard implements CardInterface
{
    public function __construct(
        private ViewInterface $view,
        private OrderRepository $orderRepository,
        private FilterInputInterface $input,
        private string $title = '',
        private string $group = '',
        private int $priority = 0,
    ) {}

    public function group(): string
    {
        return $this->group;
    }

    public function priority(): int
    {
        return $this->priority;
    }
    
    public function render(): string
    {
        return $this->view->render('card/orders', ['card' => $this]);
    }
    
    public function statuses(): array
    {
        return ['paid' => 'Paid', 'unpaid' => 'Unpaid'];
    }
    
    public function activeStatus(): string
    {
        $value = $this->input->get('order.status', '');
        
        // validate input as data may come from user input:
        if (in_array($value, array_keys($this->statuses()))) {
            return $value;
        }
        
        return 'paid';
    }
    
    public function getOrders(): array
    {
        return $this->orderRepository->findAll(
            where: [
                'status' => $this->activeStatus(),
            ],
            limit: 15,
        );
    }
    
    public function title(): string
    {
        return $this->title;
    }
}

The card/orders view file:

Make sure the names of the form elements are like card[name]. In addition, make sure you have added the card.js asset in your main view which will update the cards content while filtering. See Displaying Cards In Views

This example uses Form Service which can be integrated by the Form Boot.

<div class="card">
    <?php if ($card->title()) { ?>
        <div class="card-head"><?= $view->esc($card->title()) ?></div>
    <?php } ?>
    <div class="card-body">
        <div class="card-filters">
            <?php
            $form = $view->form();
            echo $form->form(attributes: [
                'action' => '',
                'method' => 'GET',
                'data-card-filter' => 'orders',
            ]);
            echo $form->select(
                name: 'card.order.status',
                items: $card->statuses(),
                selected: $card->activeStatus(),
                selectAttributes: ['class' => 'small'],
            );
            echo $form->button(
                text: 'Filter',
                attributes: ['class' => 'display-none-if-js'],
                escText: true,
            );
            echo $form->close();
            ?>
        </div>
        <div data-card-content="orders">
            <?php
            foreach($card->getOrders() as $order) {
                // show order ...
            }
            ?>
        </div>
    </div>
</div>

Credits