PHP code example of phpmystic / typic

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);
}

/** @extends DataLoader<int, list<Comment>> */
class CommentsByPostLoader extends DataLoader
{
    protected function batch(array $postIds): iterable
    {
        return $this->groupBy(Comment::wherePostIdIn($postIds), 'postId');   // [postId => list<Comment>]
    }
}

/** @return list<Comment> */
#[ResolveField(target: Post::class, arrayOf: Comment::class)]
public function comments(Post $post, CommentsByPostLoader $comments): array
{
    return $comments->get($post->id);
}

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
}

#[Mutation]
public function avatar(
    #[Assert\MaxFileSize(2 * 1024 * 1024)]
    #[Assert\MimeType('image/png', 'image/jpeg')]
    UploadedFileInterface $file,
): bool {
    $file->moveTo('/srv/uploads/' . bin2hex(random_bytes(16)));
    return true;
}

use Typic\Subscription\SubscriptionOptions;

$server = Typic::builder()
    ->scanDirectories([__DIR__ . '/GraphQL'])
    ->withCache(__DIR__ . '/var/typic')
    ->withQueryLimits(maxDepth: 8, maxComplexity: 200)
    ->withIntrospection(false)
    ->withMaxRequestSize(1024 * 1024)
    ->withLogger($psr3Logger)
    ->withSubscriptions(options: new SubscriptionOptions(
        keepAlive: 30,
        connectionInitTimeout: 5,
        maxSubscriptionsPerConnection: 100,
        onConnectionInit: static fn (array $params, $request): bool => $tokens->valid($params['token'] ?? null),
    ))
    ->build();