olamilekan / laravel-google-sheets
A Laravel package for working with Google Sheets API with support for multiple spreadsheet connections
Package info
github.com/oluwatosinolamilekan/laravel-google-sheets
pkg:composer/olamilekan/laravel-google-sheets
Requires
- php: ^8.1
- google/apiclient: ^2.15
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.5|^11.5
This package is auto-updated.
Last update: 2026-06-01 11:41:13 UTC
README
A fluent Laravel package for reading, writing, and managing Google Sheets with first-class support for multiple spreadsheet connections.
Requirements
- PHP 8.1+
- Laravel 10, 11, 12, or 13
- A Google Cloud project with the Sheets API enabled
- A service account JSON credentials file
Installation
composer require olamilekan/laravel-google-sheets
Publish the configuration file:
php artisan vendor:publish --tag=google-sheets-config
Configuration
1. Credentials
Place your service account JSON file somewhere secure (e.g. storage/app/google/service-account.json) and set the path in your .env:
GOOGLE_SHEETS_CREDENTIALS_PATH=/path/to/service-account.json
2. Spreadsheet Connections
Define as many named connections as you need in config/google-sheets.php:
'sheets' => [ 'default' => [ 'spreadsheet_id' => env('GOOGLE_SHEETS_SPREADSHEET_ID'), 'sheet' => 'Sheet1', ], 'users' => [ 'spreadsheet_id' => env('GOOGLE_SHEETS_USERS_SPREADSHEET_ID'), 'sheet' => 'Users', ], 'reports' => [ 'spreadsheet_id' => env('GOOGLE_SHEETS_REPORTS_SPREADSHEET_ID'), 'sheet' => 'Monthly', ], ],
Set the default connection:
GOOGLE_SHEETS_DEFAULT_CONNECTION=default GOOGLE_SHEETS_SPREADSHEET_ID=your-spreadsheet-id-here
Usage
Using the Facade
use Olamilekan\GoogleSheets\Facades\GoogleSheets;
Reading Data
// All rows from the default connection (first row treated as headers) $rows = GoogleSheets::all(); // Specific range $rows = GoogleSheets::range('A1:D10')->get(); // First row only $row = GoogleSheets::first(); // Get column headers $headers = GoogleSheets::headers(); // Without header mapping (raw arrays) $rows = GoogleSheets::connection('users')->withoutHeaders()->get();
Querying Data
// Find rows where a column matches a value $admins = GoogleSheets::find('role', 'admin'); // Where clause with operators $highScores = GoogleSheets::where('score', '>=', 90); // Partial text matching $results = GoogleSheets::where('name', 'like', 'john');
Writing Data
// Append rows GoogleSheets::append([ ['Alice', 'alice@example.com', 'admin'], ['Bob', 'bob@example.com', 'user'], ]); // Update a specific range GoogleSheets::range('A2:C2')->update([ ['Alice Updated', 'alice-new@example.com', 'superadmin'], ]); // Batch update multiple ranges at once GoogleSheets::batchUpdate([ 'A2:C2' => [['Alice', 'alice@example.com', 'admin']], 'A3:C3' => [['Bob', 'bob@example.com', 'user']], ]); // Clear a range GoogleSheets::range('A2:C100')->clear();
Associative Rows, Upserts, And Validation
// Map associative arrays to the sheet's header row before appending GoogleSheets::connection('users')->appendAssoc([ ['name' => 'Alice', 'email' => 'alice@example.com', 'role' => 'admin'], ]); // Update existing rows by key column and append missing rows GoogleSheets::connection('users')->upsert('email', [ ['name' => 'Alice Updated', 'email' => 'alice@example.com', 'role' => 'owner'], ['name' => 'Charlie', 'email' => 'charlie@example.com', 'role' => 'user'], ]); // Validate imported rows with Laravel validation rules $validRows = GoogleSheets::connection('users')->validate([ 'name' => ['required', 'string'], 'email' => ['required', 'email'], ]); // Write row-level validation errors to an "Import Errors" tab before failing $validRows = GoogleSheets::connection('users')->validateWithErrorSheet([ 'name' => ['required', 'string'], 'email' => ['required', 'email'], ]); // Ensure required sheet headers exist GoogleSheets::connection('users')->requireHeaders(['name', 'email', 'role']);
Import Diff Preview
Preview the impact of an import before writing anything. The preview separates new, changed, deleted, invalid, and conflict rows.
use App\Models\User; use Olamilekan\GoogleSheets\Facades\GoogleSheets; $preview = GoogleSheets::connection('users') ->diffAgainst(User::query(), key: 'email') ->rules([ 'name' => ['required', 'string'], 'email' => ['required', 'email'], ]) ->preview(); $preview->counts(); // ['new' => 1, 'changed' => 2, 'deleted' => 0, 'invalid' => 1, 'conflicts' => 0] $preview->new; // rows in the sheet that are not in the query $preview->changed; // rows where sheet values differ from existing query values $preview->deleted; // query rows missing from the sheet $preview->invalid; // rows failing validation or missing the key $preview->conflicts; // duplicate key rows in the sheet or query
By default, changed rows compare sheet columns that also exist on the query/model row, excluding the key. You may narrow the comparison:
$preview = GoogleSheets::connection('users') ->diffAgainst(User::query(), key: 'email') ->only(['name', 'role']) ->except(['updated_at']) ->preview();
Sync Methods
Sync methods return a SyncReport with created, updated, deleted, skipped, conflict, and failed counts.
use App\Models\User; use Olamilekan\GoogleSheets\Facades\GoogleSheets; // Database / Eloquent -> Google Sheet $report = GoogleSheets::connection('users') ->syncFromModel(User::class, keyColumn: 'email', options: [ 'columns' => ['name', 'email', 'role'], 'conflict' => 'app_wins', ]); // Google Sheet -> Database / Eloquent $report = GoogleSheets::connection('users') ->syncToModel(User::class, keyColumn: 'email'); // CSV -> Google Sheet $report = GoogleSheets::connection('users') ->importCsv(storage_path('app/users.csv'), keyColumn: 'email'); // Google Sheet -> CSV $report = GoogleSheets::connection('users') ->exportCsv(storage_path('app/users-export.csv')); // API -> Google Sheet $report = GoogleSheets::connection('orders') ->syncFromApi('https://api.example.com/orders', keyColumn: 'order_id', options: [ 'data_key' => 'data', 'headers' => ['Authorization' => 'Bearer '.$token], ]); // Google Sheet -> API $report = GoogleSheets::connection('orders') ->syncToApi('https://api.example.com/orders/bulk'); // Conflict-aware two-way sync $report = GoogleSheets::connection('users') ->syncTwoWay(User::class, keyColumn: 'email', options: [ 'conflict' => 'fail', // app_wins, sheet_wins, skip, fail ]); $report->counts(); $report->created(); $report->updated(); $report->conflicts(); $report->errors();
Run syncs in the queue:
GoogleSheets::connection('users') ->queueSync('syncFromModel', [User::class, 'email'], queue: 'imports');
Every sync is audit logged through Laravel's logger and kept in the in-process audit log:
$records = GoogleSheets::connection('users')->syncAuditLog();
Notify teams when a sync finishes or fails:
GoogleSheets::connection('users')->syncFromModel(User::class, 'email', [ 'notify' => [ 'slack_webhook' => config('services.slack.sync_webhook'), 'mail_to' => 'ops@example.com', ], ]);
Import And Export Classes
use App\Models\User; use Olamilekan\GoogleSheets\Imports\SheetImport; class UsersImport extends SheetImport { public function target() { return User::query(); } public function key(): string { return 'email'; } public ?string $errorSheet = 'Import Errors'; public function rules(): array { return ['email' => ['required', 'email']]; } public function model(array $row): User { return User::updateOrCreate( ['email' => $row['email']], ['name' => $row['name']] ); } } GoogleSheets::import(new UsersImport(), 'users');
Preview an import from the command line without writing rows:
php artisan google-sheets:sync "App\\Imports\\UsersImport" users --dry-run
Dry-run imports compare the sheet rows against the import class target() using the column returned by key(). Validation rules are applied when the import defines rules().
use App\Models\Report; use Olamilekan\GoogleSheets\Exports\SheetExport; class ReportsExport extends SheetExport { public bool $replace = true; public function headings(): array { return ['Date', 'Name', 'Total']; } public function collection() { return Report::query() ->latest() ->get() ->map(fn (Report $report) => [ $report->created_at->toDateString(), $report->name, $report->total, ]); } } GoogleSheets::export(new ReportsExport(), 'reports');
Multiple Connections
// Switch between configured connections $users = GoogleSheets::connection('users')->all(); $reports = GoogleSheets::connection('reports')->all(); // Create an ad-hoc connection to any spreadsheet $data = GoogleSheets::make('some-spreadsheet-id', 'TabName')->all();
Switching Sheets (Tabs) at Runtime
$sheet = GoogleSheets::connection('default'); $sheet1Data = $sheet->sheet('Sheet1')->all(); $sheet2Data = $sheet->sheet('Sheet2')->all();
Sheet / Tab Management
// List all sheet tabs in a spreadsheet $tabs = GoogleSheets::listSheets(); // ['Sheet1', 'Users', 'Reports'] // Check if a tab exists GoogleSheets::sheetExists('Users'); // true // Create a new tab GoogleSheets::createSheet('Archive'); // Duplicate an existing tab GoogleSheets::duplicateSheet('Sheet1', 'Sheet1 Copy'); // Delete a tab GoogleSheets::deleteSheet('Archive');
Caching
Enable caching in config or at runtime to reduce API calls:
// In config/google-sheets.php 'cache' => [ 'enabled' => true, 'store' => 'redis', 'ttl' => 300, // seconds 'prefix' => 'google_sheets_', ], // At runtime $rows = GoogleSheets::enableCache(600)->all(); $rows = GoogleSheets::disableCache()->all();
Write operations now clear remembered read cache keys for the active spreadsheet, so cached ranges are refreshed after updates.
Retry And Backoff
Transient Google Sheets API failures are retried by default, including rate limits, quota throttling, and backend errors. The delay uses exponential backoff with jitter.
// In config/google-sheets.php 'retry' => [ 'enabled' => true, 'attempts' => 3, 'delay' => 250, // milliseconds 'max_delay' => 5000, // milliseconds ], // At runtime $rows = GoogleSheets::withRetries(attempts: 5, delay: 500)->all(); $rows = GoogleSheets::withoutRetries()->all();
Chunked Processing
GoogleSheets::chunk(100, function ($chunk) { foreach ($chunk as $row) { // process each row } }); GoogleSheets::lazy(500)->each(function (array $row) { // process one row at a time });
Formatting, Formulas, And Named Ranges
GoogleSheets::connection('reports') ->sheet('Monthly') ->boldHeader() ->freezeRows(1) ->autoResizeColumns(1, 4); GoogleSheets::connection('reports')->append([ ['Total', GoogleSheets::formula('SUM(C2:C100)')], ]); $summaryRows = GoogleSheets::connection('reports') ->namedRange('MonthlySummary') ->get();
Testing
use Olamilekan\GoogleSheets\Facades\GoogleSheets; $fake = GoogleSheets::fake([ 'users' => [ ['name' => 'Alice', 'email' => 'alice@example.com'], ], ]); GoogleSheets::connection('users')->appendAssoc([ ['name' => 'Bob', 'email' => 'bob@example.com'], ]); $fake->assertAppended('users', ['name' => 'Bob', 'email' => 'bob@example.com']);
Artisan Commands
php artisan google-sheets:list users php artisan google-sheets:clear reports --sheet=Monthly --range=A2:D100 php artisan google-sheets:sync "App\\Imports\\UsersImport" users php artisan google-sheets:sync "App\\Imports\\UsersImport" users --dry-run php artisan google-sheets:sync "App\\Exports\\ReportsExport" reports
Spreadsheet Metadata
$title = GoogleSheets::getTitle(); $id = GoogleSheets::getSpreadsheetId();
Dependency Injection
use Olamilekan\GoogleSheets\GoogleSheetsManager; class UserImportService { public function __construct( protected GoogleSheetsManager $sheets ) {} public function import(): void { $rows = $this->sheets->connection('users')->all(); foreach ($rows as $row) { User::updateOrCreate( ['email' => $row['email']], ['name' => $row['name']] ); } } }
API Reference
GoogleSheetsManager
| Method | Description |
|---|---|
connection(?string $name) |
Get a named connection (lazy-loaded & cached) |
make(string $spreadsheetId, string $sheet) |
Create an ad-hoc sheet instance |
getDefaultConnection() |
Get the default connection name |
purge(?string $name) |
Remove a resolved connection |
reconnect(?string $name) |
Purge and re-resolve a connection |
Sheet
| Method | Returns | Description |
|---|---|---|
spreadsheet(string $id) |
static |
Override the spreadsheet ID |
sheet(string $name) |
static |
Switch to a different tab |
range(string $range) |
static |
Set A1 range for the next operation |
get() |
Collection |
Read rows (headers mapped) |
all() |
Collection |
Read all rows from the sheet |
first() |
?array |
First data row |
last() |
?array |
Last data row |
headers() |
array |
Column headers (row 1) |
find(col, val) |
Collection |
Filter rows by column value |
where(col, op, val) |
Collection |
Filter with comparison operators |
chunk(size, cb) |
void |
Process rows in chunks |
append(array $rows) |
int |
Append rows (returns row count) |
update(array $rows) |
int |
Update range (returns row count) |
batchUpdate(array $data) |
int |
Update multiple ranges |
clear() |
bool |
Clear values in range |
appendAssoc(array) |
int |
Append associative rows mapped to sheet headers |
updateAssoc(array) |
int |
Update associative rows mapped to sheet headers |
upsert(key, rows) |
int |
Update rows by key column and append missing rows |
syncRows(rows, key, options) |
SyncReport |
Sync array/collection rows into the sheet |
syncFromModel(model, key, options) |
SyncReport |
Sync Eloquent model records into the sheet |
syncToModel(model, key, options) |
SyncReport |
Sync sheet rows into an Eloquent model |
importCsv(path, key, options) |
SyncReport |
Sync a CSV file into the sheet |
exportCsv(path, options) |
SyncReport |
Export sheet rows to a CSV file |
syncFromApi(url, key, options) |
SyncReport |
Pull JSON rows from an API into the sheet |
syncToApi(url, options) |
SyncReport |
Push sheet rows to an API endpoint |
syncTwoWay(target, key, options) |
SyncReport |
Run conflict-aware two-way sync |
queueSync(method, args, queue) |
PendingDispatch |
Dispatch a sync method to Laravel's queue |
syncAuditLog() |
Collection |
Read in-process sync audit records |
validate(rules) |
Collection |
Validate rows with Laravel validation rules |
requireHeaders(array) |
static |
Ensure required headers exist |
lazy(size) |
LazyCollection |
Iterate rows lazily from a collection-backed read |
createSheet(string) |
static |
Add a new tab |
deleteSheet(string) |
bool |
Remove a tab |
duplicateSheet(src, new) |
static |
Copy a tab |
listSheets() |
array |
List all tab names |
sheetExists(string) |
bool |
Check if a tab exists |
namedRange(string) |
static |
Set a named range for the next operation |
listNamedRanges() |
array |
List named ranges |
formula(string) |
string |
Create a formula cell value |
boldHeader() |
static |
Bold the first row |
freezeRows(int) |
static |
Freeze leading rows |
autoResizeColumns(start, end) |
static |
Auto-resize columns |
formatRange(range, format) |
static |
Apply cell formatting |
withHeaders() |
static |
Map first row as keys (default) |
withoutHeaders() |
static |
Return raw arrays |
enableCache(?int $ttl) |
static |
Enable caching |
disableCache() |
static |
Disable caching |
withRetries(?attempts, ?delay) |
static |
Enable retries and optionally override attempts/delay |
withoutRetries() |
static |
Disable retries for the current sheet instance |
Environment Variables
| Variable | Default | Description |
|---|---|---|
GOOGLE_SHEETS_CREDENTIALS_PATH |
storage/app/google/service-account.json |
Path to credentials |
GOOGLE_SHEETS_DEFAULT_CONNECTION |
default |
Default connection name |
GOOGLE_SHEETS_SPREADSHEET_ID |
— | Spreadsheet ID for default connection |
GOOGLE_SHEETS_APPLICATION_NAME |
Laravel Google Sheets |
App name for API requests |
GOOGLE_SHEETS_CACHE_ENABLED |
false |
Enable response caching |
GOOGLE_SHEETS_CACHE_STORE |
null (default driver) |
Cache store to use |
GOOGLE_SHEETS_CACHE_TTL |
300 |
Cache lifetime in seconds |
GOOGLE_SHEETS_RETRY_ENABLED |
true |
Retry transient Google API failures |
GOOGLE_SHEETS_RETRY_ATTEMPTS |
3 |
Maximum attempts per API call |
GOOGLE_SHEETS_RETRY_DELAY |
250 |
Initial retry delay in milliseconds |
GOOGLE_SHEETS_RETRY_MAX_DELAY |
5000 |
Maximum retry delay in milliseconds |
GOOGLE_SHEETS_VALUE_RENDER |
FORMATTED_VALUE |
Value render option |
GOOGLE_SHEETS_VALUE_INPUT |
USER_ENTERED |
Value input option |
License
MIT