basement-lms
basement-lms copied to clipboard
Suporte á arquivos SCORM.
Se isto se tornará de fato um LMS, é importante o mesmo ter suporte á norma SCORM, utilizada na maioria dos LMS's do Mercado.
A norma SCORM define comunicações entre o conteúdo do lado do cliente e um host chamado de ambiente de execução. O SCORM permite que você empacote seu conteúdo e o compartilhe com outros sistemas de forma mais eficiente
Leia mais sobre em: https://scorm.com/scorm-explained/technical-scorm/scorm-12-overview-for-developers/ https://pt.wikipedia.org/wiki/SCORM https://www.easy-lms.com/pt/centro-de-conhecimento/centro-conhecimento/o-que-e-scorm/item10195 https://scorm.com/scorm-explained/technical-scorm/scorm-2004-overview-for-developers/
Outras implementações: https://github.com/moodle/moodle/blob/master/mod/scorm/ https://github.com/jcputney/scorm-again https://github.com/dhodges47/SCORM-LearningManagementSystem https://github.com/mesuyog/scorm
Legal! Vou dar uma lida nos conteúdos, sabe se tem alguma biblioteca pronta para resolver este problema?
Infelizmente desconheço. Recomendei, pois conheço profissionais da Área de Educação, que trabalham com esse formato incorporando os mesmos a seus respectivos LMS, onde trabalham.
Alguém na live deixou esse tutorial
https://github.com/EAD-Facil/SCORM-tutorial
Como implementar SCORM¹
¹ (de forma simples)
Também deixo aqui a implementação de um leitor de SCORM do padrão 1.2 que funciona com qualquer arquivo SCORM criado com a ferramenta iSpring de minha autoria.
A primeira classe que criei foi a Scorm que é inicializada no construtor com o Path de um arquivo Scorm e faz a leitura do mesmo (dentro do arquivo .scorm que é um .zip na verdade).
Scorm.php
<?php
namespace Helpers;
use Exception;
use Illuminate\Support\Facades\Log;
use ZipArchive;
use SimpleXMLElement;
class Scorm {
// private
private $zip_path;
private $content_path;
private $manifest;
private $index;
private $name;
private $location;
// consts
private const ROOT_FOLDER = 'public/storage/';
private const ZIP_FOLDER = self::ROOT_FOLDER . 'z/';
private const CONTENT_FOLDER = self::ROOT_FOLDER . 'c/';
public function __construct(String $fileName) {
$this->path = ( is_readable(self::ZIP_FOLDER . $fileName)) ? $fileName : '';
$this->read_manifest();
$this->name = $fileName;
$this->location = public_path() . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR;
$this->zip_path = $this->location . $fileName;
$this->folderName = explode('.', explode(DIRECTORY_SEPARATOR, $this->name)[2])[0];
$this->content_path = $this->location . 'scorm' . DIRECTORY_SEPARATOR .'c' . DIRECTORY_SEPARATOR . $this->folderName;
}
public function unzip() {
try {
if (is_readable($this->zip_path)) {
$unzip = new ZipArchive();
$unzip->open($this->zip_path);
$unzip->extractTo($this->content_path);
$unzip->close();
return $this;
} else {
throw new Exception("File not " . $this->zip_path ."found", 1);
}
} catch (\Throwable $th) {
Log::error($th->getMessage());
return false;
}
}
public static function getFolderPath() {
return self::ZIP_FOLDER;
}
public function getIndexPath() {
$path = 'scorm' . DIRECTORY_SEPARATOR .'c' . DIRECTORY_SEPARATOR . $this->folderName . '/res/index.html';
$clean_path = strtr($path, '/\\', '/');
$clean_path = preg_replace('/\\\\+/','\\', $clean_path);
return $clean_path;
}
public function getZipPath() {
return $this->zip_path;
}
public function read_manifest()
{
if (is_readable($this->path)) {
$zip_scorm_file = new ZipArchive();
$contents = '';
$file = null;
if ($zip_scorm_file->open($this->path)) {
$file = $zip_scorm_file->getStream('imsmanifest.xml');
} else {
$file = "";
}
if ($file !== "") {
while (!feof($file)) {
$contents .= fread($file, 2);
}
}
$manifest_contents_as_object = new SimpleXMLElement($contents);
$this->manifest = $manifest_contents_as_object;
$zip_scorm_file->close();
return $this;
}
}
public function getManifest() {
return $this->manifest;
}
public function getScormVersion(): string {
return $this->manifest->metadata->schemaversion;
}
public function getIndex(): string {
return $this->content_path . 'res/index.html';
}
public function readIndex() {
$zip_scorm_file = new ZipArchive();
$html = '';
$zip_scorm_file->open($this->path);
// $html = $zip_scorm_file->statIndex( 7 );
$serialization = $zip_scorm_file->getStream( $this->getIndex() );
$html = stream_get_contents($serialization);
$zip_scorm_file->close();
return $html;
}
}
Quando faço a função de salvar o conteúdo lido usando a classe para passar as informações relevantes para o BD em um Service do Laravel. ScormService.php (trecho)
public function store($data)
{
try {
$modulo = Modulos::find($data['modulo_id']);
// ConteudosModulos = Conteudos
$store = new ConteudosModulos();
// pega modulo && trilha
$store->modulo_id = $modulo->id; // id unsigned auto increment
$store->trilha_id = $modulo->trilha->id; // id unsigned
// guarda tipo, titulo, capa, descricao
$store->tipo_id = 1;
$store->titulo = $data->titulo;
$store->capa = $data->capa;
$store->descricao = $data->descricao;
// $store->disponivel
$dia= (!empty($data['disponivel_data'])?null:date('Y-m-d', strtotime(str_replace('/', '-', $data['disponivel_data']))));
$hora = (!empty($data['disponivel_hora'])?null: $data['disponivel_hora']);
if(empty($dia) || empty($hora)){
$disponivel = null;
}else{
$disponivel = $dia.' '. $hora;
}
$store->disponivel = (empty($disponivel)? null : $disponivel);
// end $store->disponivel
// salva o conteúdo
if ($data->has('uploaded_file')) {
$scormHandler = new Scorm($data->uploaded_file);
$store->arquivo = $scormHandler->unzip()->getIndexPath();
$store->is_scorm = true;
} else {
throw new Exception("Erro na leitura do arquivo", 1);
}
$store->ordem = $modulo->conteudos->count()+1;
$store->save();
return $store;
} catch (\Exception $exception) {
logger()->error($exception);
return false;
}
}
E na hora de salvar no Controller uso regex para fazer o clean dos dados e chamo o service para cuidar disso. ScormContentController.php método store* (trecho)
if ($request->hasFile('scorm')) {
$file = $request->file('scorm');
$path = Storage::disk('public')->putFileAs('scorm' . DIRECTORY_SEPARATOR . 'z', new File($file->getRealPath()), (DIRECTORY_SEPARATOR . time() . $file->getClientOriginalName() ) );
$path = strtr($path, '/\\', DIRECTORY_SEPARATOR);
$path = preg_replace('/\\\\+/','\\', $path);
$path = preg_replace('#/+#','/',$path);
$request->merge(['uploaded_file' => $path]);
if ($this->service->store($request)) {
return redirect()->back()->with(['type' => 'success', 'message' => 'Item cadastrado com sucesso']);
}
} else {
return redirect()->back()->with(['type' => 'error', 'message' => 'Erro ao cadastrar o item']);
}
No front-end eu carrego um iframe que chama o path do arquivo inicial html do Scorm que está salvo no servidor e que foi salvo no BD apontando para seu local na pasta storage do Laravel.
@if($conteudo->is_scorm)
<iframe name="myframe" src="{{ URL::to('/')}}/storage/{{$conteudo->arquivo}}" height="450" width="100%" style="border: none !important;">Your browser does not support frames.</iframe>
@endif
E isso carrega o SCORM 1.2 do iSpring dentro do iframe e tudo funciona como devia, utilizando os cookies do Navegador para salvar as informações de progresso do curso, notas e demais.
Depois disso é facil fazer a implementação de ler essas infos dos cookies e passar por API ou Ajax para o backend para fazer a integração do SCORM com o sistema.
É uma idéia de como pode funcionar, infelizmente essa técnica apenas funciona com o iSpring, mas a vantagem é que é fácil de implementar para ler arquivos porém com o Rusticii Driver que explico no link do @NicolasPereira é possivel exportar de forma facilitada, a técnica que usei com o iSpring pode facilmente ser adaptada para funcionar com os arquivos gerados pelo Rustici Driver e assim tendo uma integração SCORM minimamente funcional, simples e gratuita.
Espero que ajude!
Mais links: