Jestem otwarty na nowe rozwiązania w dziedzinie interfejsu graficznego aplikacji, ale wyświetlanie katalogów przed plikami w managerze plików jest dla mnie aksjomatem. Tymczasem po którejś aktualizacji Debiana Nautilus zaczął pokazywać pliki wymieszane z katalogami (w kolejności alfabetycznej). Co gorsza, nie dało się tego zmienić przez wyklikanie bezpośrednio w Nautilusie. Musiałem uciec się do googlania, który podpowiedział w użycie dconf-editor
i kluczu org.gnome.nautilus.preferences
zaznaczenie opcji sort-directories-first
.
Kategoria: Technikalia
Wpisy na tematy techniczne, związane z programowaniem, komputerami itp.
Checkbox kontra NOT NULL
W encji mamy pole legal1 typu boolean, które nie może być NULL-em.
/** * @var boolean * * @ORM\Column(name="legal1", type="boolean", columnDefinition="TINYINT(1)") */ private $legal1;
W formularzu pole jest prezentowane jako checkbox. Niby wszystko powinno działać, a jednak jeśli wyślemy formularz bez zaznaczania checkboxa to na encji nie jest wywoływana metoda setLegal1()
, a w rezultacie Doctrine próbuje zapisać NULL jako wartość pola legal1, co oczywiście kończy się wyjątkiem.
Można kombinować na samej encji (np. zwracać z getLegal1()
0 jeśli wartość to NULL), ale chyba najbardziej eleganckim rozwiązaniem będzie zastosowanie transformera, który będzie tłumaczył wartości pomiędzy encją a formularzem. Prościutki transformer realizujący takie zadanie może wyglądać tak.
namespace MDurys\CommonBundle\Lib\DataTransformer; use Symfony\Component\Form\DataTransformerInterface; class BoolTransformer implements DataTransformerInterface { public function transform($input) { return boolval($input); } public function reverseTransform($input) { return intval($input); } }
W formularzu należy go zastosować jako model transfomer:
$builder ->add( $builder->create('legal1', 'checkbox')->addModelTransformer(new BoolTransformer()) )
Jego działanie polega na tym, że podczas tworzenia formularza zostanie wywołana metoda transform()
, która wartość z bazy danych (0 lub 1) rzutuje na wartość boolowską. Z kolei podczas zapisywania formularza zostanie wywołana metoda reverseTransform()
, która rzutuje wartość z formularza na liczbę całkowitą. Tym sposobem z NULL-a zrobi się 0.
Dekodowanie daty urodzenia i płci z PESEL-u
Wczoraj poprawiałem walidację numeru PESEL w aktualnym projekcie. PESEL zawiera informacje o płci i dacie urodzenia osoby, do wyciągnięcia tych danych napisałem sobie taką klasę.
class Pesel { public static function extractSex($pesel) { return intval(substr($pesel, 9, 1)) % 2 == 0 ? 1 : 2; } public static function extractDate($pesel) { list($year, $month, $day) = sscanf($pesel, '%02s%02s%02s'); switch (substr($month, 0, 1)) { case 2: case 3: $month -= 20; $year += 2000; break; case 4: case 5: $month -= 40; $year += 2100; case 6: case 7: $month -= 60; $year += 2200; break; case 8: case 9: $month -= 80; $year += 1800; break; default: $year += 1900; break; } return checkdate($month, $day, $year) ? new \DateTime("$year/$month/$day") : null; } }
Metoda extractSex()
zwraca 1 jeśli numer PESEL należy do kobiety lub 2 jeśli do mężczyzny. Metoda extractDate()
zwraca obiekt DateTime
z datą urodzenia z PESEL-u.
W klasie brakuje walidacji poprawności PESEL-u, ale w moim projekcie załatwia to osobny walidator.
aptitude: Błędna suma kontrolna
Kilka dni temu aptitude
po aktualizacji dostępnych pakietów powiedział mi:
W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/main/source/SourcesIndex: Błędna suma kontrolna W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/contrib/source/SourcesIndex: Błędna suma kontrolna W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/non-free/source/SourcesIndex: Błędna suma kontrolna W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/main/binary-amd64/PackagesIndex: Błędna suma kontrolna W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/contrib/binary-amd64/PackagesIndex: Błędna suma kontrolna W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/non-free/binary-amd64/PackagesIndex: Błędna suma kontrolna W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/main/binary-i386/PackagesIndex: Błędna suma kontrolna W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/contrib/binary-i386/PackagesIndex: Błędna suma kontrolna W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/non-free/binary-i386/PackagesIndex: Błędna suma kontrolna W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/contrib/i18n/Translation-enIndex: Błędna suma kontrolna W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/main/i18n/Translation-plIndex: Błędna suma kontrolna W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/main/i18n/Translation-enIndex: Błędna suma kontrolna W: Nie udało się pobrać http://ftp.pl.debian.org/debian/dists/testing/non-free/i18n/Translation-enIndex: Błędna suma kontrolna E: Nie udało się pobrać niektórych plików indeksu, zostały one zignorowane lub użyto ich starszej wersji. E: Nie można przebudować informacji o pakietach
Poproszony o upgrade
odparł zaś, że ma milion niespełnionych zależności i jeśli chcę to mogę go uruchomić z opcją --full-resolver
. Na takie dictum acerbum chwilowo skapitulowałem, wszak nie muszę codziennie mieć najświeższych pakietów. Zakładałem, że coś się skopało na debianowym serwerze i zaraz to naprawią. Jednak i dzień później i w kolejne dni aptitude
twardo obstawał przy swoim. Tego już było za wiele. Trzeba było pokazać, kto tu rządzi:
aptitude update -o Acquire::Pdiffs=false
Powyższa opcja wymusza pobranie pełnych list pakietów, a nie tylko diffów. Pomogło.
Filtry strumieni w PHP
Ostatnio pracowałem nad wymianą plików w formacie CSV. PHP ma funkcje fputcsv()
i fgetcsv()
, które dbają o właściwe umieszczanie/dekodowanie cudzysłowów, więc zadanie jest trywialne. Jedyny problem polegał na tym, że zdalny system zapisywał i łykał pliki z kodowaniem Windows-1250 i znacznikami końca linii CRLF. Oczywiście można traktować iconvem tablice przekazywane do fputcsv()
i zwracane przez fgetcsv()
, przy zapisie można po każdym fputcsv()
zapisywać znacznik LF, ale to rozwiązanie niezbyt eleganckie.
Na szczęście architektura PHP 5 jest elastyczna, funkcje wyjścia/wejścia operują na strumieniach, a do strumieni można dodawać filtry. Powyższy problem można rozwiążać dodając do strumienia filtr, który dokona konwersji kodowania i znaczników końca linii. Wystarczy do tego malutka klasa:
namespace MDurys\DataBundle\StreamFilter; class CP1250CRLFFilter extends \php_user_filter { public function filter($in, $out, &$consumed, $closing) { while ($bucket = stream_bucket_make_writeable($in)) { $bucket->data = str_replace("\n", "\r\n", iconv('UTF-8', 'CP1250', $bucket->data)); $consumed += $bucket->datalen; stream_bucket_append($out, $bucket); } return PSFS_PASS_ON; } }
Plik otwieramy jak zwykle:
if (null === ($fh = fopen($path, 'w'))) { throw new FileException($path); }
Potem wystarczy zarejestrować filtr i dodać go do uchwytu naszego pliku:
stream_filter_register('windows', '\MDurys\DataBundle\StreamFilter\CP1250CRLFFilter'); stream_filter_append($fh, 'windows');
Od tej pory wszystko co zapiszemy do pliku zostanie skonwertowane z UTF-8 na CP1250, a znacznki końca linii zamienione na CRLF.
Edycja boot menu UEFI w Debianie
Mój służbowy laptop (HP Pavilion G6) musiał powędrować do serwisu, więc admin przełożył dysk i pamięć z mojego komputera do identycznego modelu. Na chłopski rozum wszystko powinno ruszyć bez problemu, ale niestety po takiej operacji komputer przywitał mnie komunikatem „Hard Disk – (3F0) BootDevice not found”. Oczywiście opcje UEFI były identycznie ustawione w obydwu komputerach.
Z problemem biedziłem się kilka godzin, zastanawiałem się czy to problem z szyfrowaniem dysku (LUKS), flagą boot partycji, a może identyfikatorami partycji. Problem musiał być trywialny, bo po uruchomieniu systemu z LiveCD partycje można było podmontować, a dane były na miejscu. Na wszelkie wypadek dodam, że ponowna instalacja GRUB-a nie pomogła.
Odkryłem, że jeśli wystartuję komputer z LiveCD, ale zamiast uruchamiać Linuksa wyjdę z menu GRUB-a (Escape, a potem exit) to komputer pokazuje menu UEFI, z którego mogę wybrać urządzenie i plik uruchomieniowy. Na moim dysku komputer widział plik EFI/debian/grubx64.efi
, po wybraniu którego system startował normalnie.
Z pomocą kolegi trafiłem na trop programu efibootmgr
, który służy do edycji menu uruchomieniowego UEFI. Po kilku próbach doszedłem do polecenia, które postawiło mój system na nogi:
efibootmgr --create --label "Debian" --loader \\EFI\\debian\\grubx64.efi
Powyższe polecenia zakłada, że partycja EFI jest na /dev/sda
i jest podmontowana jako /boot/efi
. Jeżeli jest inaczej należy je wskazać odpowiednio przez --disk
i --part
. Użycie backslashy w parametrze --loader
jest istotne.
Efekt działania można sprawdzić poleceniem efibootmgr --verbose
. U mnie wygląda to tak:
BootCurrent: 0001 Timeout: 0 seconds BootOrder: 3001,2001,2002,2003 Boot0001* Debian HD(1,800,f3800,b865a07b-fdff-428d-9443-9fb85820b4f6)File(\EFI\debian\grubx64.efi) Boot2001* USB Drive (UEFI) RC Boot2002* Internal CD/DVD ROM Drive (UEFI) RC Boot3001* Internal Hard Disk RC
SSHFS – problemy i rozwiązania
Trochę wody w Wiśle upłynęło odkąd ostatnio używałem SSHFS, więc kiedy przyszło mi podmontować udział z serwera przez ten protokół nie obyło się bez problemów.
Instalacja
O ile wcześniej tego nie zrobiliśmy to SSHFS trzeba zainstalować:
aptitude install sshfs
Montowanie:
sshfs mdurys@10.0.0.2:/home/mdurys/projekt /home/joe/Projekty/projekt
Moja nazwa użytkownika na zdalnej maszynie jest inna niż na lokalnej, więc musiałem ją podać w ścieżce.
Tu może pojawić się pierwszy problem:
failed to open /dev/fuse: Permission denied
Rozwiązanie jest proste:
usermod -aG fuse joe
Następnie trzeba się przelogować, lub, jeśli ktoś lub starą windowsową szkołę, ponownie uruchomić komputer.
Dla pewności można sprawdzić czy użytkownik trafił do grupy fuse.
groups joe joe : joe cdrom floppy audio dip video plugdev fuse scanner netdev bluetooth
Problem z właścicielem plików
Jeśli nazwy i identyfikatory użytkowników są różne na obu maszynach to wystąpią problemy z dostępem do plików ponieważ SSHFS w lokalnym systemie pokaże takiego właściciela jak na zdalnym systemie.
-rw-r--r-- 1 1001 1001 141 sty 9 11:13 AppCache.php -rw-r--r-- 1 1001 1001 2395 sty 9 11:13 AppKernel.php -rw-r--r-- 1 1001 1001 267 sty 9 11:13 autoload.php -rwxr-xr-x 1 1001 1001 865 sty 9 11:13 console
Można temu zaradzić używając opcji idmap=user
sshfs -o idmap=user mdurys@10.0.0.2:/home/mdurys/projekt /home/joe/Projekty/projekt
Dzięki temu użytkownik dokonujący montowania zostanie zmapowany na użytkownika, na którego się łączymy.
-rw-r--r-- 1 joe 1001 141 sty 9 11:13 AppCache.php -rw-r--r-- 1 joe 1001 2395 sty 9 11:13 AppKernel.php -rw-r--r-- 1 joe 1001 267 sty 9 11:13 autoload.php -rwxr-xr-x 1 joe 1001 865 sty 9 11:13 console
Problem z grupą plików
Jak widać na liście plików z poprzedniego akapitu, pliki mają poprawnego właściciela, ale grupa ciągle nie jest zmapowana. To może powodować problemy z dostępem, ale i na to jest remedium:
sshfs -o idmap=user -o uid=1000 -o gid=1000 mdurys@10.0.0.2:/home/mdurys/projekt /home/joe/Projekty/projekt
Po takim zabiegu również grupy są poprawne:
-rw-r--r-- 1 joe joe 141 sty 9 11:13 AppCache.php -rw-r--r-- 1 joe joe 2395 sty 9 11:13 AppKernel.php -rw-r--r-- 1 joe joe 267 sty 9 11:13 autoload.php -rwxr-xr-x 1 joe joe 865 sty 9 11:13 console
SSHFS w /etc/fstab
Żeby nie wpisywać wszystkiego z palca z każdym razem można definicję zasobu wrzucić do /etc/fstab
. W takim wypadku trzeba podać ścieżkę do klucza, który ma zostać użyty do autoryzacji.
sshfs#mdurys@10.0.0.2:/home/mdurys/projekt /home/joe/Projekty/projekt fuse defaults,IdentityFile=/home/joe/.ssh/id_rsa,uid=1000,gid=1000 0 0
Jeśli zasób możne być niedostępny podczas uruchamiania komputera to nie warto montować go automatycznie, lepiej umożliwić montowanie go przez użytkowników na żądanie.
sshfs#mdurys@10.0.0.2:/home/mdurys/projekt /home/joe/Projekty/projekt fuse defaults,noauto,user,uid=1000,gid=1000 0 0
bash: brak dostępu
Ostatni problem, na który się nartknąłem był związany z uruchamianiem skryptów:
app/console bash: app/console: Brak dostępu
Dzieje się tak ponieważ parametr user
w opcjach montowania implikuje noexec
, co można łatwo sprawdzić poleceniem
mount -l
Oczywiście zamiast app/console
można posługiwać się php app/console
, ale jeśli chcemy korzystać z dobrodziejstw autouzupełniania poleceń albo po prostu oszczędzać klawiaturę to należy dodać parametr exec
:
sshfs#mdurys@10.0.0.2:/home/mdurys/projekt /home/joe/Projekty/projekt fuse defaults,noauto,user,exec,uid=1000,gid=1000 0 0
Lektura dodatkowa
Dlaczego drukowanie dwustronne przestało działać?
Drukowanie dwustronne to obok majtek z gumką jeden z najprzydatniejszych wynalazków naszej ery. Zrozumiecie zatem moje zdumienie i zaniepokojenie, kiedy to po świeżej instalacji systemu stosowna opcja zniknęła z okienka ustawień wydruku.
Przyczyna problemu tkwi w wybranym sterowniku drukarki. Jak widzicie na obrazku, do mojego Lexmarka E450DN Debian proponuje trzy sterowniki. W tej chwili nie pamiętam czemu wybrałem lj5gray, jednak ten sterownik nie pozwala korzystać z dobrodziejstw druku dwustronnego. Po zmianie sterownika na Postscript znowu mogę drukować dwustronnie.
Symfony2, Twig i SwiftMailer czyli rzecz o obrazkach w mailu
Ongiś pisanie maili w HTML-u było naruszeniem netykiety, ale obecnie zdecydowana większość odbiorców jak i nadawców woli otrzymywać maile w HTML-u. Generowanie i wysyłanie takowych w aplikacji opartej na Symfony2 jest banalnie proste dzięki Twigowi i Swift Mailerowi. Nieco mniej przyjemnie zaczyna się robić, kiedy w treści maili ma pojawić się grafika.
Możliwości są dwie i każda rodzi pewne problemy:
- Linkowanie do plików na zdalnym serwerze. Niestety funkcja
url()
w Twigu akceptuje tylko ścieżki zdefiniowane w routingu, nie można nią linkować do grafik. Z kolei znacznikimage
obsługiwany przez Assetica domyślnie generuje relatywne URL-e, a poza tym jest trochę nieporęczny w użyciu. - Osadzenie plików w wiadomości. Tu problem natury programistycznej polega na tym, że żeby osadzić grafikę w wiadomości trzeba skorzystać z metody
Swift_Message::embed()
, do której nie mamy dostępu z poziomu Twiga.
Wszystkie powyższe problemy można rozwiązać tworząc rozszerzenie do Twiga.
namespace MDurys\MailBundle\Twig; use Symfony\Component\DependencyInjection\ContainerInterface; class MailExtension extends \Twig_Extension { private $kernel; private $host; private $imageCache; private $messageCache; public function __construct(ContainerInterface $container) { $context = $container->get('router')->getContext(); $this->host = $context->getScheme() . '://' . $context->getHost(); $this->kernel = $container->get('kernel'); } public function getFunctions() { return array( new \Twig_SimpleFunction('mail_embed', array($this, 'mailEmbed')), new \Twig_SimpleFunction('mail_url', array($this, 'mailUrl')), ); } public function mailEmbed(\Swift_Message $message, $file) { if (!isset($this->messageCache[$message->getId()][$file])) { if (!isset($this->imageCache[$file])) { $this->imageCache[$file] = \Swift_Image::fromPath($this->kernel->locateResource($file)); } $this->messageCache[$message->getId()][$file] = $message->embed($this->imageCache[$file]); } return $this->messageCache[$message->getId()][$file]; } public function mailUrl($file) { return $this->host . $file; } public function getName() { return 'mdurys_mail_extension'; } }
Rozszerzenie udostępnia dwie funkcje Twiga: mail_embed()
i mail_url()
, które odpowiednio umożliwiają osadzenie obrazka w wiadomości i wygenerowanie abosultnego URL-a do zasobu. Osadzane obrazki są cacheowane, więc jeśli dany obrazek jest wykorzystywany kilka razy w danej wiadomości to zostanie osadzony tylko raz, a jeśli obrazek zostanie osadzony w kilku wiadomościach to proces konwersji na obiekt Swift_Image
zostanie wykonany tylko raz.
Aby móc z tego rozszerzenia korzystać należy je uaktywnić w services.yml
bundla. Ważne jest, żeby otagować usługę jako „twig.extension”.
services: mdurys.twig.mail_extension: class: MDurys\MailBundle\Twig\MailExtension arguments: [ "@service_container" ] tags: - { name: twig.extension }
Jeśli chcemy osadzić obrazek w wiadomości to podczas renderowania szablonu trzeba przekazać obiekt wiadomości.
$message = \Swift_Message::newInstance(); $htmlPart = $this->renderView( 'MailBundle:Test:email.html.twig', array('message' => $message) ); $message ->setFrom('mail@example.com') ->setTo('test@fexample.com') ->setSubject('Mail z obrazkami') ->setBody($htmlPart, 'text/html'); $this->get('mailer')->send($message);
W szablonie Twiga wystarczy wywołać funkcję mail_embed()
przekazując jej jako parametry obiekt wiadomości i lokalizację pliku.
<img src="{{ mail_embed(message, '@MDurysMailBundle/Resources/public/images/smiley.gif') }}" width="16" height="16" alt=":-)" />
Linkowanie do zewnętrznych zasobów jest prostsze i sprowadza się do wywołania funkcji mail_url()
w szablonie:
<img src="{{ mail_url('/bundles/mdurysmail/images/big_logo.jpg') }}" width="600" height="240" alt="Big Logo" />
Klucz obcy jako klucz główny w Doctrine2
Jest to jak najbardziej możliwe i bardzo ładnie opisane w dokumentacji Doctrine2. Przykład poniżej.
namespace Acme\Bundle\ShopBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity * @ORM\Table(name="customer_preferences") */ class CustomerPreferences { /** * @ORM\Id * @ORM\OneToOne(targetEntity="Acme\Bundle\ShopBundle\Entity\Customer") * @ORM\JoinColumn(name="customer_id", referencedColumnName="id") */ private $customer; // ... }
W tabeli wygenerowanej z tej encji pole customer_id
będzie zarówno kluczem obcym wskazującym na pole id
w tabeli customer
jak i kluczem głównym.
Z jednej strony wydaje się to oczywiste i intuicyjne, ale parę razy przekonałem się, że rozwiązania w Doctrine2 nie zawsze są oczywiste i intuicyjne. Na szczęście akurat w tym wypadku są.