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.

Maintainers

Package info

github.com/smronju/nativephp-document-picker

Type:nativephp-plugin

pkg:composer/smronju/nativephp-document-picker

Transparency log

Statistics

Installs: 18

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.1.2 2026-08-18 07:29 UTC

This package is auto-updated.

Last update: 2026-08-18 07:30:06 UTC


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:), Android ACTION_OPEN_DOCUMENT. Hands PHP an absolute path to a copy of the chosen file.
  • Save — iOS UIDocumentPickerViewController(forExporting:), Android ACTION_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.
  • $name on DocumentPicked is 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's forExporting shows 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 $suggestedName through Intent.EXTRA_TITLE directly; 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 on pick() falls back to every file.

License

MIT