1

I'm trying to create a controller for a user to download a membership card as a pass in Apple Wallet. I'm looking to use the PKPass Library.

I've got things set up and working using their example like this:

$pass = new PKPass(Yii::getAlias('path/to/cert.p12'), 'myPassword');
$data = [
  'description' => 'Membership Card',
  // etc as per example: https://github.com/includable/php-pkpass/blob/master/examples/example.php
];

$pass->setData($data);

$pass->create(true);

but get a 'Headers already sent' error when trying to download the pass. I feel like this is because the PKPass class is outputting to the browser before my module has finished processing the request.

So changing the last line to $createdPass = $pass->create(false); and then handling the output is probably what I want to do, but I'm stuck on how to get that $zip response back into my controller as a file to download. Here is the create method:

public function create($output = false)
{
    // Prepare payload
    $manifest = $this->createManifest();
    $signature = $this->createSignature($manifest);
// Build ZIP file
$zip = $this->createZip($manifest, $signature);

// Return pass
if (!$output) {
    return $zip;
}

// Output pass
header('Content-Description: File Transfer');
header('Content-Type: application/vnd.apple.pkpass');
header('Content-Disposition: attachment; filename="' . $this->getName() . '"');
header('Content-Transfer-Encoding: binary');
header('Connection: Keep-Alive');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s T'));
header('Pragma: public');
echo $zip;

return '';

}

This might be more of a basic PHP question, but does anyone know how I could do that?

supazu
  • 576
  • 4
  • 12
  • Not sure what you're missing here – if $output is false, you're already returning the $zip, so the controller can output it. What is not working? As a sidenote, setting headers and outputting content should not happen in a utility method, it should happen in the controller. – MoritzLost Jun 30 '23 at 07:39
  • 1
    Thanks for the response. Whenever I see you respond to one of my questions I always get excited because I know it's going to be helpful! (I mean that sincerely) And indeed this is a good thing that I can't answer very well. I was just getting a "Safari can't download this file" error with the previous code. August's reply has helped push me in the right direction. Thanks again for the reply! – supazu Jul 01 '23 at 00:56

1 Answers1

3

Assuming the Pass bundle is created in a service, your controller should only be responsible for calling that method and preparing a response. As @MoritzLost said, setting headers from a service method (or allowing another library to control the HTTP response) is generally inadvisable—in part because those tools exist in Craft/Yii, and in part because you should be able to create a Pass and ZIP file without caring about when or where it will be used (say, precompiling a bundle in a background job or building one to be attached to an email).

To that end, your service might look like this:

namespace mynamespace\myplugin\services;

use craft\base\Component; use PKPass\PKPass;

class Passes extends Component { public function createPass(): string { $pass = new PKPass('...', '...'); $pass->setData([ // ... ]);

    // Return the ZIP’s content, directly:
    return $pass->create(false);
}

}

(This method could be parameterized so that it can look up an entry or whatever other info you need to create a unique pass!)

Your controller would look like this:

namespace mynamespace\myplugin\controllers;

use craft\web\Controller; use mynamespace\myplugin\Plugin;

class PassController extends Controller { public function actionGetPass(int $id) { // Call service to create pass: $pass = Plugin::getInstance()->getPasses()->createPass($someArg);

    // Send the file’s contents:
    return $this->response->sendContentAsFile($pass, 'Pass File Name.pkpass', [
        'mimeType' => 'application/vnd.apple.pkpass',
    ]);
}

}

We're using the built-in sendContentAsFile() response method via the controller’s response property, and declaring the appropriate MIME type so that the client has a hint about what to do with the attachment.

Read more about sending files in the Yii documentation on handling requests!

August Miller
  • 3,380
  • 9
  • 25
  • Thanks August, this is great (and a good reminder of how much I still have to learn.) I think I'm close, but unsure on the $this->sendContentAsFile part. With that in place, I get a 'calling unknown method' error. Excuse my lack of knowledge, but how could I make that work? – supazu Jul 01 '23 at 00:45
  • Looks like I needed $this->response->sendContentAsFile() - got it working now! Thank you!! – supazu Jul 02 '23 at 06:05
  • 1
    Ah, shoot, you're totally right. Updating the answer accordingly! – August Miller Jul 03 '23 at 15:06