NativePHP Document Picker#
Presents the system document picker in a NativePHP Mobile app — to open a foreign file, and to save one back out to a location the user chooses.
- Open — iOS
UIDocumentPickerViewController(forOpeningContentTypes:asCopy:), AndroidACTION_OPEN_DOCUMENT. Hands PHP an absolute path to a copy of the chosen file. - Save — iOS
UIDocumentPickerViewController(forExporting:), AndroidACTION_CREATE_DOCUMENT. Copies a file the app already wrote to wherever the user picks — Downloads, an SD card, a cloud provider's own app.
No storage permission on either platform, either direction: both pickers are system UI, and the user choosing a file or a destination is the entire grant.
Why open hands back a copy#
Neither platform hands back something PHP can read. Android returns a content:// URI
belonging to another app's provider, which fopen() cannot open at all; iOS returns a
URL inside the provider's sandbox that needs coordinated access and a security-scoped
resource dance the embedded runtime can't perform. So pick() copies first and reports
the copy's path.
Why save exists at all#
The system share sheet (Share::file() from nativephp/mobile-share) can only offer
whatever apps happen to register a share receiver — on a device with no such app
installed, there is nothing to save to. save() is backed by the OS's own storage
layer instead (Android's Storage Access Framework; iOS's document provider system), so
it always has somewhere to put the file, independent of what's installed.
Installation#
composer require smronju/nativephp-document-pickerphp artisan vendor:publish --tag=nativephp-plugins-providerphp artisan native:plugin:register smronju/nativephp-document-pickerphp artisan native:plugin:list # DocumentPicker.Pick and DocumentPicker.Save should appear
Then rebuild — native code only compiles in at build time:
php artisan native:run android # or ios
Usage#
Opening a file#
use Smronju\NativephpDocumentPicker\Facades\DocumentPicker; DocumentPicker::pick(); // anythingDocumentPicker::pick(['text/csv', 'text/plain']); // narrowed
pick() only presents the picker. The result arrives via an event, because choosing a
file can take arbitrarily long — a file in a cloud provider is downloaded first.
use Native\Mobile\Attributes\On;use Smronju\NativephpDocumentPicker\Events\DocumentPicked; #[On(DocumentPicked::class)]public function handleDocumentPicked(?string $path = null, ?string $name = null): void{ if ($path === null) { return; // cancelled, or the copy failed } $contents = file_get_contents($path);}
Saving a file#
use Smronju\NativephpDocumentPicker\Facades\DocumentPicker; DocumentPicker::save($absolutePathToCsv, 'report.csv', 'text/csv');
$sourcePath must already exist — this presents a picker for a file the app already
wrote; it does not create one. Like pick(), this only presents the picker; the
outcome arrives via an event.
use Native\Mobile\Attributes\On;use Smronju\NativephpDocumentPicker\Events\DocumentSaved; #[On(DocumentSaved::class)]public function handleDocumentSaved(?string $name = null): void{ if ($name === null) { return; // cancelled, or the copy failed } // $name is the destination's display name — there is nothing further to read. // The destination can be a cloud-backed document with no local filesystem path // at all, which is why none is reported.}
Every listener parameter needs its own = null default even though the types are
nullable — the #[On] dispatcher supplies an argument only for payload keys that are
present, and this plugin omits every key on cancel. (An explicit JSON null would
arrive as '', not null.)
Notes#
pick()'s copy is temporary. Read it, or move it somewhere durable, inside the listener. Android keeps only the most recently picked file; iOS may clear its temporary directory at any time.$nameonDocumentPickedis untrusted — it comes from another app's provider. The Android side strips path separators before using it as a filename; do the same before putting it into a path of your own.save()'s Save panel name. iOS'sforExportingshows the given file's own name in the panel, with no separate "suggested name" parameter — so the plugin stages a renamed copy first. Android passes$suggestedNamethroughIntent.EXTRA_TITLEdirectly; the destination provider may still adjust it.- MIME types are the shared vocabulary, both directions. iOS maps each to a
UTType; anything it doesn't recognise is dropped rather than guessed at, and an empty list onpick()falls back to every file.
License#
MIT