wjbecker/current-rms-php

A framework-agnostic PHP library for the Current RMS API with fluent query builder, pagination, and collections

Installs: 3

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/wjbecker/current-rms-php

v1.2.0 2026-01-09 15:30 UTC

This package is auto-updated.

Last update: 2026-01-09 15:32:24 UTC


README

A framework-agnostic PHP library for interacting with the Current RMS API. Provides both a flexible generic HTTP client and typed endpoint classes for common operations.

composer require wjbecker/current-rms-php

Features

  • Framework Agnostic: Works standalone or with Laravel (zero required dependencies)
  • Fluent Query Builder: Build queries with chainable methods instead of arrays
  • Memory-Efficient Pagination: Generator-based cursor for processing large datasets
  • Custom Collection Class: Lightweight collection with map, filter, reduce, and more
  • Paginator with Navigation: Page metadata with next/previous navigation
  • Type-Safe DTOs: Strongly-typed data objects with helper methods
  • Guzzle HTTP Client: Built on Guzzle for reliable HTTP requests
  • Laravel Integration: Optional service provider with auto-discovery and facade

Installation

composer require wjbecker/current-rms-php

Quick Start

Standalone Usage (No Laravel)

use Wjbecker\CurrentRms\Client\Auth\ApiKeyAuth;
use Wjbecker\CurrentRms\Client\CurrentRmsClient;

// Create authentication
$auth = new ApiKeyAuth(
    subdomain: 'yourcompany',
    apiToken: 'your-api-token-here'
);

// Create client
$client = new CurrentRmsClient(
    baseUrl: 'https://api.current-rms.com/api/v1',
    auth: $auth
);

// Use the client
$opportunities = $client->opportunities()->list();
$opportunity = $client->opportunities()->find(123);

Laravel Usage

1. Publish Configuration (Optional)

php artisan vendor:publish --tag=current-rms-config

2. Configure Environment

Add to your .env file:

CURRENT_RMS_API_BASE_URL=https://api.current-rms.com/api/v1
CURRENT_RMS_AUTH_TYPE=api_key
CURRENT_RMS_SUBDOMAIN=yourcompany
CURRENT_RMS_API_TOKEN=your-api-token-here

3. Use the Client

Via Dependency Injection:

use Wjbecker\CurrentRms\Client\CurrentRmsClient;

class OpportunityController extends Controller
{
    public function __construct(
        private CurrentRmsClient $client
    ) {}

    public function index()
    {
        $opportunities = $this->client->opportunities()->list();
        return view('opportunities.index', compact('opportunities'));
    }
}

Via Facade:

use Wjbecker\CurrentRms\Facades\CurrentRms;

$opportunities = CurrentRms::opportunities()->list();
$opportunity = CurrentRms::opportunities()->find(123);

Query Builder

Build queries with a fluent interface instead of awkward arrays:

// Find confirmed opportunities for a specific member
$orders = $client->opportunities()->query()
    ->whereState(3)  // Confirmed
    ->forMember(456)
    ->createdAfter('2025-01-01')
    ->with('member', 'venue')
    ->get();

// Search by subject
$results = $client->opportunities()->query()
    ->whereContains('subject', 'Wedding')
    ->whereBetween('starts_at', '2025-06-01', '2025-08-31')
    ->get();

// Filter opportunity items
$rentalItems = $client->opportunityItems()->query()
    ->whereEquals('transaction_type', 1)  // Rentals
    ->forOpportunity(123)
    ->with('item')
    ->get();

// Complex queries with Ransack predicates
$items = $client->opportunityItems()->query()
    ->where('quantity', 'gteq', 5)           // quantity >= 5
    ->where('item_id', 'in', [1, 2, 3])      // item_id in array
    ->whereNotNull('charge_total')
    ->get();

Available Query Methods

// Comparison operators
->whereEquals('field', $value)
->whereNotEquals('field', $value)
->whereGreaterThan('field', $value)
->whereGreaterThanOrEqual('field', $value)
->whereLessThan('field', $value)
->whereLessThanOrEqual('field', $value)

// String matching
->whereContains('field', 'value')       // LIKE %value%
->whereContainsAll('field', ['a', 'b']) // Contains ALL values (AND)
->whereContainsAny('field', ['a', 'b']) // Contains ANY value (OR)
->whereNotContains('field', 'value')    // Does NOT contain
->whereStartsWith('field', 'value')
->whereNotStartsWith('field', 'value')
->whereEndsWith('field', 'value')
->whereNotEndsWith('field', 'value')
->whereMatches('field', '%pattern%')    // SQL LIKE pattern
->whereNotMatches('field', '%pattern%')

// Array/Null/Presence checks
->whereIn('field', [1, 2, 3])
->whereNotIn('field', [1, 2, 3])
->whereNull('field')
->whereNotNull('field')
->wherePresent('field')    // Not null AND not blank
->whereBlank('field')      // Null OR empty string
->whereTrue('field')
->whereFalse('field')

// Date ranges
->whereBetween('field', $start, $end)
->createdAfter('2025-01-01')
->createdBefore('2025-12-31')
->createdBetween('2025-01-01', '2025-12-31')
->updatedAfter('2025-01-01')

// Convenience methods
->whereState(3)           // Filter by state
->forMember($memberId)    // Filter by member
->forOpportunity($id)     // Filter by opportunity
->forItem($itemId)        // Filter by item

// Includes and pagination
->with('item', 'member')  // Include associations
->perPage(50)             // Set page size

Complex Queries with AND/OR Logic

For more complex queries, use whereOr() and whereAnd() to group conditions:

// Simple OR: name contains "Bill" OR name contains "Fred"
$results = $client->opportunities()->query()
    ->whereOr(function ($or) {
        $or->whereContains('name', 'Bill');
        $or->whereContains('name', 'Fred');
    })
    ->get();

// Simple AND: state = 3 AND member_id = 123 (grouped)
$results = $client->opportunities()->query()
    ->whereAnd(function ($and) {
        $and->whereEquals('state', 3);
        $and->whereEquals('member_id', 123);
    })
    ->get();

// Complex: (name = "Bill" AND active = true) OR (name = "Fred" AND active = true)
$results = $client->opportunities()->query()
    ->whereOr(function ($or) {
        $or->group(function ($g) {
            $g->whereEquals('name', 'Bill');
            $g->whereTrue('active');
        });
        $or->group(function ($g) {
            $g->whereEquals('name', 'Fred');
            $g->whereTrue('active');
        });
    })
    ->get();

// Combine regular filters with grouped conditions
$results = $client->opportunities()->query()
    ->whereState(3)  // Regular filter (always applied)
    ->whereOr(function ($or) {
        $or->whereContains('subject', 'Wedding');
        $or->whereContains('subject', 'Corporate');
    })
    ->with('member')
    ->get();

// Date range OR queries
$results = $client->opportunities()->query()
    ->whereOr(function ($or) {
        $or->group(function ($g) {
            $g->whereBetween('starts_at', '2025-01-01', '2025-06-30');
        });
        $or->group(function ($g) {
            $g->whereBetween('starts_at', '2025-07-01', '2025-12-31');
        });
    })
    ->get();

How it works:

  • Each where* call inside whereOr()/whereAnd() creates a separate condition group
  • Groups are combined with OR (for whereOr) or AND (for whereAnd)
  • Use group() to bundle multiple conditions that should be ANDed together within a group
  • Regular filters (outside of whereOr/whereAnd) are always applied in addition to grouped conditions

Query Execution Methods

// Get a Collection of results
$results = $query->get();

// Get just the first result
$item = $query->first();

// Get paginated results with metadata
$page = $query->paginate(1);

// Iterate through all pages (memory-efficient)
foreach ($query->cursor() as $item) {
    // Process one item at a time
}

// Check existence
$exists = $query->exists();

// Get count (requires API call)
$count = $query->count();

Pagination

Basic Pagination

// Get a specific page
$page = $client->opportunities()->paginate(page: 1, perPage: 25);

// Access items on current page
foreach ($page->items() as $opportunity) {
    echo $opportunity->subject;
}

// Check pagination metadata
echo "Page {$page->currentPage()} of {$page->lastPage()}";
echo "Total items: {$page->total()}";
echo "Has more pages: " . ($page->hasMorePages() ? 'yes' : 'no');

Page Navigation

// Navigate between pages
$nextPage = $page->nextPage();
$prevPage = $page->previousPage();
$specificPage = $page->goToPage(5);

Memory-Efficient Iteration (Generators)

For large datasets, use the cursor() method which yields items one at a time:

// Process thousands of items without loading all into memory
foreach ($client->opportunityItems()->cursor() as $item) {
    echo "{$item->getItemName()} - Qty: {$item->quantity}\n";
    // Each item is fetched as needed, pages are loaded lazily
}

// With filters via query builder
foreach ($client->opportunityItems()->query()->forOpportunity(123)->cursor() as $item) {
    processItem($item);
}

Pagination Limits

  • Opportunities: Max 25 items per page
  • Other Endpoints: Max 100 items per page (default)

Collections

The package includes a lightweight Collection class:

$opportunities = $client->opportunities()->list();

// Filtering
$confirmed = $opportunities->filter(fn($o) => $o->state === 3);
$drafts = $opportunities->where('state', 1);

// Mapping
$titles = $opportunities->map(fn($o) => $o->getTitle());
$subjects = $opportunities->pluck('subject');

// Aggregation
$total = $opportunities->sum('charge_total');
$average = $opportunities->avg('charge_total');

// Iteration
$opportunities->each(function($o) {
    echo $o->subject;
});

// Sorting
$sorted = $opportunities->sortBy('starts_at');

// Other operations
$first = $opportunities->first();
$last = $opportunities->last();
$count = $opportunities->count();
$chunk = $opportunities->take(5);

// Convert to array
$array = $opportunities->toArray();

Lazy Loading Relationships

DTOs returned from endpoints support lazy loading of related resources:

// Get opportunities
$opportunities = $client->opportunities()->list();

foreach ($opportunities as $opportunity) {
    // Lazy load items for each opportunity (makes API call)
    $items = $opportunity->items()->get();

    foreach ($items as $item) {
        echo "{$item->name} - Qty: {$item->quantity}\n";
    }
}

How It Works

When you retrieve data through endpoints (list(), find(), create(), etc.), the client reference is automatically injected into the DTOs. This enables the items() method to make API calls on your behalf.

// These all support lazy loading
$opportunity = $client->opportunities()->find(123);
$opportunity = $client->opportunities()->list()->first();
$opportunity = $client->opportunities()->query()->whereState(3)->first();

// Access items without knowing the opportunity ID
$items = $opportunity->items()->get();
$items = $opportunity->items()->query()->whereEquals('transaction_type', 1)->get();

When Lazy Loading is NOT Available

If you create a DTO manually without the client, lazy loading will throw an exception:

// Manual creation - no client available
$opportunity = OpportunityData::from(['id' => 123]);
$opportunity->items(); // Throws RuntimeException

// Use the explicit endpoint instead
$items = $client->opportunities()->items(123)->list();

API Reference

Opportunities Endpoint

// List all opportunities
$opportunities = $client->opportunities()->list();

// List with filters (legacy array syntax)
$opportunities = $client->opportunities()->list([
    'q[state_eq]' => 1
]);

// Find specific opportunity
$opportunity = $client->opportunities()->find(123);

// Find with includes
$opportunity = $client->opportunities()->find(123, ['owner', 'member']);

// Create opportunity
$opportunity = $client->opportunities()->create([
    'subject' => 'New Project',
    'member_id' => 456,
    'starts_at' => '2025-01-15T08:00:00.000Z',
    'ends_at' => '2025-01-16T18:00:00.000Z',
]);

// Update opportunity
$opportunity = $client->opportunities()->update(123, [
    'subject' => 'Updated Project Name'
]);

// Delete opportunity
$client->opportunities()->destroy(123);

// Checkout (convert to order)
$opportunity = $client->opportunities()->checkout([
    'opportunity_id' => 123
]);

// Clone opportunity
$newOpportunity = $client->opportunities()->clone(123);

// Finalize check-in
$opportunity = $client->opportunities()->finalizeCheckIn(123, [
    'return' => [
        'return_at' => '2025-01-15T18:00:00.000Z'
    ],
    'move_outstanding' => false,
    'complete_sales_items' => false,
]);

Opportunity Items Endpoint

// List all opportunity items
$items = $client->opportunityItems()->list();

// List with includes (item data, assets, etc.)
$items = $client->opportunityItems()->list([], ['item', 'item_assets']);

// List items for specific opportunity (scoped)
$items = $client->opportunities()->items(123)->list();

// Find specific item
$item = $client->opportunities()->items(123)->find(456);

// Find with includes
$item = $client->opportunities()->items(123)->find(456, ['item', 'rate_definition']);

// Create item
$item = $client->opportunities()->items(123)->create([
    'item_id' => 789,
    'quantity' => 5
]);

// Update item
$item = $client->opportunities()->items(123)->update(456, [
    'quantity' => 10
]);

// Delete item
$client->opportunities()->items(123)->destroy(456);

Generic HTTP Methods

// GET request
$data = $client->get('/endpoint', ['param' => 'value']);

// POST request
$data = $client->post('/endpoint', ['key' => 'value']);

// PUT request
$data = $client->put('/endpoint', ['key' => 'value']);

// PATCH request
$data = $client->patch('/endpoint', ['key' => 'value']);

// DELETE request
$client->delete('/endpoint');

Configuration Options

When creating the client manually:

$client = new CurrentRmsClient(
    baseUrl: 'https://api.current-rms.com/api/v1',  // required
    auth: $auth,                                      // optional
    timeout: 30,                                      // optional, default: 30
    connectTimeout: 10,                               // optional, default: 10
    verifySsl: true                                   // optional, default: true
);

Data Objects

All endpoints return strongly-typed Data Transfer Objects (DTOs) with helper methods.

OpportunityData

// Properties (all nullable)
$opportunity->id;
$opportunity->subject;
$opportunity->state;              // State ID (1=draft, 2=quote, 3=order, etc.)
$opportunity->state_name;         // "Draft", "Quote", "Order", etc.
$opportunity->status;
$opportunity->starts_at;
$opportunity->ends_at;
$opportunity->charge_total;
$opportunity->opportunity_items;  // Array of OpportunityItemData (if included)

// Helper methods
$opportunity->isDraft();          // Check if in draft state
$opportunity->isOpen();           // Check if status is open
$opportunity->getTitle();         // Get subject/title
$opportunity->getMemberName();    // Get customer name from nested data
$opportunity->getOwnerName();     // Get owner name from nested data
$opportunity->getCustomField('key'); // Get custom field value

// Lazy loading (when retrieved via endpoint)
$opportunity->items();            // Get ScopedOpportunityItemsEndpoint
$opportunity->items()->get();     // Fetch all items
$opportunity->items()->query()->whereEquals('transaction_type', 1)->get();

OpportunityItemData

// Properties (all nullable)
$item->id;
$item->opportunity_id;
$item->item_id;               // Product/Item ID
$item->transaction_type;      // 1=Rental, 2=Sale, 3=Service
$item->quantity;
$item->name;
$item->charge;
$item->item;                  // ItemData object (if included)

// Helper methods
$item->isRental();            // Check if rental (transaction_type === 1)
$item->isSale();              // Check if sale (transaction_type === 2)
$item->isService();           // Check if service (transaction_type === 3)
$item->getItemName();         // Get item name (from item or name field)
$item->getItemBarcode();      // Get barcode (from item or sku field)
$item->getCustomField('key'); // Get custom field value

ItemData (Product/Item)

// Available when including 'item' association
$item = $opportunityItem->item;

// Properties
$item->id;
$item->name;
$item->description;
$item->barcode;
$item->active;
$item->replacement_charge;
$item->weight;

// Helper methods
$item->isActive();
$item->isAccessoryOnly();
$item->isDiscountable();
$item->getProductGroupName();
$item->getTaxClassName();
$item->getIconUrl();
$item->getCustomField('key');

Testing

./vendor/bin/pest

Package Structure

.
├── src/
│   ├── Client/
│   │   ├── CurrentRmsClient.php
│   │   ├── Auth/
│   │   │   ├── AuthManager.php
│   │   │   ├── ApiKeyAuth.php
│   │   │   └── TokenStorage.php
│   │   └── Exceptions/
│   ├── Data/
│   │   ├── OpportunityData.php
│   │   ├── OpportunityItemData.php
│   │   └── ItemData.php
│   ├── Endpoints/
│   │   ├── BaseEndpoint.php
│   │   ├── OpportunitiesEndpoint.php
│   │   ├── OpportunityItemsEndpoint.php
│   │   └── ScopedOpportunityItemsEndpoint.php
│   ├── Query/
│   │   ├── QueryBuilder.php
│   │   └── GroupBuilder.php
│   ├── Support/
│   │   ├── Collection.php
│   │   └── Paginator.php
│   ├── Facades/
│   │   └── CurrentRms.php
│   └── CurrentRmsServiceProvider.php
├── config/
│   └── current-rms.php
├── tests/
├── LICENSE
├── README.md
└── composer.json

License

MIT License. See LICENSE for more information.

Credits

  • Author: William Becker
  • Package: wjbecker/current-rms-php