parse/php-sdk
Official Parse PHP SDK: a clean, Composer-ready client for Parse Server. Store and query objects, manage users and sessions, upload files, send push notifications, and run cloud functions—everything you need to add a full Parse backend to any PHP application with minimal setup.
Install and configure once per process.
composer require parse/php-sdk
Pin at least ^2.4 — 2.4.0 added PHP 8.3 support, so it is the first release safe to run on a modern PHP 8.3 runtime (see version pinning below for the caveats that remain).
Add credentials to config/services.php (never to config/database.php — this is a remote BaaS, not a connection):
'parse' => [
'app_id' => env('PARSE_APP_ID'),
'rest_key' => env('PARSE_REST_KEY'),
'master_key' => env('PARSE_MASTER_KEY'), // server-side only
'server_url' => env('PARSE_SERVER_URL', 'http://127.0.0.1:1337'),
'mount_path' => env('PARSE_MOUNT_PATH', '/parse'),
],
Initialize it in a service provider — the SDK is entirely static, so guard against double initialization (config cache, Octane workers, test bootstrap):
// app/Providers/ParseServiceProvider.php
public function boot(): void
{
if (Parse::$booted) return; // small wrapper with a static flag
$cfg = config('services.parse');
ParseClient::initialize(
$cfg['app_id'], $cfg['rest_key'], $cfg['master_key'],
$cfg['server_url'], $cfg['mount_path']
);
// The SDK default session storage expects $_SESSION, which Laravel never starts.
ParseClient::setStorage(new ParseMemoryStorage());
}
Where to look first: the README for the initialize signature, then src/Parse/ParseClient.php (base URL, HTTP client, storage), src/Parse/ParseObject.php (CRUD, batch, pointers) and src/Parse/ParseQuery.php (the full constraint surface). Everything else (ParseUser, ParseFile, ParseCloud, ParsePush, ParseSchema, ParseACL) is a thin REST wrapper you can read in a couple of minutes.
First use case: treat Laravel as the trusted admin/back-office layer. A typical first win is a small service that writes objects the mobile app reads, or runs a Cloud Code function:
$order = ParseObject::create('Order');
$order->set('status', 'paid');
$order->set('total', 4999);
$order->set('customer', ParseObject::createWithoutData('Customer', $customerId)); // pointer
$order->set('placedAt', new DateTime());
$order->save();
Wrap the SDK, don't scatter it. Create app/Services/Parse/ classes that take scalars/arrays and return DTOs. Reasons: the SDK throws on HTTP errors, returns ParseObject instances with magic get()/set(), and leaks remote class names into your controllers. One OrderRepository with find, create, updateStatus, paginate keeps the blast radius small and makes swapping in a fake trivial.
Map objects explicitly. Parse objects are untyped bags; hydrate into arrays or small DTOs at the boundary:
private function toOrder(ParseObject $o): array
{
return [
'id' => $o->getObjectId(),
'status' => $o->get('status'),
'total' => $o->get('total'),
'placed_at' => $o->get('placedAt')?->format(DATE_ATOM), // DateTime back
];
}
Queries are the bulk of day-to-day work. Compose constraints fluently; find() returns an array, first() a single object or null, count() an int.
$q = new ParseQuery('Order');
$q->equalTo('status', 'paid')
->greaterThan('createdAt', new DateTime('-7 days'))
->containedIn('region', ['eu', 'uk'])
->includeKey('customer') // resolve pointer in one round trip
->orderByDescending('createdAt')
->limit(100);
$orders = $q->find(true); // true = use master key
For "page 3 of the admin table", ->skip($page * $perPage)->limit($perPage) — there is no cursor pagination in this SDK. For OR logic use ParseQuery::orQueries([$q1, $q2]); for joins on a relation use ->matchesQuery('field', $otherQuery) instead of N+1 lookups.
Master key usage should be explicit and auditable. Every find, save, delete, fetch accepts a trailing $useMasterKey boolean. Default to false and only opt in inside admin services — that way ACL mistakes surface in staging instead of quietly being bypassed.
Auth flows. Don't rely on ParseUser::getCurrentUser(); in Laravel you have your own session. Instead accept the Parse session token from the client and call ParseUser::become($token), or validate it explicitly:
try {
ParseUser::become($request->bearerToken());
$user = ParseUser::getCurrentUser();
} catch (ParseException $e) {
abort(401); // 209 = invalid session token
}
Remember become() mutates static state — reset it (ParseUser::logOut()) or scope it inside a job/request, never share it across a queue worker's lifetime.
Files. ParseFile::createFromData($bytes, $name, $mime) then $file->save(), then attach the ParseFile to an object field. $file->getURL() gives you the CDN URL to store alongside your own record. Deleting the object does not delete the file — track URLs and clean up.
Cloud Code as your transaction boundary. Parse has no multi-object transactions. Anything that must be atomic (decrement inventory and create an order) belongs in a Cloud Code function called via ParseCloud::run('createOrder', $params), which does run server-side atomically. This is also where you keep business logic that mobile clients must not bypass.
Schema as code. ParseSchema::create('Order', ['fields' => [...]]) and ParseSchema::update(...) let you put class definitions in a seeder or a php artisan parse:schema command, which is the closest thing to a migration you get. Do this early — hand-created classes drift between environments.
/parse in URLs. server_url must be the host (http://host:1337) and mount_path the path (/parse). Passing http://host:1337/parse as server_url yields /parse/parse/classes/... and confusing 404s. Verify with a curl against /parse/classes/_User before blaming the SDK.become() or a re-initialize() in a test can poison subsequent requests. Initialize once, reset user state per job.$_SESSION storage. The default storage silently breaks in Laravel (no session started) — set ParseMemoryStorage or implement ParseStorageInterface on top of Laravel's session if you actually want SDK-managed user persistence.skip cost. limit() above 1000 is clamped; deep skip() gets slower. For exports, page with orderByAscending('createdAt') and a greaterThan cursor on the last seen timestamp instead of skip.ParseObject::saveAll()/destroyAll() hit the /batch endpoint, which accepts a bounded number of sub-requests (50). Chunk large collections yourself rather than assuming the SDK splits them.setArray() for lists and build associative payloads explicitly.DateTime in, get DateTime (UTC) out. Storing ISO strings works but breaks date-range queries and sorting; always use real Parse date fields for anything you will filter on.ParseException::getCode() returns the Parse error code: 101 object not found, 100 connection failure, 201/202 username missing/taken, 209 invalid session token, 119 invalid ACL. Catch ParseObjectNotFoundException where it matters, and log the code — messages are terse and occasionally misleading.try/catch, and retry only idempotent reads. For writes, use a client-generated idempotency key field rather than blind retries.parseplatform/parse-server + Mongo in a docker-compose.yml and point PARSE_SERVER_URL at it in phpunit.xml. For unit tests, swap the transport via ParseClient::setHttpClient() with an implementation of ParseHttpable that records and replays responses — that's the cleanest seam in the package.ParseACL on every object at creation ($obj->setACL($acl)) — defaults are public-read/write, which is rarely what you want. Role-based access requires the role to exist and the user to be in it; changing a role's membership does not retroactively fix objects with per-user ACLs.ParseObject::createWithoutData(). Pointing at an unsaved object throws.^2.4) rather than floating, run CI on the exact PHP version you deploy, and re-check the package's support matrix before jumping to a future PHP minor: the SDK has historically lagged new PHP releases, and deprecation noise from older internals is a real risk. Upgrading 2.3.x → 2.4.0 is a drop-in with no code changes required.How can I help you explore Laravel packages today?