tribe/core

All classes to run Tribe

Installs: 626

Dependents: 4

Suggesters: 0

Security: 0

Stars: 0

Watchers: 1

Forks: 5

Open Issues: 0

pkg:composer/tribe/core

v5.1.2 2026-02-19 07:18 UTC

This package is auto-updated.

Last update: 2026-02-21 08:39:51 UTC


README

A PHP-based headless CMS framework providing a JSON API for dynamic content management with flexible content types, authentication, and file handling.

Compatibility

  • PHP 8.0 or above
  • MySQL 8.x
  • Composer for dependency management

Installation

  1. Install via Composer
composer require tribe-framework/tribe-core
  1. Configure environment variables in .env:
DB_HOST=localhost
DB_PORT=3306
DB_NAME=your_database
DB_USER=your_user
DB_PASS=your_password
SSL=true
TRIBE_API_SECRET_KEY=your_secret_key
  1. Initialize database
CREATE TABLE `data` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`content`)),
  `type` varchar(255) DEFAULT NULL,
  `slug` varchar(255) DEFAULT NULL,
  `content_privacy` enum('public','private','draft','pending','sent') DEFAULT 'public',
  `user_id` int(11) DEFAULT NULL,
  `role_slug` varchar(50) DEFAULT NULL,
  `created_on` int(11) DEFAULT NULL,
  `updated_on` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `type` (`type`),
  KEY `slug` (`slug`),
  KEY `content_privacy` (`content_privacy`),
  KEY `user_id` (`user_id`),
  KEY `created_on` (`created_on`),
  KEY `updated_on` (`updated_on`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Core Architecture

Single-Table Design

Tribe uses a unified data table with JSON-based content storage:

  • id - Auto-increment primary key
  • content - JSON field containing all object attributes
  • type - Content type identifier (e.g., 'post', 'user', 'file')
  • slug - URL-friendly unique identifier
  • content_privacy - Access level: public, private, draft, pending, sent
  • user_id - Owner/creator reference
  • role_slug - User role identifier
  • created_on / updated_on - Unix timestamps

Core Classes

\Tribe\Core      - Object CRUD and search operations
\Tribe\API       - RESTful JSON API handler
\Tribe\Config    - Content type definitions and configuration
\Tribe\MySQL     - Database abstraction layer
\Tribe\Uploads   - File upload and image processing

Configuration & Content Types

Content Type Definition

Content types are defined in JSON format and can be:

  • Stored locally: /config/types.json
  • Uploaded dynamically: /uploads/types/*.json
  • Loaded from remote: GitHub repository blueprints

Type Structure

{
  "post": {
    "slug": "post",
    "title": "Posts",
    "sendable": false,
    "modules": [
      {
        "input_slug": "title",
        "input_type": "text",
        "input_primary": true,
        "input_unique": true,
        "input_placeholder": "Post Title"
      },
      {
        "input_slug": "body",
        "input_type": "textarea",
        "input_placeholder": "Post Content"
      },
      {
        "input_slug": "author_id",
        "input_type": "number",
        "linked_type": "user"
      }
    ]
  }
}

Module Properties

  • input_slug - Field identifier (required)
  • input_type - Field type: text, textarea, number, select, file, etc.
  • input_primary - Mark as primary/title field (one per type)
  • input_unique - Enforce uniqueness across type
  • input_placeholder - UI label/hint
  • var_type - Data type: int, float, bool, string
  • linked_type - Reference another content type
  • input_lang - Multi-language field configuration
  • list_field - Display in list views
  • input_options - For select/radio fields

Auto-Generated Fields

Every content type (except 'webapp') automatically includes:

For Regular Types:

{
  "input_slug": "content_privacy",
  "input_options": [
    { "slug": "public", "title": "Public link" },
    { "slug": "private", "title": "Private link" },
    { "slug": "pending", "title": "Submit for moderation" },
    { "slug": "draft", "title": "Draft" }
  ]
}

For Sendable Types:

{
  "input_slug": "content_privacy",
  "input_options": [
    { "slug": "sent", "title": "Send now" },
    { "slug": "draft", "title": "Save draft" }
  ]
}

Configuration Methods

$config = new \Tribe\Config();

// Get all content types
$types = $config->getTypes();

// Get type schema (all field slugs)
$schema = $config->getTypeSchema('post');

// Get primary module for a type
$primary = $config->getTypePrimaryModule('post', $types);

// Get linked modules (relationships)
$linked = $config->getTypeLinkedModules('post');

// Get project root directory
$root = $config->projectRoot();

Core Class - Object Management

Creating/Updating Objects

$core = new \Tribe\Core();

// Create new object
$post = [
    'type' => 'post',
    'title' => 'My First Post',
    'body' => 'This is the content.',
    'content_privacy' => 'public'
];
$postId = $core->pushObject($post);

// Update existing object
$post = [
    'id' => $postId,
    'type' => 'post',
    'title' => 'Updated Title'  // Only updates provided fields
];
$core->pushObject($post);

// Overwrite entire object
$post = [
    'id' => $postId,
    'type' => 'post',
    'title' => 'New Title',
    'body' => 'New content'
];
$core->pushObject($post, true);  // Second parameter overwrites

Batch Creating/Updating Objects

// pushObjects - Batch insert/update multiple objects efficiently
// Reduces SQL queries by batching operations instead of individual pushObject calls

// Create multiple new objects
$posts = [
    [
        'type' => 'post',
        'title' => 'First Post',
        'body' => 'Content of first post',
        'content_privacy' => 'public'
    ],
    [
        'type' => 'post',
        'title' => 'Second Post',
        'body' => 'Content of second post',
        'content_privacy' => 'public'
    ],
    [
        'type' => 'post',
        'title' => 'Third Post',
        'body' => 'Content of third post',
        'content_privacy' => 'draft'
    ]
];

$ids = $core->pushObjects($posts);
// Returns: [123, 124, 125] - Array of IDs in same order as input

// Update existing objects in batch
$updates = [
    ['id' => 123, 'type' => 'post', 'title' => 'Updated First'],
    ['id' => 124, 'type' => 'post', 'title' => 'Updated Second'],
    ['id' => 125, 'type' => 'post', 'content_privacy' => 'public']
];

$ids = $core->pushObjects($updates);
// Only updates provided fields for each object

// Overwrite entire objects (replace all fields)
$replacements = [
    ['id' => 123, 'type' => 'post', 'title' => 'New Title', 'body' => 'New body'],
    ['id' => 124, 'type' => 'post', 'title' => 'Another Title', 'body' => 'Another body']
];

$ids = $core->pushObjects($replacements, true);  // Second parameter overwrites

// Custom chunk size for very large batches
$largeDataset = [/* 1000+ objects */];
$ids = $core->pushObjects($largeDataset, false, 1000);  // Chunk size of 1000

// Mixed new and existing objects
$mixed = [
    ['type' => 'post', 'title' => 'New Post'],           // No ID - will create
    ['id' => 50, 'type' => 'post', 'title' => 'Update'], // Has ID - will update
    ['type' => 'post', 'title' => 'Another New']         // No ID - will create
];

$ids = $core->pushObjects($mixed);
// Returns IDs maintaining input order

Performance Benefits:

// BAD: Individual operations (N queries for N objects)
foreach ($posts as $post) {
    $core->pushObject($post);
}

// GOOD: Batch operation (2-3 queries total regardless of N)
$core->pushObjects($posts);

// Example: 500 objects
// pushObject in loop: ~1000 queries (INSERT + UPDATE per object)
// pushObjects: 3-4 queries (batched INSERTs and UPDATEs)

Parameters:

  • $posts (array): Array of post arrays in same format as pushObject
  • $overwrite_posts (bool, default: false): When true, replaces entire object instead of merging
  • $chunk_size (int, default: 500): Number of rows to batch per SQL statement

Return Value:

  • Array of resulting IDs in the same order as input posts

Notes:

  • All standard pushObject features apply: type casting, slug generation, uniqueness checks
  • Syncs to Typesense automatically (when enabled)
  • Handles mixed new and existing objects seamlessly
  • Use for bulk imports, migrations, or batch updates

Reading Objects

// Get single object by ID
$post = $core->getObject(123);
echo $post['title'];
echo $post['created_on'];

// Get specific attribute
$title = $core->getAttribute(123, 'title');

// Get multiple objects by IDs
$posts = $core->getObjects([1, 2, 3, 4, 5]);
foreach ($posts as $post) {
    echo $post['title'];
}

Deleting Objects

// Delete single object
$core->deleteObject(123);

// Delete multiple objects
$core->deleteObjects([1, 2, 3, 4, 5]);

Updating Single Attributes

// Update specific field
$core->pushAttribute(123, 'title', 'New Title');

// Useful for quick updates without loading full object
$core->pushAttribute(123, 'view_count', 150);

Slug Generation

// Generate slug from string
$slug = $core->slugify('My Post Title');
// Result: my-post-title-5f8a3c2b1

// Generate slug without unique ID (when field is unique)
$slug = $core->slugify('My Post Title', true);
// Result: my-post-title

Search Operations

Basic Search:

// Search for posts
$searchArray = [
    'type' => 'post',
    'title' => 'Tribe'
];

$ids = $core->getIDs(
    $searchArray,           // Search criteria
    "0, 10",               // Limit (offset, count)
    'created_on',          // Sort field
    'DESC',                // Sort order
    true                   // Show public only
);

// Get objects from IDs
$posts = $core->getObjects($ids);

Advanced Search with Parameters:

$ids = $core->getIDs(
    search_arr: [
        'type' => 'post',
        'category' => 'tech',
        'status' => 'published'
    ],
    limit: "20, 10",                      // Skip 20, get 10
    sort_field: ['created_on', 'title'],  // Multiple sort
    sort_order: ['DESC', 'ASC'],
    show_public_objects_only: false,      // Include all privacy levels
    ignore_ids: [5, 10, 15],             // Exclude specific IDs
    show_partial_search_results: true,    // Enable LIKE search
    show_case_sensitive_search_results: false,
    comparison_within_module_phrase: 'LIKE',
    inbetween_same_module_phrases: 'OR',
    between_different_module_phrases: 'AND'
);

Search with Array Values (OR conditions):

// Find posts with multiple categories
$ids = $core->getIDs([
    'type' => 'post',
    'category' => ['tech', 'science', 'programming']  // OR condition
]);

Range Search:

// Find posts within date range
$ids = $core->getIDs(
    search_arr: ['type' => 'post'],
    range: [
        'created_on' => [
            'from' => 1640000000,
            'to' => 1650000000
        ],
        'view_count' => [
            'from' => 100
        ]
    ]
);

Random Ordering:

// Get random posts
$ids = $core->getIDs(
    search_arr: ['type' => 'post'],
    limit: "0, 5",
    sort_field: '(random)'
);

Get Total Count:

$totalCount = $core->getIDsTotalCount(
    search_arr: ['type' => 'post', 'status' => 'published'],
    limit: "0, 10",
    sort_field: 'created_on',
    sort_order: 'DESC'
);

Full-Text Database Search:

// Fallback search when advanced search is unavailable
$results = $core->searchDatabase(
    query: 'machine learning tutorial',
    options: [
        'type' => 'post',
        'limit' => '0, 20',
        'sort_field' => 'created_on',
        'sort_order' => 'DESC',
        'show_public_objects_only' => true
    ]
);

// Returns array with:
// - objects: Array of matching objects
// - total_found: Total count
// - search_time_ms: Query execution time
// - source: 'database'

API Class - RESTful Interface

Basic Usage

// api.php
require __DIR__ . '/_init.php';
$api = new \Tribe\API;
$api->jsonAPI('1.1');

Endpoint Structure

The API uses a simplified path structure without /api/v1.1/ prefix:

GET    /api.php/{type}           - List objects
GET    /api.php/{type}/{id}      - Get single object by ID
GET    /api.php/{type}/{slug}    - Get single object by slug
POST   /api.php/{type}           - Create object
PATCH  /api.php/{type}/{id}      - Update object
DELETE /api.php/{type}/{id}      - Delete object

Examples:

GET https://tribe.yourwebsitelink.com/api.php/petition/108820
GET https://tribe.yourwebsitelink.com/api.php/webapp/0
GET https://tribe.yourwebsitelink.com/api.php/post
POST https://tribe.yourwebsitelink.com/api.php/post

Authentication Levels

1. Junction/Tribe Domains (Full Access)

  • Requests from trusted internal domains (defined in $_ENV['BARE_URL'])
  • Full CRUD operations
  • No API key required
  • Example: Requests from tribe.yourwebsitelink.com to itself

2. API Key with Full Access

Authorization: Bearer YOUR_FULL_ACCESS_KEY
  • Complete read/write operations
  • Can create, update, delete
  • Configured in database as apikey_record type with readonly=false

3. API Key with Read Access

Authorization: Bearer YOUR_READ_ONLY_KEY
  • GET operations only
  • Cannot create, update, or delete
  • Configured in database as apikey_record type with readonly=true

4. Development Mode

Authorization: Bearer YOUR_API_KEY
  • API key with devmode=true flag
  • Full access when request originates from localhost/127.0.0.1
  • Useful for local development

5. Domain Whitelisting

  • API keys can have whitelisted_domains field
  • Supports wildcard patterns (e.g., *.example.com)
  • Restricts API key usage to specific domains
  • Format: One domain per line in the field

6. Public Access

  • No authentication required
  • Access to content_privacy='public' objects only
  • GET operations on public content

Creating API Keys

API keys are stored as objects in the database with type apikey_record:

$core = new \Tribe\Core();

// Create read-only API key
$apiKey = [
    'type' => 'apikey_record',
    'apikey' => bin2hex(random_bytes(32)), // Generate secure random key
    'readonly' => true,
    'content_privacy' => 'private', // or 'public'
    'whitelisted_domains' => "example.com\n*.myapp.com", // Optional
    'devmode' => false
];
$keyId = $core->pushObject($apiKey);

// Create full-access API key
$fullAccessKey = [
    'type' => 'apikey_record',
    'apikey' => bin2hex(random_bytes(32)),
    'readonly' => false,
    'content_privacy' => 'private',
    'whitelisted_domains' => "admin.example.com"
];
$fullKeyId = $core->pushObject($fullAccessKey);

// Create development API key
$devKey = [
    'type' => 'apikey_record',
    'apikey' => bin2hex(random_bytes(32)),
    'readonly' => false,
    'devmode' => true, // Full access from localhost only
    'content_privacy' => 'private'
];
$devKeyId = $core->pushObject($devKey);

Request Examples

List Objects:

# Basic list
GET /api.php/post

# With filtering
GET /api.php/post?filter[status]=published&filter[category]=tech

# With pagination
GET /api.php/post?page[offset]=20&page[limit]=10

# With sorting (- prefix for descending)
GET /api.php/post?sort=-created_on,title

# Show all (requires auth)
GET /api.php/post?show_public_objects_only=false

Get Single Object by ID:

GET /api.php/post/123

Get Single Object by Slug:

GET /api.php/post/my-post-title-abc123

Get Types Configuration (webapp/0):

GET /api.php/webapp/0

Returns all content type definitions, statistics, and configuration.

Create Object:

POST /api.php/post
Content-Type: application/vnd.api+json
Authorization: Bearer YOUR_API_KEY

{
  "data": {
    "type": "post",
    "attributes": {
      "modules": {
        "title": "My Post",
        "body": "Content here",
        "category": "tech"
      }
    }
  }
}

Update Object:

PATCH /api.php/post/123
Content-Type: application/vnd.api+json
Authorization: Bearer YOUR_API_KEY

{
  "data": {
    "type": "post",
    "id": "123",
    "attributes": {
      "modules": {
        "title": "Updated Title"
      }
    }
  }
}

Delete Object:

DELETE /api.php/post/123
Authorization: Bearer YOUR_API_KEY

Response Format

All responses follow JSON:API specification v1.1 format.

Success Response (Single Object):

{
  "jsonapi": {
    "version": "1.1"
  },
  "data": {
    "type": "petition",
    "id": "108820",
    "attributes": {
      "modules": {
        "id": "108820",
        "type": "petition",
        "slug": "2022-petition-application",
        "title": "2022 Petition Application",
        "case_number": "114-2022",
        "district": "Gurugram",
        "files": [
          {
            "url": "/uploads/cases/114-2022/document.pdf",
            "mime": "application/pdf",
            "name": "document.pdf",
            "transcription_ocr_en": "...",
            "transcription_ocr_hi": "..."
          }
        ],
        "created_on": "1767180487",
        "updated_on": "1767788697",
        "content_privacy": "public"
      },
      "slug": "2022-application"
    }
  }
}

List Response:

{
  "jsonapi": {
    "version": "1.1"
  },
  "data": [
    {
      "type": "post",
      "id": "123",
      "attributes": {
        "modules": {
          "id": "123",
          "title": "Post 1",
          "body": "Content...",
          "slug": "post-1-abc123"
        },
        "slug": "post-1-abc123"
      }
    },
    {
      "type": "post",
      "id": "124",
      "attributes": {
        "modules": {
          "id": "124",
          "title": "Post 2",
          "body": "Content...",
          "slug": "post-2-def456"
        },
        "slug": "post-2-def456"
      }
    }
  ],
  "meta": {
    "total": 2
  }
}

Types Configuration Response (webapp/0):

{
  "jsonapi": {
    "version": "1.1"
  },
  "data": {
    "type": "webapp",
    "id": "0",
    "attributes": {
      "modules": {
        "post": {
          "name": "Post",
          "slug": "post",
          "plural": "Posts",
          "description": "Blog posts",
          "total_objects": 42,
          "primary_module": "title",
          "modules": [
            {
              "input_slug": "title",
              "input_type": "text",
              "input_primary": true,
              "input_unique": false,
              "list_field": true,
              "var_type": "string"
            }
          ]
        },
        "webapp": {
          "size_in_gb": "2.45",
          "total_objects": 1250
        }
      },
      "slug": "webapp"
    }
  }
}

Error Response:

{
  "errors": [
    {
      "status": "403",
      "title": "Forbidden",
      "detail": "Access denied"
    }
  ]
}

Linked Modules

The API automatically resolves relationships defined in content types:

// If 'post' type has linked_type for 'author_id' → 'user'
// Response includes linked object

{
  "data": {
    "type": "post",
    "id": "123",
    "attributes": {
      "modules": {
        "title": "My Post",
        "author_id": 5,
        "author_id__linked": {
          "id": 5,
          "type": "user",
          "name": "John Doe",
          "email": "john@example.com"
        }
      }
    }
  }
}

Search Endpoint

The API provides a dedicated search endpoint for full-text search:

GET /api.php/search?q=query&type=post&page=1&per_page=20

Query Parameters:

  • q - Search query (required)
  • type - Content type to search (required)
  • page - Page number (default: 1)
  • per_page - Results per page (default: 10)
  • sort_by - Sort field (e.g., created_on:desc)
  • facet_by - Faceting field for aggregations
  • Additional filters can be passed as query parameters

Search Response:

{
  "jsonapi": { "version": "1.1" },
  "data": [
    {
      "type": "post",
      "id": "123",
      "attributes": {
        "modules": { "title": "Matching Post", ... },
        "search_highlights": {
          "title": "Matching <mark>Post</mark>"
        }
      }
    }
  ],
  "meta": {
    "search": {
      "total_found": 42,
      "search_time_ms": 15,
      "search_source": "database",
      "query": "tutorial",
      "page": 1,
      "per_page": 20,
      "facet_counts": {}
    }
  }
}

JavaScript Example:

// Search for posts
const searchResults = await fetch(
  "https://tribe.yourwebsitelink.com/api.php/search?" +
    new URLSearchParams({
      q: "application",
      type: "petition",
      page: 1,
      per_page: 20,
      sort_by: "created_on:desc",
    }),
).then((r) => r.json());

console.log(searchResults.meta.search.total_found);
console.log(searchResults.data); // Array of matching records

Advanced Filtering

Multiple Filters (AND):

?filter[status]=published&filter[category]=tech&filter[featured]=true

Array Filters (OR):

?filter[category][]=tech&filter[category][]=science

Range Queries:

?range[created_on][from]=1640000000&range[created_on][to]=1650000000

Partial Search:

?filter[title]=tutorial&show_partial_search_results=true

CORS Configuration

The API automatically handles CORS:

// Automatic headers
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true

Uploads Class - File Management

Basic File Upload

// uploads.php
require __DIR__ . '/_init.php';
$uploads = new \Tribe\Uploads;
$api = new \Tribe\API;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $_POST = $api->requestBody;
}

if (($_FILES ?? false) || ($_POST ?? false)) {
    header('Content-type: application/json; charset=utf-8');
    echo json_encode($uploads->handleUpload(
        $_FILES ?? [],
        $_POST ?? [],
        $_GET ?? []
    ));
}

Upload via URL

// JavaScript/Ember example
async uploadFile(file) {
  try {
    const response = await file.upload('/uploads.php');
    const data = await response.json();

    if (data.status === 'success') {
      console.log(data.file.url);      // Main file URL
      console.log(data.file.name);     // File name
      console.log(data.file.mime);     // MIME type

      // Image variants (if image)
      console.log(data.file.xl.url);   // 2100px max
      console.log(data.file.lg.url);   // 1400px max
      console.log(data.file.md.url);   // 700px max
      console.log(data.file.sm.url);   // 350px max
      console.log(data.file.xs.url);   // 100px max

      // Video HLS (if video)
      console.log(data.file.hls.url);  // Adaptive streaming
    }
  } catch (error) {
    console.error(error);
  }
}

Upload Response

{
  "status": "success",
  "success": 1,
  "error": 0,
  "file": {
    "name": "document_abc123",
    "url": "/uploads/2025/01-January/26-Monday/document_abc123.pdf",
    "mime": "application/pdf"
  }
}

Image Upload Response:

{
  "status": "success",
  "file": {
    "name": "photo_xyz789",
    "url": "/uploads/2025/01-January/26-Monday/photo_xyz789.jpg",
    "mime": "image/jpeg",
    "xl": {
      "name": "photo_xyz789",
      "url": "/uploads/2025/01-January/26-Monday/xl/photo_xyz789.jpg"
    },
    "lg": {
      "name": "photo_xyz789",
      "url": "/uploads/2025/01-January/26-Monday/lg/photo_xyz789.jpg"
    },
    "md": { "url": "..." },
    "sm": { "url": "..." },
    "xs": { "url": "..." }
  }
}

Video Upload Response:

{
  "status": "success",
  "file": {
    "name": "video_abc123",
    "url": "/uploads/2025/01-January/26-Monday/video_abc123.mp4",
    "mime": "video/mp4",
    "hls": {
      "url": "/uploads/2025/01-January/26-Monday/hls/video_abc123.m3u8",
      "filename": "video_abc123.m3u8",
      "type": "adaptive",
      "qualities": ["xl", "lg", "md", "sm", "xs"]
    }
  }
}

File Organization

/uploads/
  /YYYY/                    # Year
    /MM-Month/              # Month
      /DD-Day/              # Day
        file.pdf            # Original files
        /xs/                # 100x100px max
        /sm/                # 350x350px max
        /md/                # 700x700px max
        /lg/                # 1400x1400px max
        /xl/                # 2100x2100px max
        /hls/               # Video streaming files
          video.m3u8        # Master playlist
          video_xl.m3u8     # 2160p (4K)
          video_lg.m3u8     # 1080p (Full HD)
          video_md.m3u8     # 720p (HD)
          video_sm.m3u8     # 540p
          video_xs.m3u8     # 360p
          *.ts              # Video segments

Supported File Types

Images:

  • JPG, JPEG, PNG, WebP, GIF, SVG
  • Auto-generates 5 sizes (xl, lg, md, sm, xs)
  • Maintains aspect ratio

Videos:

  • MP4, MOV, AVI, MKV, WebM
  • Converts to HLS with multiple quality levels
  • Generates adaptive streaming playlists

Documents:

  • PDF, DOC, DOCX, TXT, HTML, JSON

Audio:

  • MP3, M4A, OGG, OGA, WAV

Subtitles:

  • VTT, SRT

Image Processing

$uploads = new \Tribe\Uploads();

// Get specific image size
$file = $uploads->getUploadedImageInSize(
    '/uploads/2025/01-January/26-Monday/photo.jpg',
    'md'  // Size: xs, sm, md, lg, xl
);

echo $file['url'];   // /uploads/.../md/photo.jpg
echo $file['path'];  // Filesystem path

// Get all versions of a file
$versions = $uploads->getUploadedFileVersions(
    '/uploads/2025/01-January/26-Monday/photo.jpg',
    'sm'  // Preferred thumbnail size
);

echo $versions['url']['source'];     // Original
echo $versions['url']['xl'];         // 2100px
echo $versions['url']['lg'];         // 1400px
echo $versions['url']['md'];         // 700px
echo $versions['url']['sm'];         // 350px
echo $versions['url']['xs'];         // 100px
echo $versions['url']['thumbnail'];  // Preferred size (sm)

// For videos
echo $versions['url']['hls'];        // Master playlist
echo $versions['url']['hls_xl'];     // 4K quality
echo $versions['url']['hls_lg'];     // 1080p quality

Video HLS Streaming

Quality Settings:

// Automatically generated qualities:

// XL - 2160p (4K)
// Resolution: 3840x2160
// Video bitrate: 15000k
// Audio bitrate: 192k

// LG - 1080p (Full HD)
// Resolution: 1920x1080
// Video bitrate: 8000k
// Audio bitrate: 128k

// MD - 720p (HD)
// Resolution: 1280x720
// Video bitrate: 4000k
// Audio bitrate: 128k

// SM - 540p
// Resolution: 960x540
// Video bitrate: 2000k
// Audio bitrate: 96k

// XS - 360p
// Resolution: 640x360
// Video bitrate: 1000k
// Audio bitrate: 64k

Check HLS Status:

$uploads = new \Tribe\Uploads();

// Check if HLS conversion is complete
$isReady = $uploads->isHLSReady(
    '/uploads/2025/01-January/26-Monday/hls/video.m3u8'
);

// Get conversion status
$status = $uploads->getHLSStatus('video_abc123');

// Returns:
// [
//   'status' => 'completed',      // or 'processing', 'failed'
//   'progress' => 100,             // 0-100
//   'qualities' => [...]           // Available quality levels
// ]

File Search

// Search by filename and/or content
$results = $uploads->handleFileSearch(
    'invoice##2024',  // Search terms separated by ##
    true              // Deep search (includes PDF content)
);

echo json_encode([
    'by_file_name' => $results['by_file_name'],
    'by_file_content' => $results['by_file_content']
]);

Copy File from URL

$uploads = new \Tribe\Uploads();

$localUrl = $uploads->copyFileFromURL(
    'https://example.com/image.jpg'
);

// Returns: /uploads/2025/01-January/26-Monday/1640000000-image.jpg

Delete File Record

$uploads = new \Tribe\Uploads();

// Delete file and all its variants
$uploads->deleteFileRecord([
    'url' => '/uploads/2025/01-January/26-Monday/photo.jpg',
    'file' => [
        'xl' => ['url' => '...'],
        'lg' => ['url' => '...'],
        'md' => ['url' => '...'],
        'sm' => ['url' => '...'],
        'xs' => ['url' => '...'],
        'hls' => ['url' => '...']
    ]
]);

Custom Upload Handling

# POST request with file
POST /uploads.php
Content-Type: multipart/form-data

# POST request with URL
POST /uploads.php
Content-Type: application/json
{
  "url": "https://example.com/existing-file.jpg"
}

# File search
POST /uploads.php
Content-Type: application/json
{
  "search": true,
  "q": "invoice##2024",
  "deep_search": true
}

MySQL Class - Database Layer

Basic Usage

$sql = new \Tribe\MySQL();

// Execute query
$results = $sql->executeSQL("SELECT * FROM `data` WHERE `type`='post' LIMIT 10");

// Single result
if (count($results) === 1) {
    echo $results[0]['title'];
}

// Multiple results
foreach ($results as $row) {
    echo $row['title'];
}

// Get last insert ID
$id = $sql->lastInsertID();

// Close connection
$sql->closeConnection();

Query Results

// Returns array of results or 0 if no results
$results = $sql->executeSQL($query);

if ($results === false) {
    // Query error
    echo $sql->lastError;
    echo $sql->lastQuery;
} elseif ($results === 0) {
    // No results found
} else {
    // Process results
    foreach ($results as $row) {
        // ...
    }
}

// Properties
$sql->records;        // Number of rows returned
$sql->affected;       // Number of rows affected
$sql->lastQuery;      // Last executed query
$sql->lastError;      // Last error message

Schema Awareness

// The MySQL class is aware of the table schema:
$sql->schema = [
    'id',
    'content',
    'updated_on',
    'created_on',
    'user_id',
    'role_slug',
    'slug',
    'content_privacy',
    'type'
];

// Use schema to build dynamic queries safely

Automatic Data Processing

// Automatically:
// - Escapes values to prevent SQL injection
// - Strips slashes from retrieved data
// - Preserves JSON data integrity
// - Handles UTF-8 encoding (utf8mb4)

Email Integration

Sending OTP/Verification Emails

// sendotp.php
require __DIR__ . '/../../_init.php';

use PHPMailer\PHPMailer\PHPMailer;

// Environment variables required:
// MAILPACE_SERVER_TOKEN
// MAILPACE_DOMAIN

// POST request
// {
//   "email": "user@example.com",
//   "otp": "123456"
// }

// Sends HTML email with OTP
// Subject: "Your OTP is 123456"
// From: no-reply@YOUR_DOMAIN

Environment Configuration

Required Variables

# Database
DB_HOST=localhost
DB_PORT=3306
DB_NAME=your_database
DB_USER=your_username
DB_PASS=your_password

# Security
SSL=true
TRIBE_API_SECRET_KEY=your_secret_jwt_key
BARE_URL=yourdomain.com  # Trusted domain for internal requests

# File Storage (optional)
S3_UPLOADS_BUCKET_CDN_URL=https://cdn.example.com

# Email (optional)
MAILPACE_SERVER_TOKEN=your_mailpace_token
MAILPACE_DOMAIN=yourdomain.com

# Debugging (optional)
DISPLAY_ERRORS=false

Development vs Production

// Development
$_ENV['DISPLAY_ERRORS'] = true;

// Production
$_ENV['DISPLAY_ERRORS'] = false;

Security Features

Content Privacy Levels

  • public - Accessible without authentication
  • private - Requires API key, visible to authenticated users
  • draft - Only visible to creator/owner
  • pending - Awaiting moderation/approval
  • sent - For sendable types (messages, notifications)

API Key Security

// Store API keys securely in database
// Keys should be:
// - Randomly generated (32+ characters)
// - Stored with access level (read/full)
// - Associated with specific domains (optional)
// - Rotatable/revocable

SQL Injection Prevention

// MySQL class automatically escapes values
$sql->executeSQL("SELECT * FROM `data` WHERE `id`='" . $safeId . "'");

// Always use the MySQL class methods
// Never concatenate raw user input into queries

CORS Protection

// API automatically validates:
// - Request origin against allowed domains
// - Preflight OPTIONS requests
// - Authentication headers
// - Content-Type headers

Performance Optimization

Database Indexing

-- Ensure indexes on frequently queried columns
CREATE INDEX idx_type ON data(type);
CREATE INDEX idx_slug ON data(slug);
CREATE INDEX idx_privacy ON data(content_privacy);
CREATE INDEX idx_user ON data(user_id);
CREATE INDEX idx_created ON data(created_on);
CREATE INDEX idx_updated ON data(updated_on);

Query Optimization

// Use specific field selection
$core->getIDs(['type' => 'post'], "0, 20");

// Rather than loading all then filtering
// Use backend filtering in getIDs()

// Leverage pagination
$core->getIDs(['type' => 'post'], "0, 20");  // First 20
$core->getIDs(['type' => 'post'], "20, 20"); // Next 20

File Upload Optimization

// Images auto-generate sizes asynchronously
// Videos convert to HLS in background
// Use appropriate size variants in frontend

// For thumbnails: xs (100px)
// For lists: sm (350px) or md (700px)
// For detail views: lg (1400px)
// For full-screen: xl (2100px)

Caching Strategies

// Static files served with cache headers
// API responses can be cached by client
// Use ETags for conditional requests

// Example: Cache-Control header
header('Cache-Control: public, max-age=3600');

Usage Examples

Complete CRUD Example

require __DIR__ . '/_init.php';

$core = new \Tribe\Core();
$config = new \Tribe\Config();

// 1. Create a blog post
$postData = [
    'type' => 'post',
    'title' => 'Introduction to Tribe Framework',
    'body' => 'Tribe is a flexible PHP-based CMS...',
    'category' => 'tutorials',
    'author_id' => 5,
    'content_privacy' => 'public'
];
$postId = $core->pushObject($postData);
echo "Created post ID: $postId\n";

// 2. Read the post
$post = $core->getObject($postId);
echo "Post title: {$post['title']}\n";
echo "Created: " . date('Y-m-d H:i:s', $post['created_on']) . "\n";

// 3. Update the post
$updateData = [
    'id' => $postId,
    'type' => 'post',
    'title' => 'Getting Started with Tribe Framework'
];
$core->pushObject($updateData);
echo "Updated post title\n";

// 4. Search for posts
$ids = $core->getIDs(
    ['type' => 'post', 'category' => 'tutorials'],
    "0, 10",
    'created_on',
    'DESC',
    true
);
$posts = $core->getObjects($ids);
echo "Found " . count($posts) . " tutorial posts\n";

// 5. Delete the post
$core->deleteObject($postId);
echo "Deleted post ID: $postId\n";

API Integration Example

// JavaScript frontend example
class TribeAPI {
  constructor(baseURL, apiKey = null) {
    this.baseURL = baseURL;
    this.apiKey = apiKey;
  }

  async request(endpoint, options = {}) {
    const headers = {
      "Content-Type": "application/vnd.api+json",
      ...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
    };

    const response = await fetch(`${this.baseURL}${endpoint}`, {
      ...options,
      headers: { ...headers, ...options.headers },
    });

    return response.json();
  }

  // List posts
  async listPosts(filters = {}, page = { offset: 0, limit: 10 }) {
    const params = new URLSearchParams();

    Object.entries(filters).forEach(([key, value]) => {
      params.append(`filter[${key}]`, value);
    });

    params.append("page[offset]", page.offset);
    params.append("page[limit]", page.limit);

    return this.request(`/api.php/post?${params}`);
  }

  // Get single post by ID
  async getPost(id) {
    return this.request(`/api.php/post/${id}`);
  }

  // Get single post by slug
  async getPostBySlug(slug) {
    return this.request(`/api.php/post/${slug}`);
  }

  // Get types configuration
  async getTypes() {
    return this.request("/api.php/webapp/0");
  }

  // Create post
  async createPost(data) {
    return this.request("/api.php/post", {
      method: "POST",
      body: JSON.stringify({
        data: {
          type: "post",
          attributes: { modules: data },
        },
      }),
    });
  }

  // Update post
  async updatePost(id, data) {
    return this.request(`/api.php/post/${id}`, {
      method: "PATCH",
      body: JSON.stringify({
        data: {
          type: "post",
          id: String(id),
          attributes: { modules: data },
        },
      }),
    });
  }

  // Delete post
  async deletePost(id) {
    return this.request(`/api.php/post/${id}`, {
      method: "DELETE",
    });
  }

  // Upload file
  async uploadFile(file) {
    const formData = new FormData();
    formData.append("file", file);

    const response = await fetch(`${this.baseURL}/uploads.php`, {
      method: "POST",
      body: formData,
      headers: this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {},
    });

    return response.json();
  }
}

// Usage
const api = new TribeAPI("https://tribe.yourwebsitelink.com", "your_api_key");

// Get types configuration
const config = await api.getTypes();
console.log(config.data.attributes.modules.post); // Post type definition

// Create post
const newPost = await api.createPost({
  title: "My First Post",
  body: "This is the content",
  content_privacy: "public",
});

// List posts
const posts = await api.listPosts(
  { category: "tech" },
  { offset: 0, limit: 20 },
);

// Get post by slug
const post = await api.getPostBySlug("my-first-post-abc123");

// Upload image
const fileInput = document.querySelector("#file-input");
const uploadResult = await api.uploadFile(fileInput.files[0]);
console.log(uploadResult.file.md.url); // Medium-sized image

Multi-Language Content

// Content type with multi-language support
$types = [
    'product' => [
        'modules' => [
            [
                'input_slug' => 'title',
                'input_lang' => [
                    ['slug' => 'en', 'title' => 'English'],
                    ['slug' => 'es', 'title' => 'Spanish'],
                    ['slug' => 'fr', 'title' => 'French']
                ]
            ]
        ]
    ]
];

// Saving multi-language content
$product = [
    'type' => 'product',
    'title_en' => 'Blue Shoes',
    'title_es' => 'Zapatos Azules',
    'title_fr' => 'Chaussures Bleues',
    'content_privacy' => 'public'
];
$productId = $core->pushObject($product);

// Retrieving
$product = $core->getObject($productId);
echo $product['title_en'];  // Blue Shoes
echo $product['title_es'];  // Zapatos Azules

Linked Content Types

// Define relationships
$types = [
    'post' => [
        'modules' => [
            [
                'input_slug' => 'author_id',
                'linked_type' => 'user'
            ],
            [
                'input_slug' => 'category_id',
                'linked_type' => 'category'
            ]
        ]
    ]
];

// Create linked content
$post = [
    'type' => 'post',
    'title' => 'My Post',
    'author_id' => 5,
    'category_id' => 3
];
$core->pushObject($post);

// API automatically includes linked objects
GET /api/v1.1/post/123

// Response includes:
{
  "data": {
    "attributes": {
      "author_id": 5,
      "author_id__linked": {
        "id": 5,
        "type": "user",
        "name": "John Doe"
      },
      "category_id": 3,
      "category_id__linked": {
        "id": 3,
        "type": "category",
        "name": "Technology"
      }
    }
  }
}

Common Patterns

Pagination

// Page 1
$page1 = $core->getIDs(['type' => 'post'], "0, 20");

// Page 2
$page2 = $core->getIDs(['type' => 'post'], "20, 20");

// Page 3
$page3 = $core->getIDs(['type' => 'post'], "40, 20");

// Get total for pagination
$total = $core->getIDsTotalCount(['type' => 'post'], "0, 20");
$totalPages = ceil($total / 20);

Filtering & Sorting

// Complex filter
$results = $core->getIDs(
    search_arr: [
        'type' => 'product',
        'category' => ['electronics', 'computers'],
        'status' => 'available'
    ],
    limit: "0, 50",
    sort_field: ['price', 'created_on'],
    sort_order: ['ASC', 'DESC'],
    show_partial_search_results: true
);

Unique Constraints

// Ensure unique slugs
$post = [
    'type' => 'post',
    'title' => 'Unique Title',
    'content_privacy' => 'public'
];

$postId = $core->pushObject($post);

if ($postId === 0) {
    // Uniqueness constraint violated
    echo "A post with this title already exists\n";
} else {
    echo "Created post ID: $postId\n";
}

Soft Updates

// Update only specific fields (default behavior)
$update = [
    'id' => 123,
    'type' => 'post',
    'title' => 'New Title'
    // Other fields remain unchanged
];
$core->pushObject($update);

// Complete replacement
$replacement = [
    'id' => 123,
    'type' => 'post',
    'title' => 'New Title',
    'body' => 'New content'
    // All other fields will be removed
];
$core->pushObject($replacement, true);  // true = overwrite

Troubleshooting

Common Issues

1. Database Connection Failed

Error: Unable to connect to MySQL
  • Check DB_HOST, DB_NAME, DB_USER, DB_PASS in .env
  • Verify MySQL service is running
  • Check firewall/network connectivity

2. Uniqueness Constraint Violation

pushObject() returns 0
  • Another object with same primary field value exists
  • Check if input_unique is set on primary module
  • Generate new slug or modify unique field

3. File Upload Fails

Permission denied / mkdir failed
  • Check write permissions on /uploads directory
  • Set permissions: chmod 755 uploads
  • Verify PHP upload_max_filesize and post_max_size

4. HLS Video Conversion Stuck

Video uploaded but HLS not available
  • Check FFmpeg is installed and accessible
  • View conversion log: /tmp/ffmpeg_*.log
  • Increase server timeout settings

5. API Returns Empty Results

{"data": []}
  • Check content_privacy settings
  • Verify authentication for private content
  • Use show_public_objects_only=false with auth

6. Linked Modules Not Resolving

author_id__linked not in response
  • Verify linked_type is defined in type configuration
  • Check that linked object exists
  • Ensure proper API version (v1.1)

Debug Mode

// Enable error display
$_ENV['DISPLAY_ERRORS'] = true;

// View SQL queries
$debug = true;
$core->getIDs(
    ['type' => 'post'],
    debug_show_sql_statement: true
);

// Check last query
$sql = new \Tribe\MySQL();
$sql->executeSQL($query);
echo $sql->lastQuery;
echo $sql->lastError;

Performance Issues

// Optimize queries
// BAD: Load all, filter in PHP
$all = $core->getObjects($core->getIDs(['type' => 'post'], "0, 1000"));
$filtered = array_filter($all, fn($p) => $p['status'] === 'published');

// GOOD: Filter in database
$filtered = $core->getObjects($core->getIDs([
    'type' => 'post',
    'status' => 'published'
], "0, 1000"));

// Use pagination
$page = $core->getIDs(['type' => 'post'], "0, 20");

// Optimize image loading
// Use appropriate size variant
<img src="{{ file.sm.url }}" />  <!-- 350px for thumbnails -->
<img src="{{ file.lg.url }}" />  <!-- 1400px for detail view -->

Best Practices

Content Type Design

// Define clear, focused content types
// BAD: Generic "content" type with 50 modules
// GOOD: Specific types (post, page, product, user)

// Use appropriate field types
[
    'input_slug' => 'price',
    'input_type' => 'number',
    'var_type' => 'float'
]

// Mark primary/unique fields
[
    'input_slug' => 'email',
    'input_primary' => true,
    'input_unique' => true
]

API Usage

// Use proper HTTP methods
GET    - Reading data
POST   - Creating new objects
PATCH  - Updating existing objects
DELETE - Removing objects

// Include proper headers
headers: {
  'Content-Type': 'application/vnd.api+json',
  'Authorization': 'Bearer YOUR_KEY'
}

// Handle errors gracefully
try {
  const data = await api.getPost(123);
} catch (error) {
  if (error.status === 404) {
    // Handle not found
  } else if (error.status === 403) {
    // Handle unauthorized
  }
}

File Management

// Store file references, not files in database
$post = [
    'type' => 'post',
    'image_url' => '/uploads/2025/01-January/26-Monday/photo.jpg',
    'files' => [
        ['url' => '/uploads/.../document.pdf', 'name' => 'Report'],
        ['url' => '/uploads/.../image.jpg', 'name' => 'Photo']
    ]
];

// Use appropriate image sizes
// Thumbnails: xs or sm
// List views: sm or md
// Detail views: lg
// Full-screen: xl
// Original: Only when necessary

// Clean up old files periodically
// Implement file retention policies
// Use S3 for long-term storage

Security

// Always validate input
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    throw new Exception('Invalid email');
}

// Use content_privacy appropriately
'draft'   - Work in progress
'pending' - Needs approval
'private' - Authenticated only
'public'  - Everyone

// Rotate API keys regularly
// Use read-only keys when possible
// Implement rate limiting
// Log security events

Performance

// Batch operations
$core->deleteObjects([1, 2, 3, 4, 5]);  // Better than 5 individual deletes

// Use pagination
$limit = "0, 20";  // Not "0, 1000"

// Selective loading
// Only load what you need
$ids = $core->getIDs(['type' => 'post', 'status' => 'published']);

// Cache frequently accessed data
// Use ETags for conditional requests
// Implement HTTP caching headers

Advanced Topics

Custom Endpoints

// Create custom endpoint: api/custom.php
require __DIR__ . '/_init.php';
$api = new \Tribe\API;
$core = new \Tribe\Core();

header('Content-Type: application/json');

if ($_SERVER['REQUEST_METHOD'] === 'GET') {
    // Custom logic
    $stats = [
        'total_posts' => $core->getIDsTotalCount(['type' => 'post']),
        'total_users' => $core->getIDsTotalCount(['type' => 'user']),
        'recent_posts' => $core->getObjects(
            $core->getIDs(['type' => 'post'], "0, 5", 'created_on', 'DESC')
        )
    ];

    echo json_encode(['data' => $stats]);
}

Webhooks Integration

// After object creation/update
$postId = $core->pushObject($post);

// Trigger webhook
if ($postId && ($_ENV['WEBHOOK_URL'] ?? false)) {
    $payload = json_encode([
        'event' => 'post.created',
        'object_id' => $postId,
        'object_type' => 'post',
        'timestamp' => time()
    ]);

    $ch = curl_init($_ENV['WEBHOOK_URL']);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_exec($ch);
    curl_close($ch);
}

Custom Authentication

// Implement custom auth logic
class CustomAuth {
    public static function validateToken($token) {
        // Verify JWT or custom token
        // Return user ID or false
    }

    public static function checkPermission($userId, $action, $objectType) {
        // Check if user can perform action
        // Return true or false
    }
}

// Use in API
if (!CustomAuth::validateToken($_SERVER['HTTP_AUTHORIZATION'] ?? '')) {
    http_response_code(401);
    echo json_encode(['errors' => [['status' => '401', 'title' => 'Unauthorized']]]);
    exit;
}

Migration & Deployment

Database Migration

-- Backup existing data
mysqldump -u username -p database_name > backup.sql

-- Migrate to Tribe structure
CREATE TABLE data_new LIKE data;

INSERT INTO data_new (id, content, type, slug, content_privacy, user_id, created_on, updated_on)
SELECT
  id,
  JSON_OBJECT('title', title, 'body', body, 'status', status) as content,
  'post' as type,
  slug,
  CASE
    WHEN status = 'published' THEN 'public'
    WHEN status = 'draft' THEN 'draft'
    ELSE 'private'
  END as content_privacy,
  author_id as user_id,
  UNIX_TIMESTAMP(created_at) as created_on,
  UNIX_TIMESTAMP(updated_at) as updated_on
FROM old_posts_table;

-- Verify and swap tables
RENAME TABLE data TO data_old, data_new TO data;

Deployment Checklist

# 1. Set environment variables
cp .env.example .env
# Edit .env with production values

# 2. Set permissions
chmod 755 uploads/
chmod 644 .env

# 3. Install dependencies
composer install --no-dev --optimize-autoloader

# 4. Configure web server
# - Point document root to public/
# - Enable mod_rewrite (Apache)
# - Configure PHP-FPM (Nginx)

# 5. Set up SSL
# - Install SSL certificate
# - Force HTTPS redirects
# - Set SSL=true in .env

# 6. Database optimization
# - Add indexes
# - Configure connection pooling
# - Set up read replicas (if needed)

# 7. File storage
# - Configure S3_UPLOADS_BUCKET_CDN_URL
# - Set up CDN for static assets
# - Enable gzip compression

# 8. Monitoring
# - Enable error logging
# - Set up performance monitoring
# - Configure backup schedule

# 9. Security hardening
# - Disable directory listing
# - Set restrictive file permissions
# - Implement rate limiting
# - Configure firewall rules

Backup Strategy

# Database backup (daily)
mysqldump -u user -p database | gzip > backup-$(date +%Y%m%d).sql.gz

# File backup (to S3)
aws s3 sync /path/to/uploads/ s3://bucket-name/uploads/ --delete

# Automated backup script
#!/bin/bash
DATE=$(date +%Y%m%d)
mysqldump -u $DB_USER -p$DB_PASS $DB_NAME | gzip > /backups/db-$DATE.sql.gz
aws s3 sync /var/www/uploads/ s3://tribe-backups/uploads-$DATE/
find /backups -mtime +30 -delete  # Remove backups older than 30 days

License

This project is licensed under the GNU GPL v3 License.

Support