Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ref Lib Ris Laravel Package

funstaff/ref-lib-ris

A PHP package for working with RIS bibliographic reference files. Parse, build, and export citation records such as journal articles, books, and conference papers, with a simple API that fits neatly into Laravel applications. Handy when importing or generating reference data.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pin. There is no service provider, no facade, no config file — nothing to discover with php artisan. Installing it is a single Composer line, and the only setup that matters is pinning:

composer require funstaff/ref-lib-ris:^1.0   # then edit composer.json to an exact version, e.g. "1.0.3"

Before you commit, check vendor/funstaff/ref-lib-ris/composer.json for the PHP constraint and any ext-* requirements. This package has effectively zero adoption, so treat its declared constraints as unverified claims rather than a compatibility guarantee.

Where to look first. The library is small enough to read in one sitting — do that before writing any application code:

  1. vendor/funstaff/ref-lib-ris/src/ — find the one class that actually emits text. Everything else is probably tag constants and small DTOs.
  2. Grep for mb_, strlen, \n, and ER to answer the three questions that decide whether this is usable: does it handle multibyte strings, does it fold long lines, and does it terminate records correctly?
  3. Grep for parse/read methods. The package description advertises parsing and validation, but independent assessment says it is writer-only. Read the source and settle this yourself — do not design an import feature on the description alone.

First use case: one record, one file, imported into Zotero. Do this before wiring anything into your app. A throwaway Artisan command is enough:

// app/Console/Commands/RisSpikeCommand.php
public function handle(): int
{
    $ris = app(\App\Services\Citation\RisExporter::class)->exportOne(
        title: 'On the Origin of Species',
        authors: ['Darwin, Charles'],
        journal: 'Nature',
        year: 1859,
        doi: '10.1000/example',
    );

    $this->line($ris);                       // eyeball it
    file_put_contents(storage_path('app/spike.ris'), $ris);
    return self::SUCCESS;
}

Then import spike.ris into Zotero and EndNote. That import — not a passing unit test — is the acceptance criterion for the whole integration. If it fails on accents, an abstract, or two authors, stop and decide between fixing the output yourself and writing the ~150-line formatter in-house.

First real endpoint. Once the spike passes, wrap it behind an interface you own and expose a download:

Route::get('/articles/{article}/citation.ris', ExportCitationController::class)
    ->name('articles.citation');

The handler should be three lines: resolve the exporter from the container, build the record, return a response with Content-Type: text/plain; charset=UTF-8 and Content-Disposition: attachment; filename="article-{$article->id}.ris".

Implementation Patterns

Interface boundary, always. This package must never appear in a controller, model, Blade template, or Livewire component. Define your own contract and make the package one implementation of it:

namespace App\Services\Citation;

interface CitationExporter
{
    /** @param  array<int, Reference>  $references */
    public function export(array $references): string;
}
namespace App\Services\Citation;

use Funstaff\RefLibRis\...; // the one class you found in src/

class RisCitationExporter implements CitationExporter
{
    public function export(array $references): string
    {
        $out = '';
        foreach ($references as $ref) {
            $out .= $this->exportOne($ref) . "\r\n";   // blank line between records
        }
        return $out;
    }

    // mapping lives here and nowhere else
}

Bind it in AppServiceProvider::register() with $this->app->bind(CitationExporter::class, RisCitationExporter::class);. A singleton is fine — the writer is stateless — but only if you have verified it holds no internal buffer between calls.

Own the field mapping explicitly. Serialization is the package's job; tag selection is yours. Write the mapping down as a table in the adapter's docblock, because this is where every bug will live:

Domain RIS Notes
title TI Also emit T1 for some consumers
authors[] AU One tag per author, Last, First
journal JO / T2 JF full, JO abbreviated
year PY / Y1 Year only, no month
volume / issue VL / IS Strings, not ints
pages "123-130" SP 123, EP 130 Split; do not put the range in one tag
doi DO Also UR if you must, but DO is what Zotero reads
url UR
abstract AB Long — check line folding
type TY JOUR, BOOK, CONF, THES, RPRT…

Store that mapping in a plain array-driven method (Reference::toRisArray()) so it can be unit-tested without instantiating the package at all.

Per-record download. The common case. Keep it synchronous, no queue:

return response($exporter->export([$article->toReference()]), 200, [
    'Content-Type'        => 'text/plain; charset=UTF-8',
    'Content-Disposition' => 'attachment; filename="citation.ris"',
]);

Use response()->streamDownload() with a generator if you want a consistent download UX, but for one record the plain response is fine and easier to test.

Bulk export → queue → Storage. Never build a 10,000-record string inside a web request. Push a job that writes to disk and notifies the user with a signed URL:

class ExportCitationsJob implements ShouldQueue
{
    public function handle(CitationExporter $exporter): void
    {
        $path = "exports/{$this->export->id}.ris";
        $handle = Storage::disk('local')->writeStream($path, null);

        Article::query()->whereIn('id', $this->articleIds)->lazyById(500)
            ->each(function ($article) use ($exporter, $handle) {
                fwrite($handle, $exporter->export([$article->toReference()]) . "\r\n");
            });

        fclose($handle);
    }
}

Chunked generation via lazyById + writeStream bounds memory regardless of what the library does internally. Confirm the exporter does not accumulate state between calls — if it does, instantiate it per chunk.

Attach to mail. Submission confirmations often want the .ris inline. Use attachData rather than writing a temp file:

$mail->attachData($exporter->export([$article->toReference()]), 'citation.ris', [
    'mime' => 'application/x-research-info-systems',
]);

application/x-research-info-systems is the correct MIME type and improves how mail clients and OS file associations handle the attachment.

Golden-file tests. This is the highest-value thing you will write. Commit reference .ris files produced by Zotero from known input, and assert your exporter matches byte-for-byte (or tag-for-tag after normalizing line endings):

it('matches the Zotero golden file', function () {
    $actual = app(CitationExporter::class)->export([$this->darwinReference()]);
    expect($actual)->toBe(file_get_contents(base_path('tests/fixtures/darwin.ris')));
});

Add a dataset with the nasty cases: diacritics (Müller, Ñuño), a 2,000-character abstract, a corporate author, a book with no journal, and a record with an empty DOI. Those five cover most real-world breakage.

Feature flag it. If the export is user-facing, ship it behind a flag. It makes the rollback story trivial if a reference manager rejects the output in the field, and it lets you compare against an in-house writer without touching product code.

Gotchas and Tips

The description and the assessment disagree about parsing. Marketing text says "parse, validate, and generate"; the product assessment is emphatic that this is writer-only. Grep the source before you promise anyone an import feature. If it really is writer-only, an import requirement is a different ticket with a different solution.

Line folding is the #1 cause of rejected .ris files. RIS physical lines should stay under roughly 255 characters, with continuation lines starting with a space. A 1,500-character abstract emitted as one giant line will import into some tools and silently truncate in others. Test with a real long abstract and inspect with cat -A or open it in a plain-text editor with wrapping off.

Terminator and whitespace are load-bearing. TY must be the first tag, ER - (note the two spaces after the tag and the space-dash-space separator) must be the last, and each record should be followed by a blank line. Files that look right in an editor but lack the trailing blank line will merge records in some consumers. Do not "tidy" the output with a formatter.

Encoding, end to end. Set the database connection and column collations to utf8mb4, and assert the exporter does not mangle multibyte input. Do not add a UTF-8 BOM — it looks like a stray tag to some parsers. If you are pulling data from a legacy source, normalize to UTF-8 before it reaches the exporter, not after. A round-trip test with Müller, Škoda, and one CJK title catches almost everything.

Line endings. Many reference managers tolerate \n, some Windows tools prefer \r\n. Emit \r\n and terminate records with \r\n\r\n, then normalize in tests so the fixtures are readable in git. Document the choice — a future contributor will "fix" it otherwise.

Author name forms are not interchangeable. AU with Last, First for personal names; institutional authors typically need a trailing comma (World Health Organization,) to stop parsers splitting on the space. Check whether the library accepts pre-formatted strings or tries to parse "First Last" itself — if it parses, feed it already-formatted Last, First and verify the output does not double-comma.

Pages need to be split. SP/EP, not one pages field. Splitting "123-130" is trivial until you hit "e12345", "Suppl 2", or an en-dash pasted from Word. Normalize – to - and handle single-page records (SP only, no EP).

DOI belongs in DO, not just UR. Zotero will not populate the DOI field from UR. Emit both if you like, but DO is the one that matters.

Memory on bulk exports. The package builds strings in memory, which is fine per record and dangerous per result set. Use lazyById or cursor(), never ::all(). Test with 10,000 synthetic records before shipping "download all results" and watch peak memory in the job.

Silent data loss is the sneaky failure. If a domain field has no mapping, nothing throws — the citation is just quietly poorer. Assert coverage: keep a list of exported domain attributes and fail a test when a new one appears without a mapping entry. Log a warning at export time for unmapped attributes.

There is nothing to configure, so don't try. No config/ref-lib-ris.php will appear, and php artisan vendor:publish will list nothing. Resist wrapping it in a facade — a facade over a five-method adapter hides the swap boundary you deliberately created.

Assume you will fork it. One star, zero dependents, no release cadence. If the source is a handful of files, copy it into app/Support/Ris/ with a // FORKED FROM funstaff/ref-lib-ris @ <sha> header and delete the dependency. MIT makes this free, the golden-file tests make it safe, and you remove the supply-chain question entirely. Budget half a day; it is usually the right call for a library this small.

Debugging workflow. Write the raw output to a file, then check three things in order: cat -A for invisible whitespace and line endings, an import into Zotero for structural acceptance, and a diff against the golden fixture for regression. Do not debug by staring at a JSON response — escape sequences will lie to you.

Never dd() an RIS payload. It is plain text with meaningful leading and trailing whitespace; a browser or terminal will hide exactly the characters that are broken. Use Storage::put or file_put_contents and open the file.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin