borsche/google-drive-docs-bundle
A Symfony bundle that turns a Google Shared Drive into a document workspace inside your own app. Users browse folders, create and edit real Google Sheets/Docs, and manage access, while documents stay in Google and editors render in an embedded iframe.
The Google client is built lazily. Authenticating asks Google for an access token, and that
happened while the client was being constructed — so every request to a controller holding
DriveDocumentService paid for a token whether it called Drive or not. Measured in a compiled
container: fetching the service takes 1.9 ms lazily against 242.9 ms eagerly.
It defers the token request rather than removing it: the first call that reaches Drive pays for it, so a request that does use Drive is no faster. What changes is that a request which never touches Drive stops paying at all.
Verified through the paths where proxying could plausibly break, against the live API and beside a
plain client for comparison: a resource call, export(), exportRevision() through authorize(),
a resumable upload over 5 MB, and the retry configuration read back. Byte-identical results, and
the initializer runs exactly once. The client is the right thing to defer and Drive is not —
Google\Service's constructor only type-checks its argument, and the client has no public
properties, while Drive exposes its resources as twenty-two
DRIVE_* attributes does not make the policy stricter, and the
README had been inviting exactly that. Symfony's default decision strategy is affirmative, so
one granting voter is enough: a stricter voter of yours votes DENY, this bundle's votes GRANT, and
access is allowed — whichever order they are registered in. Measured against Symfony's own
AccessDecisionManager and written down as a table, together with what to do instead (replace the
service definition, keeping one voter on those attributes) and why switching the whole application
to unanimous is the wider change. The worked example was checked against the abstract Voter it
extends, since DriveVoter is final and the obvious advice — subclass it — does not compileseesEverything() grants all four attributes, including DRIVE_DELETE. That was stated, but
in a sentence easy to read past, and the name invites wiring it to a read-only role: an auditor or
a support agent who should see the whole drive is the case where it looks harmless. It is a full
bypass of these checks, not a visibility flag, and the README now says so where the decision is
made, with the read-everything-change-nothing alternativeDriveVoter behind Symfony's own AccessDecisionManager —
a reader granted DRIVE_VIEW and refused DRIVE_EDIT, DRIVE_SHARE and DRIVE_DELETE; a
writer passing all four; a viewer with no grant refused everything; seesEverything()
bypassing; an empty subject abstained on rather than sent to Drive; and DriveDocumentResolver
raising AccessDeniedException for an unreachable id before a controller body would run. 42
checks with a second identity configured, 30 and four skips withouttools is in PHPStan's analysed paths, so a script that exists to check the bundle is itself
checked on every run of the static-analysis job. Getting it there meant removing the global
counters an analyser cannot follow — they are static properties of one small class now — and
moving the exit() out of the finally block, where it discarded whatever the body was raising.
That last one is the same mistake this script had already found in one of its own checkstools/smoke-test.php works through the
bundle's surface against a real Shared Drive and erases everything it creates, including after a
fatal error. It was a private script until now, which made the one mechanism that catches what
mocks cannot something nobody else could run — and the two bugs fixed in 1.0.4 are what it found.
Credentials come from the environment (or a file named by SMOKE_ENV_FILE), what it creates can
be renamed with SMOKE_PREFIX, and SMOKE_SECOND_EMAIL enables the two sharing checks that need
a second Google identitysetAppProperties() was annotated array<string, string|int|float|bool|null>, which is not what
PHP hands it: a key like "2024" arrives as the integer 2024. Callers doing nothing wrong got a
false error from their own PHPStan on code the method handles correctly — the casts inside it exist
for exactly that case. The annotation admits array-key now and says why. Found by bringing the
live test under static analysiswebViewLink was framed from another origin over plain HTTP in Chrome, the editor
rendered, and no X-Frame-Options or frame-ancestors refusal appeared. Written down with its
two caveats — the behaviour is undocumented by Google, and only Chrome was checked. Also noted
that docs.google.com/spreadsheets/, the marketing root rather than a document, does answer
X-Frame-Options: SAMEORIGIN, so a headers-only check on the wrong URL concludes the oppositeViewerContextInterface, and that the bundle reports rather than enforces — were a
paragraph each in the middle of the README. They are now a section of their own near the top,
where someone evaluating the bundle will actually meet themsetExpiry() never worked. Drive refuses a permissions.update whose body carries no role —
400, "The permission role field is required" — however little of the grant is being changed, so
every call failed whatever was asked of it. The grant is now read first and its own role sent
back unchanged; reading it rather than taking it from the caller is deliberate, since a role
passed in from a stale DrivePermission would quietly change what someone may do
Lifting an expiry left it in place. setExpiry($fileId, $permissionId, null) sent a JSON
null in the body and Drive answered with the old time still on the grant, so access went on
ending at a date the caller had cleared. permissions.update lifts an expiry only when asked to
in the query, and that parameter is what travels now — never alongside a new expiry, which Drive
would then drop again
The 1.0.2 release claimed this fixed; it was not. The test behind that claim asserted the request body, which was the right instinct after the release before it, but the body was never where the answer lay. Both fixes were found by running the bundle against a real Shared Drive and are verified there, not against a mock
cannotSetExpiration, saying only "Expiration dates cannot be set on this
item" — the item is fine, it is the pairing it objects to. A file's grant may expire in any of the
three roles. Measured across every combination rather than read off a pagekeptForever simply stays false.
The README had recommended keepRevision() for exactly the case it does not cover; for a Sheet or
a Doc, keeping a version means exporting itcolorStyle, the field Google asks for since the
plain color fields were deprecated, and the field masks name the twin accordingly — a mask and
the field it sets have to agree. Affects style(), borders(), conditionalFormat(),
bandedRows() and tabColor(). The bundle's own API is unchanged: colours are still #RRGGBB
or #RGB strings and the result on the sheet is the sameSpreadsheetService and SheetFormatter each carried a
copy of, moved to SheetTabIndex. The two copies had already needed the same null guard applied
separately once, which is the kind of fix that looks done and is not(string) cast on an array key looks redundant it is now explained: PHP turns a key like
"2024" into the integer 2024, so a numerically titled tab or app-property key arrives as an
int however the array is annotatedGoogle\Service\Exception is
passed through rather than wrapped, and only the cases the bundle can explain better get an
exception of their ownsetExpiry($fileId, $permissionId, null) did not lift the expiry. The Google client drops a PHP
null when it serialises a request body, so the field never reached Drive, and permissions.update
being a PATCH, an absent field means "keep the old value" — the grant kept expiring when it always
had. The placeholder the client provides for a JSON null is now used, and the test reads the
serialised body rather than the object's getter, which is how the omission went unnoticedpermission_cache.ttl however soon the grant ran out, so a viewer kept their access for up to
the TTL after Drive had dropped it. The entry now lives no longer than the soonest expiry among
the grants it holds, and a grant whose time is already up is ignored even while Drive still
lists itexportRevision() streamed an error page as the document: the export links are fetched through
the client's own authorised HTTP client, which is built with http_errors off, so a 403 or a
5xx came back as the revision's content. Any status of 400 or above is now raised as a Google
exception carrying Drive's own message and reasonchangesSince() reported changes to the Shared Drive itself — a rename, a new restriction — as
a DriveChange with an empty fileId, and cleared a cache entry under that empty key. The feed
carries them with a changeType of drive; they are now skipped, so every change is about a file.
That field is also asked for now: an unrequested field comes back null, so the check on it was
dead code quietly leaning on the empty-fileId test beside it — the two conditions are separate
on purpose and each is tested on its ownsetExpiry() explains an inherited grant with InheritedPermissionException, as revoke()
already did, instead of passing on Drive's bare 403DriveVoter abstains on an empty string as the subject instead of asking Google about the id
"" and earning a 400exportRevision() without a MIME type refuses a revision that offers several formats, naming
them, instead of taking whichever link Google listed first. Google's order is not a contract;
a revision offering one format still needs no choice, and an uploaded file's revision never did.
This is the one entry worth a second look before upgrading, because it refuses a call that used to be accepted. It sits here as a fix rather than waiting for a major release: what it replaces was an arbitrary pick from an order Google never promised, so it produced whichever format happened to be first — nothing a caller could have depended on deliberately, and quietly wrong when it was not the one they meant. Name the format and the call behaves as it always should have
Documentation: changesSince() is the drive's feed and is not filtered per viewer; the id
fallback of DriveDocumentResolver on routes where {id} is something else; what Drive
refuses to deleteRevision(); the sharing cache and expiring grants; DRIVE_EDIT also
covers lock() and the application's own metadata
DriveDocumentResolverTest collected the resolver's
answer with iterator_to_array(), which only accepts a plain array from PHP 8.2 onwards, so
six tests errored on the oldest PHP this bundle supports. The bundle itself was unaffected —
only its suite — and the CI job added in 0.7.1 to run against the lowest dependencies is what
caught itThe public API is settled. From here on, minor releases add and only a major one removes — see UPGRADE.md, which now covers every step from 0.1.0 and states the promise in full.
DriveDocumentService::delete(), deprecated since 0.3.0. It erased an item for good, which its
name never said: use deleteForever() for that behaviour, or trash() for what a delete button
in a UI probably meansDriveDocumentService::MAX_UPLOAD_BYTES, deprecated since 0.7.0 when resumable uploads removed
the ceiling it described. MULTIPART_LIMIT is where the two upload paths part; upload.max_bytes
is for a limit of your ownsymfony/deprecation-contracts is no longer required — it existed for that one deprecation^6.0 || ^7.0 narrowed to ^6.4 || ^7.0. CI only ever verified 6.4 and 7, and for a
release carrying a compatibility promise, claiming support nothing checks is worse than
narrowing it. No code changedDriveVoter decides access through is_granted(): DRIVE_VIEW, DRIVE_EDIT, DRIVE_SHARE,
DRIVE_DELETE, taking a DriveDocument or a file id. Registers itself when
symfony/security-core is installed. This is where "the bundle reports, it does not enforce"
becomes a decision that can be read, overridden and testedDriveDocumentResolver turns a route parameter into a DriveDocument controller argument,
applying the access check before the controller body runsforDrive($driveId) returns the same service pointed at another Shared Drive, carrying every
other setting over. One drive stays the configuration's businessUPGRADE.mdenforce_roles option, deliberately, and this is the release that settles it. Which role
an operation should require is arguable — Google wants organizer for some sharing changes and
writer for others depending on the drive's setup — so a matrix inside the service would be a
wrong answer nobody reviews. DriveVoter is that matrix somewhere replaceablelistFolder() on a folder that is in the trash still lists its contents, and 1.0 settles
that too. Google flags the folder alone, so checking would put another Drive call on the hottest
listing path to cover a case only a stale link reaches, where the viewer sees nothing new. Now
stated in the method's own docblock, not only in the READMEexpiresAt argument on grant(), grantToGroup() and
grantAsService(), plus setExpiry() to give an existing grant an expiry or lift the one it
has. DrivePermission gained expiresAt and expires(). Google's own restrictions — in the
future, no more than a year ahead — are checked before the call, because a Drive 400 names
neither the value nor the grantlock() and unlock() freeze a finished document against editing, with the reason Google
shows whoever tries. DriveDocument gained locked and lockReason, and a locked item's
capabilities report canEdit: false, so a UI built on capabilities needs no changes.
Reported by the new DocumentLockChangedEventstartPageToken() and changesSince() poll for work done directly in Google, returning a
DriveChanges of DriveChange entries and the token to resume from. Every change seen drops
that item's cached sharing immediately, which closes a gap open since 0.2.0: a share added by
hand in the Drive UI no longer waits for the cache to expiregoogle-drive-docs:check verifies the setup — credentials and who they authenticate as, the
Shared Drive, what the service user may do on it, and whether the root lists. Four independent
checks so a half-working setup says which half; read-only throughout. It calls out the common
case of the service user being a "Content manager", which may trash but not eraseshared_drive_id is held to Google's id alphabet before it is interpolated into a Drive query,
the way item ids already were. A deliberate debt from 0.3.1changes.watch) are deliberately not wrapped — they need a public HTTPS
endpoint with a valid certificate, which belongs to the application rather than to a librarylistRevisions(), revision(), keepRevision(), deleteRevision() and
exportRevision(), with a DriveRevision model carrying when a version was saved, who saved
it, its size, whether it is pinned and which formats it can be fetched as. The trash protects
against losing a file; this is what protects against a spreadsheet someone overwroteRevisionKeptEvent and RevisionDeletedEventexportRevision()
fetches the old content so the application can decide where it goes — import() it beside the
live document, or read the values back with SpreadsheetService. The README carries both
recipesexportRevision()
settles which of the two applies, and refuses a format the revision does not offer, before
spending a request on the document's namefalse — or 0, as a network mount or a stream wrapper can — sent the upload down
the multipart path, where the bounded read then handed the resumable fallback the size of what
it had read (5 MB) rather than the file's real size. Google measures every chunk against that
total and rejected the first one past it. The size now comes from the stream itself, and a
stream that can neither stat nor seek is reported as such instead of uploading nonsenseupload.max_bytes was skipped entirely on that same path: with no trustworthy stat the cap
was never applied. Both upload paths now check the bytes they actually handle, so the ceiling
holds whatever the filesystem claims0 stalled one byte from done: the
Google client tests its chunk argument with a loose comparison, and PHP reads the string "0"
as false. The loop no longer leaves a single byte for the final chunk — it swallows that byte
into the chunk before it, which is what makes that one the last. Shortening a chunk instead
would have sent an intermediate one that is not a multiple of 256 KB, breaking the rule
chunk_bytes itself is validated against; every chunk but the last is now asserted to be a
multiple of the granularitychunk_bytes is validated where it is configured: a value that is not a multiple of 256 KB
used to pass config validation and fail only when the service was first used, far from the
cause. It is also capped at the new MAX_CHUNK_BYTES (64 MB) — a chunk is read into memory
whole, so a gigabyte-sized one is a mistake rather than a tuning choiceroleOf() no longer stops at the permissions Google embeds in a file, which are not always
the whole list: it reads the dedicated lookup as well, so a writer grant held through a
group is not reported as the weaker reader found inlineSpreadsheetService and SheetFormatter tolerate a spreadsheet answer without sheets, as
listTabs() already did; a formatting pass against one now says the answer described no
usable tab instead of "no tab called ''"SheetRange refuses a range past the edges of the grid, naming it instead of leaving a Drive
400 to explain. The bounds are Google's own and deliberately loose: 18 278 columns (A to ZZZ)
and the ten million cells a spreadsheet can hold, which is also the most rows any range could
name. 16 384 columns and 1 048 576 rows are Excel's grid, and using those would have refused
ranges Google acceptsSheetFormatter::MAX_OPERATIONS (500): a pass travels as one batchUpdate, so the call that
overflows it raises an OverflowException naming itself. This ceiling is the bundle's own, not
a documented Google limit — the same reasoning as MAX_PAGES and MAX_BATCH_RANGES: a request
that grows without bound is a runaway rather than a styling passDriveDocumentService::MAX_CHUNK_BYTEScolumnWidth() and rowHeight() accept is described as this bundle's own
policy, which it is, rather than as Google's limitmax_bytes: 0 means no limit of your own, the chunk_bytes rules, the size of a
formatting pass, and that import() trusts the path it is givensetAppProperties(), appProperties(), findByAppProperty() and findByAppPropertyPage()
store the application's own metadata on an item and search by it, so "the spreadsheet
belonging to order 4711" is a question Drive can answer instead of a mapping table you keep.
Drive holds appProperties privately per OAuth client, so nothing else looking at the drive
sees them. Values are stored as strings, a null value removes a key, and every call is a mergeDocumentPropertiesChangedEventimport() no longer refuses a file over Google's 5 MB multipart ceiling.
Under it the file still goes up in one request; past it the bundle switches to Drive's
resumable protocol and sends the bytes in chunks, so nothing larger than one chunk is held in
memory and Drive's own 5 TB is the only ceiling leftupload.max_bytes for a policy limit of your own (0, the default, means none) and
upload.chunk_bytes for the resumable chunk size, which must be a multiple of 256 KBDriveDocumentService::MULTIPART_LIMIT and CHUNK_GRANULARITYUploadTooLargeException now means only
that the application's own upload.max_bytes was exceeded — code catching it to tell users
"too big for the integration" should either set that option or stop expecting the exceptionsearch() and the new property search share one query-escaping helper, so both treat a quote
or a backslash in the term the same wayDriveDocumentService::MAX_UPLOAD_BYTES, which no longer describes the largest upload. Use
MULTIPART_LIMIT for the point where the two upload paths part, or the upload.max_bytes
option for a limit of your ownfinally, and there is a test that a failure
mid-upload still restores itMediaFileUpload and is exercised for real rather than mocked in detailSpreadsheetService::format() starts a formatting pass and sends the whole thing as one
spreadsheets.batchUpdate: style() (bold, italic, font size, colour, background,
horizontal alignment, wrapping), numberFormat(), freeze(), autoResizeColumns(),
columnWidth(), merge() and unmerge(). Data filled into a template no longer has to
look rawSpreadsheetService::addTab() adds a tab and returns the numeric sheet id Google assignedSheetRange parses A1 notation into the zero-based, half-open indices Google's formatting
calls want, so callers keep writing 'Q3!A1:D10' and never meet a GridRange. Handles
quoted tab names, doubled apostrophes, open-ended ranges (D2:D, A:D10), whole columns
and rows, and normalises a reversed rangeborders(), bandedRows(),
conditionalFormat(), dataValidation(), basicFilter(), clearBasicFilter(),
protect(), rowHeight(), hideTab(), showTab() and tabColor(). Conditions use
Google's own vocabulary (NUMBER_LESS, ONE_OF_LIST, CUSTOM_FORMULA, …) and are passed
through rather than mirrored in a list the bundle would have to keep currentSpreadsheetService::renameTab() and deleteTab(). Renaming keeps the numeric sheet id, so
formulas and charts pointing at the tab keep working, and refuses a title another tab holds.
Deleting refuses the last remaining tab, and has no undo beyond the spreadsheet's own version
history — treat it the way you treat deleteForever()SheetFormattedEvent, SheetTabAddedEvent, SheetTabRenamedEvent and SheetTabDeletedEventstyle() writes only the attributes it is given. Google's repeatCell clears everything its
field mask covers, so each call builds a mask naming exactly what was asked for — passing
bold: true leaves the background, alignment and number format on those cells alonefreeze() and autoResizeColumns() take a tab name rather than a range, because a bare Q3
in A1 notation is the cell Q3 and a range would be ambiguous exactly where it mattersunprotect(): removing a protection means finding the id Google assigned it, which is
work for whoever is looking at the spreadsheet rather than for something generating oneroleOf() reports the role the current viewer effectively holds on an item — the strongest of
every grant that applies, whether it names them directly or reaches them through a group, on
the item itself or on a folder above it. Roles the bundle cannot grant (owner, organizer,
fileOrganizer) are reported as they are; a viewer whose seesEverything() is true gets
null, since nothing is looked up for them. Until now the access check was about reach only,
and an application had no way to tell a reader from an editor short of reading
listPermissions() itselfrole alongside the address, so the answer costs no
extra requestroleOf() reports, it does not enforce: canAccess() still passes anyone holding any grant,
and the bundle still performs the operation as the service user. Which role a given operation
should require stays the application's decision — Google's own answer varies with how the
Shared Drive is configured, and a wrong one baked into a library locks people outSpreadsheetService reads and writes the cells of a Google Sheet, so a template can be
filled with the application's own data: listTabs(), read(), readMany(), write(),
writeMany(), append(), clear() and a range() helper that quotes tab names correctly.
It runs on the same authenticated client as the Drive side, so the retry policy applies, and
every call — reads included — asks DriveDocumentService for access, so a spreadsheet's
contents are exactly as reachable as the spreadsheet itself. Registered as
google_drive_docs.spreadsheets. Until now the bundle asked for the auth/spreadsheets
scope and told you to enable the Sheets API without ever calling itSheetValuesUpdatedEvent, SheetRowsAppendedEvent and SheetRangeClearedEventSpreadsheetService::MAX_BATCH_RANGES (100): readMany() and writeMany() refuse a longer
list of ranges with an InvalidArgumentException before calling Google. The cap is the
bundle's own, not a documented Google limit — batchGet sends ranges in the query string,
where a long list becomes a URL that may be rejected unhelpfullySpreadsheetService::range() quotes tab names that read like a cell reference (Q3, A1,
ZZ999): unquoted, Google treats them as that cell of the first tab, so append() to a tab
called Q3 would have written to the wrong placeread() / readMany() keep the types Google sends under RENDER_RAW — int, float,
bool — instead of casting everything to string, which turned an unticked checkbox into
the same '' as an empty cell and lost float precisionwriteMany() reports Google's updatedRows per block in SheetValuesUpdatedEvent, as
write() already did, rather than the number of rows handed inSheetRowsAppendedEvent::$range carries where the rows actually landed as reported by
Google ('Q3'!A10:B12), falling back to the requested rangelistTabs() tolerates a spreadsheet answer without sheetsreader passes the access check and the
bundle then acts as the service user; how to tell readers from editors. Ranges are developer
input, whole-tab reads and the 100-range batch ceiling. append() listed among the
non-idempotent calls the retry policy may repeatgrant(), grantToGroup(), revoke() and listPermissions() now check the viewer's
access to the item first and throw AccessDeniedException otherwise, like every other
operation on a single item already did. Before, a viewer who knew a file id could share
any document with themselves, revoke other people's access or read who a document is
shared with. Applications whose ViewerContextInterface::seesEverything() returns true
(the default AllowAllViewerContext included) are unaffectedpermission_cache.ttl: 0 now means "do not use the shared pool" (per-request caching
only). Before, entries were saved without a lifetime and lived in the pool until it was
cleared — the opposite of what a zero lifetime suggestsimport() verifies the size of the bytes it actually read, not only the size reported by
filesize(): a failed stat or a file growing while being read no longer slips an
oversized upload past UploadTooLargeExceptioncanAccess() no longer turns a Google outage into a denial: only a 403/404 from Google
means "not shared", any other error surfaces. Likewise the sharing lookup used by listings
keeps swallowing transient API exceptions for the current request only, but lets programming
errors (TypeError and friends) propagate instead of silently hiding every documentcanAccess() reports a missing shared_drive_id with NotConfiguredException right away
instead of walking up to 25 parents in vainmove() refuses with a RuntimeException when Google does not report the item's current
parent, rather than sending an empty removeParentslistFolder() / listFolderPage() are validated against Google's id
alphabet before they are interpolated into the Drive querydeleteForever() no longer mistakes a 403 carrying rateLimitExceeded /
userRateLimitExceeded (an exhausted quota after the retries) for a missing Manager roleDocumentDeletedEvent docblock described the trash; it is dispatched on permanent deletionreason no longer breaks on an error that carries none.
Google\Service\Exception::getErrors() returns null, not an empty array, whenever the
response body has no error.errors — a proxy 502, an empty 429 — and iterating that raised a
warning that a strict error handler (Symfony's, in dev) turns into a throw, masking the real
Google failure in copy(), revoke() and deleteForever()import() reads at most one byte past the upload limit instead of the whole file, so a
failed filesize() can no longer pull an arbitrarily large file into memory before the size
check rejects itcopy() and revoke() recognise Google's fileNotCopyable / cannotCopyFile and
cannotDeletePermission / cannotModifyInheritedPermission by the machine-readable reason
instead of the wording of the message, which Google may change at any timelistFolder(), search(), listTrash(), listPermissions() and the
sharing lookup) stops with a RuntimeException after 1000 pages instead of looping — and
eventually running out of memory — on a nextPageToken that never endsgrantAsService() shares on behalf of the application, skipping the viewer's access check
that grant() performs. Without it the access check above has no answer for the
"creator gets access" pattern: a document the service user just created in the drive root is
shared with nobody, so the grant meant to give its creator access is itself a grant on an
item that creator cannot yet reach. Role and grantee type are still validated, the event is
still dispatched and the cached sharing is still invalidated. A separate method rather than a
flag on grant(), so every bypass is one grep away (#1)UnexpectedDriveStateException for the two cases where Google answers something that cannot
be true — an item on a Shared Drive with no parent, and a listing whose nextPageToken never
ends. Both previously raised a bare RuntimeException, which slipped through the typed
catches consumers map onto status codes and surfaced as an untyped 500 (#2)ViewerContextInterface is still the default AllowAllViewerContext, so running without
visibility filtering is always a choice, never an oversightcomposer.json allowsDriveCapabilities (canEdit, canRename,
canDelete, canTrash, canUntrash, canShare, canCopy, canDownload,
canAddChildren, canMove) reporting what Google will allow, so a UI can render only the
actions that work. Note the flags describe the service user the bundle authenticates as,
not the person browsing — per-viewer visibility stays with ViewerContextInterfaceDriveDocument: createdTime, size (null for Google documents),
iconLink, thumbnailLink and lastModifiedBy. All are optional constructor arguments
defaulting to null, and all appear in toArray()rateLimitExceeded, userRateLimitExceeded, backendError
and internalError, and on connection-level curl failures. Client mistakes (400/401/403/404)
are deliberately never retried. Tunable through the new retry.attempts,
retry.initial_delay and retry.max_delay options; attempts: 0 restores the old
fail-immediately behaviourlistFolderPage(), searchPage() and listTrashPage() return a DrivePage
(iterable, countable, toArray()-able) holding one Google page plus its nextPageToken,
so a listing costs a single files.list call instead of one per hundred itemsimport() uploads a local file and has Google convert it into the matching editable
document — spreadsheets into Google Sheets, text documents into Google Docs, decks into
Slides — or stores it unchanged with convert: false. Reports the new
DocumentImportedEvent, and refuses anything above Google's 5 MB multipart ceiling with
UploadTooLargeException rather than failing obscurelyexport() renders a document into XLSX, CSV, PDF, DOCX and the other formats
listed on the new DriveExport model, which carries the download filename, the real
mimeType, a contentDisposition() header value and the response body as a PSR-7 stream
so downloads are never buffered in PHP memorycopy() duplicates a document (beside the original by default, or into a folder
under a new name) and createFromTemplate() starts a document from a template documentDocumentCopiedEvent, carrying the new document, the sourceId it came from and the
target parentIdNotCopyableException, raised when Google refuses a copy — in practice a folder, which the
Drive API cannot copy at alltrash(), restore(), listTrash() and deleteForever(). Deleting is no
longer a one-way door — items stay recoverable until Google purges the trash (30 days on a
Shared Drive)DocumentTrashedEvent and DocumentRestoredEvent, both carrying the updated DriveDocumentInsufficientDriveRoleException, raised when Google refuses a permanent delete because the
service user is a "Content manager" rather than a "Manager" of the Shared DriveDriveDocument::$trashed, so get() on a trashed item no longer reports it as live.
It is the seventh constructor argument and defaults to false; toArray() gained a
matching trashed keylistFolder(), search() and listTrash() keep returning every item and their signatures
are unchanged, but now fetch pages of 1000 instead of 100 — ten times fewer round trips on
a large drive. They are built on the new paged methods, so behaviour is otherwise identicalDocumentDeletedEvent now documents what it always did: the item was erased for good, not
trashed. Its docblock and the README claimed the opposite. Listeners keep their meaning —
the event is dispatched from deleteForever()InsufficientDriveRoleException instead of
surfacing as Google\Service\Exception. Code catching the latter around delete() must
catch the new exception as welldelete()
previously left behind until the TTL expiredDriveDocumentService::delete(). Its behaviour is unchanged — it erases the item for good —
but the name no longer says so. Replace it with deleteForever() to keep that behaviour, or
with trash() to move the item to the trash instead. It will be removed in 1.0permission_cache.pool / permission_cache.ttl),
invalidated immediately when access is granted or revoked through the bundlegrantToGroup() and a $type argument on grant()ViewerContextInterface::getViewerGroups()DocumentCreatedEvent, FolderCreatedEvent,
DocumentRenamedEvent, DocumentMovedEvent, DocumentDeletedEvent, AccessGrantedEvent,
AccessRevokedEvent) so auditing and notifications can live outside the bundleViewerContextInterface gained getViewerGroups(): array. Implementations must add it;
return an empty array when sharing with groups is not used.reader / commenter / writer)ViewerContextInterface extension point so the host application decides who sees whatHow can I help you explore Laravel packages today?