smronju / 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.
Package info
github.com/smronju/nativephp-document-picker
Type:nativephp-plugin
pkg:composer/smronju/nativephp-document-picker
Requires
- php: ^8.2
- nativephp/mobile: ^3.0|^4.0
Requires (Dev)
- pestphp/pest: ^3.0|^4.0|^5.0
README
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-picker
php artisan vendor:publish --tag=nativephp-plugins-provider
php artisan native:plugin:register smronju/nativephp-document-picker
php 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(); // anything DocumentPicker::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