1. Go to this page and download the library: Download phpmystic/typic library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
phpmystic / typic example snippets
use Typic\Application;
use Typic\Subscription\Driver\Workerman\WorkermanDriver;
$app = new Application([
// Interfaces need a binding; concrete services autowire by constructor type.
UserRepository::class => DI\autowire(SqliteUserRepository::class),
]);
$app->builder()
->scanDirectories([__DIR__ . '/src/GraphQL'])
->withGraphiQL()
->withSubscriptions(driver: new WorkermanDriver(port: 8000));
$app->serve();
namespace App\GraphQL\Types;
use Typic\Attribute\ObjectType;
use Typic\Attribute\Field;
use Typic\Attribute\ID;
#[ObjectType(name: 'Post', description: 'A blog post')]
class Post
{
#[Field]
#[ID]
public int $id;
#[Field]
public string $title;
#[Field]
public ?string $content = null;
public int $authorId;
public function __construct(int $id, string $title, ?string $content, int $authorId)
{
$this->id = $id;
$this->title = $title;
$this->content = $content;
$this->authorId = $authorId;
}
// Computed field directly on the type
#[Field]
public function snippet(): string
{
return mb_substr($this->content ?? '', 0, 100) . '...';
}
}
namespace App\GraphQL\Inputs;
use Typic\Attribute\InputType;
use Typic\Attribute\Field;
#[InputType]
readonly class PostMetadataInput
{
public function __construct(
#[Field] public ?string $seoDescription = null,
#[Field(arrayOf: 'string')] public array $tags = [],
) {}
}
#[InputType]
readonly class CreatePostInput
{
public function __construct(
#[Field] public string $title,
#[Field] public string $content,
#[Field] public ?PostMetadataInput $metadata = null,
) {}
}
namespace App\GraphQL\Resolvers;
use App\GraphQL\Types\Post;
use App\GraphQL\Inputs\CreatePostInput;
use Typic\Attribute\Query;
use Typic\Attribute\Mutation;
use Typic\Attribute\Arg;
use Typic\Pagination\Paginator;
class PostResolver
{
/**
* Inferred GraphQL:
* posts(published: Boolean): [Post!]!
*/
#[Query(arrayOf: Post::class)]
public function posts(?bool $published = null): array
{
return Post::query()->when($published !== null, fn ($q) => $q->where('published', $published))->get()->all();
}
/**
* Inferred GraphQL:
* paginatedPosts(page: Int = 1, perPage: Int = 15): PostPaginator!
*/
#[Query(arrayOf: Post::class)]
public function paginatedPosts(
#[Arg(defaultValue: 1)] int $page = 1,
#[Arg(defaultValue: 15)] int $perPage = 15,
): Paginator {
$paginator = Post::paginate($perPage, page: $page);
return Paginator::from($paginator->items(), $page, $perPage, $paginator->total());
}
/**
* Inferred GraphQL:
* createPost(input: CreatePostInput!): Post!
*/
#[Mutation]
public function createPost(CreatePostInput $input): Post
{
// $input is fully hydrated with nested metadata!
return Post::create([
'title' => $input->title,
'content' => $input->content,
'seo_description' => $input->metadata?->seoDescription,
]);
}
}
namespace App\GraphQL\DataLoaders;
use App\GraphQL\Types\User;
use Typic\DataLoader\DataLoader;
/** @extends DataLoader<int, User|null> */
class UserLoader extends DataLoader
{
protected function batch(array $ids): iterable
{
return $this->keyBy(User::findMany($ids), 'id'); // [id => User]
}
}
#[ResolveField(target: Post::class)]
public function author(Post $post, UserLoader $users): ?User
{
return $users->get($post->authorId);
}
use Typic\Typic;
$server = Typic::builder()
->scanDirectories([__DIR__ . '/GraphQL'])
->withContainer($container)
->withSandbox(true)
->build();
// In any PSR-15 handler or controller:
$response = $server->handle($psr7Request);
$result = $server->execute(
query: '{ posts { id title snippet author { name } } }'
);
namespace App\GraphQL\Resolvers;
use App\GraphQL\Types\Comment;
use Typic\Attribute\ID;
use Typic\Attribute\Payload;
use Typic\Attribute\Subscription;
class CommentSubscriptionResolver
{
#[Subscription(name: 'commentAdded', type: Comment::class, topic: 'COMMENT_ADDED')]
public function commentAdded(#[ID] ?int $postId = null, #[Payload] ?Comment $payload = null): bool
{
return $postId === null || $payload?->postId === $postId;
}
}
use Typic\Subscription\Driver\Workerman\WorkermanDriver;
use Typic\Typic;
$server = Typic::builder()
->scanDirectories([__DIR__ . '/GraphQL'])
->withSubscriptions(driver: new WorkermanDriver(host: '0.0.0.0', port: 4000))
->build();
// HTTP: http://localhost:4000/graphql, WebSocket: ws://localhost:4000/graphql
$server->serve();
use Typic\Subscription\PubSub\InMemoryPubSub;
use Typic\Subscription\PubSub\RedisPubSub;
// Custom Redis server or channel prefix
->withSubscriptions(pubSub: RedisPubSub::fromUrl('redis://user:[email protected]:6379', prefix: 'shop:'))
// No Redis: single process only, so keep workerCount at 1
->withSubscriptions(pubSub: new InMemoryPubSub())
// 1. GraphiQL (Native DOM, zero iframes, direct WebSockets)
$server = Typic::builder()
->withGraphiQL()
->build();
// 2. Altair GraphQL Client (Native DOM, multi-tab, zero iframes)
$server = Typic::builder()
->withAltair()
->build();
// 3. Apollo Sandbox (Official Apollo Studio embed)
$server = Typic::builder()
->withSandbox()
->build();
use GraphQL\Error\UserError;
use Typic\Middleware\ResolverCall;
use Typic\Middleware\ResolverMiddleware;
final class Authenticated implements ResolverMiddleware
{
public function __construct(private Users $users) {}
public function handle(ResolverCall $call, callable $next): mixed
{
// A header over HTTP; WebSocket clients usually send the token in connection_init
$token = $call->request()?->getHeaderLine('Authorization')
?: ($call->connectionParams()['Authorization'] ?? '');
$user = $this->users->fromToken($token) ?? throw new UserError('You must be logged in.');
return $next($call->with('user', $user));
}
}
#[Middleware(Authenticated::class)]
class PostResolver
{
#[Mutation]
#[Middleware(new Can('posts.publish'))]
#[Middleware(new RateLimit(perMinute: 10))]
public function publishPost(#[ID] int $id, #[FromCall('user')] User $user): Post { /* ... */ }
#[Query]
#[WithoutMiddleware(Authenticated::class)]
public function publicFeed(): array { /* ... */ }
}
$server = Typic::builder()
->withResolverMiddleware(new LogSlowFields(thresholdMs: 50))
->withMiddlewareGroup('admin', [Authenticated::class, new Can('admin'), AuditLog::class])
->build();
// #[Middleware('admin')] on a class or method
use Typic\Attribute\FromRequest;
$server = Typic::builder()
->withMiddleware(new TokenAuthentication($users), RequestLogger::class)
->withCors(origins: ['https://app.example.com'], credentials: true)
->build();
// TokenAuthentication sets $request->withAttribute('user', $user)
#[Query]
public function me(#[FromRequest('user')] ?User $user): ?User
{
return $user;
}
use Typic\Attribute\Scalar;
use Typic\Scalar\ScalarValue;
#[Scalar(description: 'An email address')]
final readonly class Email implements ScalarValue
{
public function __construct(public string $value) {}
public static function parse(mixed $value): static
{
if (!is_string($value) || filter_var($value, FILTER_VALIDATE_EMAIL) === false) {
throw new InvalidArgumentException('Invalid email address.');
}
return new self($value);
}
public function serialize(): string { return $this->value; }
}
use Typic\Validation as Assert;
#[InputType]
final readonly class SignupInput
{
public function __construct(
#[Assert\NotBlank] #[Assert\Length(max: 32)] public string $name,
#[Assert\Email] public string $email,
#[Assert\Range(min: 13)] public int $age,
#[Field(arrayOf: 'string')]
#[Assert\Distinct(ignoreCase: true)]
#[Assert\Each(new Assert\Length(max: 20))] public array $tags = [],
) {}
}
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
final class NotReserved implements Rule
{
public function check(mixed $value): ?Violation
{
return in_array($value, ['admin', 'root'], true) ? new Violation('RESERVED', 'is reserved') : null;
}
}
use Typic\Input\PresenceAware;
use Typic\Input\TracksPresence;
#[InputType]
final class UpdateProfileInput implements PresenceAware
{
use TracksPresence;
public function __construct(
#[Field(nullable: true)] public ?string $name,
#[Field(nullable: true)] public ?string $bio,
) {}
}
if ($input->has('bio')) {
$profile->bio = $input->bio; // null clears it
}