mcpField
A
appwrite-php

appwrite-php

github.com/appwriteDevelopment28851 installsUpdated 1w ago
Repository

About appwrite-php

appwrite-php is an SDK for integrating Appwrite into PHP applications, aimed at server-side PHP developers. It provides user management, TablesDB queries, file and permission handling, teams, and function execution, with SSR authentication examples.

Key features

  1. 01User management
  2. 02TablesDB operations and queries
  3. 03File storage and permissions
  4. 04Team management
  5. 05Appwrite function execution

Use cases

  1. 01Manage Appwrite users from a PHP server
  2. 02Create and query data in TablesDB
  3. 03Handle Appwrite session authentication in an SSR app

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/appwrite/skills --skill appwrite-php

After installing, type/appwrite-phpin Claude Code to confirm it loaded.

Details

From the project README

Appwrite PHP SDK

Installation

composer require appwrite/appwrite

Setting Up the Client

use Appwrite\Client;
use Appwrite\ID;
use Appwrite\Query;
use Appwrite\Services\Users;
use Appwrite\Services\TablesDB;
use Appwrite\Services\Storage;
use Appwrite\Services\Functions;
use Appwrite\InputFile;

$client = (new Client())
    ->setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
    ->setProject(getenv('APPWRITE_PROJECT_ID'))
    ->setKey(getenv('APPWRITE_API_KEY'));

Code Examples

User Management
$users = new Users($client);

// Create user
$user = $users->create(ID::unique(), '[email protected]', null, 'password123', 'User Name');

// List users
$list = $users->list([Query::limit(25)]);

// Get user
$fetched = $users->get('[USER_ID]');

// Delete user
$users->delete('[USER_ID]');
Database Operations

Note: Use TablesDB (not the deprecated Databases class) for all new code. Only use Databases if the existing codebase already relies on it or the user explicitly requests it.

Tip: Prefer named arguments (PHP 8+, e.g., databaseId: '...') for all SDK method calls. Only use positional arguments if the existing codebase already uses them or the user explicitly requests it.

$tablesDB = new TablesDB($client);

// Create database
$db = $tablesDB->create(ID::unique(), 'My Database');

// Create row
$doc = $tablesDB->createRow('[DATABASE_ID]', '[TABLE_ID]', ID::unique(), [
    'title' => 'Hello World'
]);

// Query rows
$results = $tablesDB->listRows('[DATABASE_ID]', '[TABLE_ID]', [
    Query::equal('title', ['Hello World']),
    Query::limit(10)
]);

// Get row
$row = $tablesDB->getRow('[DATABASE_ID]', '[TABLE_ID]', '[ROW_ID]');

// Update row
$tablesDB->updateRow('[DATABASE_ID]', '[TABLE_ID]', '[ROW_ID]', [
    'title' => 'Updated'
]);

// Delete row
$tablesDB->deleteRow('[DATABASE_ID]', '[TABLE_ID]', '[ROW_ID]');
String Column Types

Note: The legacy string type is deprecated. Use explicit column types for all new columns.

TypeMax charactersIndexingStorage
varchar16,383Full index (if size ≤ 768)Inline in row
text16,383Prefix onlyOff-page
mediumtext4,194,303Prefix onlyOff-page
longtext1,073,741,823Prefix onlyOff-page
  • varchar is stored inline and counts towards the 64 KB row size limit. Prefer for short, indexed fields like names, slugs, or identifiers.
  • text, mediumtext, and longtext are stored off-page (only a 20-byte pointer lives in the row), so they don't consume the row size budget. size is not required for these types.
// Create table with explicit string column types
$tablesDB->createTable('[DATABASE_ID]', ID::unique(), 'articles', [
    ['key' => 'title',    'type' => 'varchar',    'size' => 255, 'required' => true],
    ['key' => 'summary',  'type' => 'text',                      'required' => false],
    ['key' => 'body',     'type' => 'mediumtext',                'required' => false],
    ['key' => 'raw_data', 'type' => 'longtext',                  'required' => false],
]);
Query Methods
// Filtering
Query::equal('field', ['value'])            // == (always pass array)
Query::notEqual('field', ['value'])         // !=
Query::lessThan('field', 100)              // <
Query::lessThanEqual('field', 100)         // <=
Query::greaterThan('field', 100)           // >
Query::greaterThanEqual('field', 100)      // >=
Query::between('field', 1, 100)            // 1 <= field <= 100
Query::isNull('field')                     // is null
Query::isNotNull('field')                  // is not null
Query::startsWith('field', 'prefix')       // starts with
Query::endsWith('field', 'suffix')         // ends with
Query::contains('field', ['sub'])          // contains (string or array)
Query::search('field', 'keywords')         // full-text search (requires index)

// Sorting
Query::orderAsc('field')
Query::orderDesc('field')

// Pagination
Query::limit(25)                           // max rows (default 25, max 100)
Query::offset(0)                           // skip N rows
Query::cursorAfter('[ROW_ID]')             // cursor pagination (preferred)
Query::cursorBefore('[ROW_ID]')

// Selection & Logic
Query::select(['field1', 'field2'])        // return only specified fields
Query::or([Query::equal('a', [1]), Query::equal('b', [2])])   // OR
Query::and([Query::greaterThan('age', 18), Query::lessThan('age', 65)])  // AND (default)
Query::relationship('comments', [Query::equal('approved', [true]), Query::limit(25)])  // related documents

// Aggregations (optional alias)
Query::count('*', 'total')
Query::sum('amount', 'revenue')
Query::avg('score', 'avgScore')
Query::min('price', 'minPrice')
Query::max('price', 'maxPrice')

// Grouping
Query::groupBy(['status', 'year'])
Query::having([Query::greaterThan('total', 1)])
Query::distinct()

// Joins (table id, left key, right key, operator, alias)
Query::join('orders', '$id', 'customerId')
Query::leftJoin('orders', '$id', 'customerId', '=', 'ord')

// Nested join ON: extra predicates stay on the JOIN, not WHERE
Query::leftJoin('orders', 'ord', [
    Query::on('$id', 'customerId'),
    Query::equal('ord.status', 'paid'),
])

// Fluent builder. Pass build() into list methods.
$queries = Query::builder()
    ->equal('status', 'published')
    ->leftJoin('orders', '$id', 'customerId', '=', 'ord')
    ->count('*', 'total')
    ->groupBy(['status'])
    ->limit(25)
    ->build();

$tablesDB->listRows('[DATABASE_ID]', '[TABLE_ID]', $queries);
File Storage
$storage = new Storage($client);

// Upload file
$file = $storage->createFile('[BUCKET_ID]', ID::unique(), InputFile::withPath('/path/to/file.png'));

// List files
$files = $storage->listFiles('[BUCKET_ID]');

// Delete file
$storage->deleteFile('[BUCKET_ID]', '[FILE_ID]');
InputFile Factory Methods
use Appwrite\InputFile;

InputFile::withPath('/path/to/file.png')                    // from filesystem path
InputFile::withData('Hello world', 'hello.txt')             // from string content
Teams
$teams = new Teams($client);

// Create team
$team = $teams->create(ID::unique(), 'Engineering');

// List teams
$list = $teams->list();

// Create membership (invite user by email)
$membership = $teams->createMembership('[TEAM_ID]', ['editor'], email: '[email protected]');

// List memberships
$members = $teams->listMemberships('[TEAM_ID]');

// Update membership roles
$teams->updateMembership('[TEAM_ID]', '[MEMBERSHIP_ID]', ['admin']);

// Delete team
$teams->delete('[TEAM_ID]');

Role-based access: Use Role::team('[TEAM_ID]') for all team members or Role::team('[TEAM_ID]', 'editor') for a specific team role when setting permissions.

Serverless Functions
$functions = new Functions($client);

// Execute function
$execution = $functions->createExecution('[FUNCTION_ID]', '{"key": "value"}');

// List executions
$executions = $functions->listExecutions('[FUNCTION_ID]');
Writing a Function Handler (PHP runtime)
// src/main.php — Appwrite Function entry point
return function ($context) {
    // $context->req->body        — raw body (string)
    // $context->req->bodyJson    — parsed JSON (array or null)
    // $context->req->headers     — headers (array)
    // $context->req->method      — HTTP method
    // $context->req->path        — URL path
    // $context->req->query       — query params (array)

    $context->log('Processing: ' . $context->req->method . ' ' . $context->req->path);

    if ($context->req->method === 'GET') {
        return $context->res->json(['message' => 'Hello from Appwrite Function!']);
    }

    $data = $context->req->bodyJson ?? [];
    if (!isset($data['name'])) {
        $context->error('Missing name field');
        return $context->res->json(['error' => 'Name is required'], 400);
    }

    return $context->res->json(['success' => true]);      // JSON
    // return $context->res->text('Hello');                // plain text
    // return $context->res->empty();                      // 204
    // return $context->res->redirect('https://...');      // 302
};
Server-Side Rendering (SSR) Authentication

SSR apps (Laravel, Symfony, etc.) use the server SDK to handle auth. You need two clients:

  • Admin client — uses an API key, creates sessions, bypasses rate limits (reusable singleton)
  • Session client — uses a session cookie, acts on behalf of a user (create per-request, never share)
use Appwrite\Client;
use Appwrite\Services\Account;

// Admin client (reusable)
$adminClient = (new Client())
    ->setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
    ->setProject('[PROJECT_ID]')
    ->setKey(getenv('APPWRITE_API_KEY'));

// Session client (create per-request)
$sessionClient = (new Client())
    ->setEndpoint('https://<REGION>.cloud.appwrite.io/v1')
    ->setProject('[PROJECT_ID]');

$session = $_COOKIE['a_session_[PROJECT_ID]'] ?? null;
if ($session) {
    $sessionClient->setSession($session);
}
Email/Password Login
$account = new Account($adminClient);
$session = $account->createEmailPasswordSession($email, $password);

// Cookie name must be a_session_<PROJECT_ID>
setcookie('a_session_[PROJECT_ID]', $session['secret'], [
    'httpOnly' => true,
    'secure' => true,
    'sameSite' => 'strict',
    'expires' => strtotime($session['expire']),
    'path' => '/',
]);
Authenticated Requests
$session = $_COOKIE['a_session_[PROJECT_ID]'] ?? null;
if (!$session) {
    http_response_code(401);
    exit;
}

$sessionClient->setSession($session);
$account = new Account($sessionClient);
$user = $account->get();
OAuth2 SSR Flow
// Step 1: Redirect to OAuth provider
$account = new Account($adminClient);
$redirectUrl = $account->createOAuth2Token(
    OAuthProvider::GITHUB(),
    'https://example.com/oauth/success',
    'https://example.com/oauth/failure',
);
header('Location: ' . $redirectUrl);

// Step 2: Handle callback — exchange token for session
$account = new Account($adminClient);
$session = $account->createSession($_GET['userId'], $_GET['secret']);

setcookie('a_session_[PROJECT_ID]', $session['secret'], [
    'httpOnly' => true, 'secure' => true, 'sameSite' => 'strict',
    'expires' => strtotime($session['expire']), 'path' => '/',
]);

Cookie security: Always use httpOnly, secure, and sameSite: 'strict' to prevent XSS. The cookie name must be a_session_<PROJECT_ID>.

Forwarding user agent: Call $sessionClient->setForwardedUserAgent($_SERVER['HTTP_USER_AGENT']) to record the end-user's browser info for debugging and security.

Error Handling

use Appwrite\AppwriteException;

try {
    $row = $tablesDB->getRow('[DATABASE_ID]', '[TABLE_ID]', '[ROW_ID]');
} catch (AppwriteException $e) {
    echo $e->getMessage();   // human-readable error message
    echo $e->getCode();      // HTTP status code (int)
    echo $e->getType();      // Appwrite error type string (e.g. 'document_not_found')
    echo $e->getResponse();  // full response body (array)
}

Common error codes:

CodeMeaning
401Unauthorized — missing or invalid session/API key
403Forbidden — insufficient permissions for this action
404Not found — resource does not exist
409Conflict — duplicate ID or unique constraint violation
429Rate limited — too many requests, retry after backoff

Permissions & Roles (Critical)

Appwrite uses permission strings to control access to resources. Each permission pairs an action (read, update, delete, create, or write which grants create + update + delete) with a role target. By default, no user has access unless permissions are explicitly set at the row/file level or inherited from the table/bucket settings. Permissions are arrays of strings built with the Permission and Role helpers.

use Appwrite\Permission;
use Appwrite\Role;
Database Row with Permissions
$doc = $tablesDB->createRow('[DATABASE_ID]', '[TABLE_ID]', ID::unique(), [
    'title' => 'Hello World'
], [
    Permission::read(Role::user('[USER_ID]')),     // specific user can read
    Permission::update(Role::user('[USER_ID]')),   // specific user can update
    Permission::read(Role::team('[TEAM_ID]')),     // all team members can read
    Permission::read(Role::any()),                 // anyone (including guests) can read
]);
File Upload with Permissions
$file = $storage->createFile('[BUCKET_ID]', ID::unique(), InputFile::withPath('/path/to/file.png'), [
    Permission::read(Role::any()),
    Permission::update(Role::user('[USER_ID]')),
    Permission::delete(Role::user('[USER_ID]')),
]);

When to set permissions: Set row/file-level permissions when you need per-resource access control. If all rows in a table share the same rules, configure permissions at the table/bucket level and leave row permissions empty.

Common mistakes:

  • Forgetting permissions — the resource becomes inaccessible to all users (including the creator)
  • Role::any() with write/update/delete — allows any user, including unauthenticated guests, to modify or remove the resource
  • Permission::read(Role::any()) on sensitive data — makes the resource publicly readable

Comments

Markdown supported