tobento / app-card
App card support.
Requires
- php: >=8.0
- tobento/app: ^1.0.7
- tobento/app-http: ^1.0
- tobento/app-migration: ^1.0
- tobento/app-view: ^1.0
- tobento/service-collection: ^1.0
- tobento/service-support: ^1.0
Requires (Dev)
- phpunit/phpunit: ^9.5
- tobento/app-testing: ^1.0
- tobento/service-container: ^1.0
- vimeo/psalm: ^4.0
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>